diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 7acbca17..d6345c82 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -21,7 +21,6 @@ DD2DC2C029BCD8AB003B383C /* HardwareModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2DC2BF29BCD8AB003B383C /* HardwareModels.swift */; }; DD2E65262767A01F00E45FC5 /* NodeDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2E65252767A01F00E45FC5 /* NodeDetail.swift */; }; DD3501892852FC3B000FC853 /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3501882852FC3B000FC853 /* Settings.swift */; }; - DD35018B2852FC79000FC853 /* UserSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD35018A2852FC79000FC853 /* UserSettings.swift */; }; DD3CC6B528E33FD100FA9159 /* ShareChannels.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3CC6B428E33FD100FA9159 /* ShareChannels.swift */; }; DD3CC6BC28E366DF00FA9159 /* Meshtastic.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = DD3CC6BA28E366DF00FA9159 /* Meshtastic.xcdatamodeld */; }; DD3CC6BE28E4CD9800FA9159 /* BatteryGauge.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3CC6BD28E4CD9800FA9159 /* BatteryGauge.swift */; }; @@ -92,7 +91,6 @@ DDA6B2E928419CF2003E8C16 /* MeshPackets.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA6B2E828419CF2003E8C16 /* MeshPackets.swift */; }; DDA6B2EB28420A7B003E8C16 /* NodeAnnotation.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA6B2EA28420A7B003E8C16 /* NodeAnnotation.swift */; }; DDAF8C5326EB1DF10058C060 /* BLEManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDAF8C5226EB1DF10058C060 /* BLEManager.swift */; }; - DDAF8C6E26ED19040058C060 /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDAF8C6D26ED19040058C060 /* Extensions.swift */; }; DDB6ABD628AE742000384BA1 /* BluetoothConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB6ABD528AE742000384BA1 /* BluetoothConfig.swift */; }; DDB6ABD928B0A4BA00384BA1 /* BluetoothModes.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB6ABD828B0A4BA00384BA1 /* BluetoothModes.swift */; }; DDB6ABDB28B0AC6000384BA1 /* DistanceText.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB6ABDA28B0AC6000384BA1 /* DistanceText.swift */; }; @@ -117,6 +115,19 @@ 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 */; }; + 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 */; }; + DDDB444229F8A88700EE2349 /* Double.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDB444129F8A88700EE2349 /* Double.swift */; }; + DDDB444429F8A8DD00EE2349 /* Float.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDB444329F8A8DD00EE2349 /* Float.swift */; }; + DDDB444629F8A96500EE2349 /* Character.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDB444529F8A96500EE2349 /* Character.swift */; }; + DDDB444829F8A9C900EE2349 /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDB444729F8A9C900EE2349 /* String.swift */; }; + DDDB444A29F8AA3A00EE2349 /* CLLocationCoordinate2D.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDB444929F8AA3A00EE2349 /* CLLocationCoordinate2D.swift */; }; + DDDB444C29F8AAA600EE2349 /* Color.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDB444B29F8AAA600EE2349 /* Color.swift */; }; + DDDB444E29F8AB0E00EE2349 /* Int.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDB444D29F8AB0E00EE2349 /* Int.swift */; }; + DDDB445029F8AC9C00EE2349 /* UIImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDB444F29F8AC9C00EE2349 /* UIImage.swift */; }; + DDDB445229F8ACF900EE2349 /* Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDB445129F8ACF900EE2349 /* Date.swift */; }; + DDDB445429F8AD1600EE2349 /* Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDB445329F8AD1600EE2349 /* Data.swift */; }; DDDE59F529AF163D00490C6C /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DD41A61C29AE7E8E003C5A37 /* WidgetKit.framework */; }; DDDE59F629AF163D00490C6C /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DD41A61E29AE7E8F003C5A37 /* SwiftUI.framework */; }; DDDE59F929AF163D00490C6C /* WidgetsBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDE59F829AF163D00490C6C /* WidgetsBundle.swift */; }; @@ -186,7 +197,6 @@ DD2DC2BF29BCD8AB003B383C /* HardwareModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HardwareModels.swift; sourceTree = ""; }; DD2E65252767A01F00E45FC5 /* NodeDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeDetail.swift; sourceTree = ""; }; DD3501882852FC3B000FC853 /* Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = ""; }; - DD35018A2852FC79000FC853 /* UserSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSettings.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 = ""; }; DD3CC6BD28E4CD9800FA9159 /* BatteryGauge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatteryGauge.swift; sourceTree = ""; }; @@ -263,7 +273,6 @@ DDA6B2E828419CF2003E8C16 /* MeshPackets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshPackets.swift; sourceTree = ""; }; DDA6B2EA28420A7B003E8C16 /* NodeAnnotation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeAnnotation.swift; sourceTree = ""; }; DDAF8C5226EB1DF10058C060 /* BLEManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BLEManager.swift; sourceTree = ""; }; - DDAF8C6D26ED19040058C060 /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = ""; }; DDB6ABD528AE742000384BA1 /* BluetoothConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothConfig.swift; sourceTree = ""; }; DDB6ABD828B0A4BA00384BA1 /* BluetoothModes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothModes.swift; sourceTree = ""; }; DDB6ABDA28B0AC6000384BA1 /* DistanceText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DistanceText.swift; sourceTree = ""; }; @@ -297,6 +306,19 @@ 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 = ""; }; + 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 = ""; }; + DDDB444129F8A88700EE2349 /* Double.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Double.swift; sourceTree = ""; }; + DDDB444329F8A8DD00EE2349 /* Float.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Float.swift; sourceTree = ""; }; + DDDB444529F8A96500EE2349 /* Character.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Character.swift; sourceTree = ""; }; + DDDB444729F8A9C900EE2349 /* String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = ""; }; + DDDB444929F8AA3A00EE2349 /* CLLocationCoordinate2D.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLLocationCoordinate2D.swift; sourceTree = ""; }; + DDDB444B29F8AAA600EE2349 /* Color.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Color.swift; sourceTree = ""; }; + DDDB444D29F8AB0E00EE2349 /* Int.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Int.swift; sourceTree = ""; }; + DDDB444F29F8AC9C00EE2349 /* UIImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIImage.swift; sourceTree = ""; }; + DDDB445129F8ACF900EE2349 /* Date.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Date.swift; sourceTree = ""; }; + DDDB445329F8AD1600EE2349 /* Data.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data.swift; sourceTree = ""; }; DDDD527729B5B83F0045BC3C /* MeshtasticDataModelV9.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV9.xcdatamodel; sourceTree = ""; }; DDDE59F429AF163D00490C6C /* WidgetsExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = WidgetsExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; DDDE59F829AF163D00490C6C /* WidgetsBundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetsBundle.swift; sourceTree = ""; }; @@ -365,6 +387,7 @@ C9697F9C279336B700250207 /* LocalMBTileOverlay.swift */, DD964FC32974767D007C176F /* MapViewFitExtension.swift */, DD2AD8A7296D2DF9001FF0E7 /* MapViewSwiftUI.swift */, + DDDB443529F6287000EE2349 /* MapButtons.swift */, ); path = Custom; sourceTree = ""; @@ -549,6 +572,7 @@ DDC2E15626CE248E0042C5E4 /* Meshtastic */ = { isa = PBXGroup; children = ( + DDDB443E29F79A9400EE2349 /* Extensions */, DD90860A26F645B700DC5189 /* Meshtastic.entitlements */, DD8ED9C6289CE4A100B3B0AB /* Enums */, DDC4D5662754996200A4208E /* Persistence */, @@ -609,7 +633,6 @@ isa = PBXGroup; children = ( DD23A50E26FD1B4400D9B90C /* PeripheralModel.swift */, - DD35018A2852FC79000FC853 /* UserSettings.swift */, ); path = Model; sourceTree = ""; @@ -655,11 +678,11 @@ children = ( DDAF8C5226EB1DF10058C060 /* BLEManager.swift */, DDC2E1A626CEB3400042C5E4 /* LocationHelper.swift */, - DDAF8C6D26ED19040058C060 /* Extensions.swift */, DD913638270DFF4C00D7ACF3 /* LocalNotificationManager.swift */, DD8169F8271F1A6100F4AB02 /* MeshLogger.swift */, DDA6B2E828419CF2003E8C16 /* MeshPackets.swift */, DD964FBC296E6B01007C176F /* EmojiOnlyTextField.swift */, + DDDB443C29F6592F00EE2349 /* NetworkManager.swift */, ); path = Helpers; sourceTree = ""; @@ -678,6 +701,24 @@ path = Persistence; sourceTree = ""; }; + DDDB443E29F79A9400EE2349 /* Extensions */ = { + isa = PBXGroup; + children = ( + DDDB444529F8A96500EE2349 /* Character.swift */, + DDDB444929F8AA3A00EE2349 /* CLLocationCoordinate2D.swift */, + DDDB444B29F8AAA600EE2349 /* Color.swift */, + DDDB445329F8AD1600EE2349 /* Data.swift */, + DDDB445129F8ACF900EE2349 /* Date.swift */, + DDDB444129F8A88700EE2349 /* Double.swift */, + DDDB444329F8A8DD00EE2349 /* Float.swift */, + DDDB444D29F8AB0E00EE2349 /* Int.swift */, + DDDB444729F8A9C900EE2349 /* String.swift */, + DDDB444F29F8AC9C00EE2349 /* UIImage.swift */, + DDDB443F29F79AB000EE2349 /* UserDefaults.swift */, + ); + path = Extensions; + sourceTree = ""; + }; DDDE59F729AF163D00490C6C /* Widgets */ = { isa = PBXGroup; children = ( @@ -896,14 +937,15 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + DDDB444829F8A9C900EE2349 /* String.swift in Sources */, DD5E520C298EE33B00D21B61 /* portnums.pb.swift in Sources */, DD457188293C7E63000C49FB /* SignalStrengthIndicator.swift in Sources */, DDFEB3BB29900C1200EE7472 /* CurrentConditionsCompact.swift in Sources */, DD836AE726F6B38600ABCC23 /* Connect.swift in Sources */, - DDAF8C6E26ED19040058C060 /* Extensions.swift in Sources */, DD5E523F298F5A9E00D21B61 /* AirQualityIndexCompact.swift in Sources */, DD964FBF296E76EF007C176F /* WaypointFormView.swift in Sources */, DD3501892852FC3B000FC853 /* Settings.swift in Sources */, + DDDB443629F6287000EE2349 /* MapButtons.swift in Sources */, DD5D0A9C2931B9F200F7EA61 /* EthernetModes.swift in Sources */, DD5E5203298EE33B00D21B61 /* config.pb.swift in Sources */, DD798B072915928D005217CD /* ChannelMessageList.swift in Sources */, @@ -911,11 +953,13 @@ DDC2E1A726CEB3400042C5E4 /* LocationHelper.swift in Sources */, DD5394FE276BA0EF00AD86B1 /* PositionEntityExtension.swift in Sources */, DD913639270DFF4C00D7ACF3 /* LocalNotificationManager.swift in Sources */, + DDDB444C29F8AAA600EE2349 /* Color.swift in Sources */, DD4F23CD28779A3C001D37CB /* EnvironmentMetricsLog.swift in Sources */, DD41A61529AB0035003C5A37 /* NodeWeatherForecast.swift in Sources */, DDB6ABD628AE742000384BA1 /* BluetoothConfig.swift in Sources */, DD769E0328D18BF1001A3F05 /* DeviceMetricsLog.swift in Sources */, DDAF8C5326EB1DF10058C060 /* BLEManager.swift in Sources */, + DDDB445229F8ACF900EE2349 /* Date.swift in Sources */, DDC4D568275499A500A4208E /* Persistence.swift in Sources */, DDD6EEAF29BC024700383354 /* Firmware.swift in Sources */, DD90860E26F69BAE00DC5189 /* NodeMap.swift in Sources */, @@ -923,6 +967,7 @@ DD964FBD296E6B01007C176F /* EmojiOnlyTextField.swift in Sources */, DD8169FF272476C700F4AB02 /* LogDocument.swift in Sources */, DDC94FC129CE063B0082EA6E /* BatteryLevel.swift in Sources */, + DDDB445429F8AD1600EE2349 /* Data.swift in Sources */, DD2AD8A8296D2DF9001FF0E7 /* MapViewSwiftUI.swift in Sources */, DD5E5213298EE33B00D21B61 /* deviceonly.pb.swift in Sources */, DD5E5208298EE33B00D21B61 /* rtttl.pb.swift in Sources */, @@ -930,12 +975,13 @@ DDA0B6B2294CDC55001356EC /* Channels.swift in Sources */, DD4A911E2708C65400501B7E /* AppSettings.swift in Sources */, DD5E5209298EE33B00D21B61 /* module_config.pb.swift in Sources */, - DD35018B2852FC79000FC853 /* UserSettings.swift in Sources */, DD2160AF28C5552500C17253 /* MQTTConfig.swift in Sources */, + DDDB444229F8A88700EE2349 /* Double.swift in Sources */, DD5E520F298EE33B00D21B61 /* cannedmessages.pb.swift in Sources */, DDF924CA26FBB953009FE055 /* ConnectedDevice.swift in Sources */, DD3CC6BE28E4CD9800FA9159 /* BatteryGauge.swift in Sources */, DD6193772862F90F00E59241 /* CannedMessagesConfig.swift in Sources */, + DDDB444A29F8AA3A00EE2349 /* CLLocationCoordinate2D.swift in Sources */, DD41582628582E9B009B0E59 /* DeviceConfig.swift in Sources */, DD3CC6B528E33FD100FA9159 /* ShareChannels.swift in Sources */, DD1BF2F92776FE2E008C8D2F /* UserMessageList.swift in Sources */, @@ -943,6 +989,7 @@ DD3CC6C228EB9D4900FA9159 /* UpdateCoreData.swift in Sources */, DDE0F7C5295F77B700B8AAB3 /* AppSettingsEnums.swift in Sources */, DDB6ABE628B1406100384BA1 /* LoraConfigEnums.swift in Sources */, + DDDB444629F8A96500EE2349 /* Character.swift in Sources */, DD23A50F26FD1B4400D9B90C /* PeripheralModel.swift in Sources */, DDB6ABDB28B0AC6000384BA1 /* DistanceText.swift in Sources */, DD5E520D298EE33B00D21B61 /* storeforward.pb.swift in Sources */, @@ -961,6 +1008,7 @@ DDD9E4E4284B208E003777C5 /* UserEntityExtension.swift in Sources */, DD2553592855B52700E55709 /* PositionConfig.swift in Sources */, DD97E96828EFE9A00056DDA4 /* About.swift in Sources */, + DDDB444029F79AB000EE2349 /* UserDefaults.swift in Sources */, DDB6ABE028B13AC700384BA1 /* DeviceEnums.swift in Sources */, DD86D40C287F401000BAEB7A /* SaveChannelQRCode.swift in Sources */, DDA1C48E28DB49D3009933EC /* ChannelRoles.swift in Sources */, @@ -974,6 +1022,7 @@ DDDE5A1029AFE69700490C6C /* MeshActivityAttributes.swift in Sources */, DD1925B928CDA93900720036 /* SerialConfigEnums.swift in Sources */, DD86D4112881D16900BAEB7A /* WriteCsvFile.swift in Sources */, + DDDB445029F8AC9C00EE2349 /* UIImage.swift in Sources */, DD86D40F2881BE4C00BAEB7A /* CsvDocument.swift in Sources */, DDA6B2E928419CF2003E8C16 /* MeshPackets.swift in Sources */, DDCE4E2C2869F92900BE9F8F /* UserConfig.swift in Sources */, @@ -986,6 +1035,7 @@ DD5E520E298EE33B00D21B61 /* mqtt.pb.swift in Sources */, DD994B69295F88B60013760A /* IntervalEnums.swift in Sources */, DD415828285859C4009B0E59 /* TelemetryConfig.swift in Sources */, + DDDB443D29F6592F00EE2349 /* NetworkManager.swift in Sources */, DD73FD1128750779000852D6 /* PositionLog.swift in Sources */, DD5E5206298EE33B00D21B61 /* localonly.pb.swift in Sources */, DD3CC6C028E7A60700FA9159 /* MessagingEnums.swift in Sources */, @@ -993,6 +1043,7 @@ DD5E523A298EFA5300D21B61 /* TelemetryWeather.swift in Sources */, C9697F9D279336B700250207 /* LocalMBTileOverlay.swift in Sources */, DD58C5F22919AD3C00D5BEFB /* ChannelEntityExtension.swift in Sources */, + DDDB444E29F8AB0E00EE2349 /* Int.swift in Sources */, DD0F791B28713C8A00A6FDAD /* AdminMessageList.swift in Sources */, DD3CC6BC28E366DF00FA9159 /* Meshtastic.xcdatamodeld in Sources */, DD5E5210298EE33B00D21B61 /* telemetry.pb.swift in Sources */, @@ -1000,6 +1051,7 @@ DD8169F9271F1A6100F4AB02 /* MeshLogger.swift in Sources */, DD41582A28585C32009B0E59 /* RangeTestConfig.swift in Sources */, DD1925B728CDA5A400720036 /* CannedMessagesConfigEnums.swift in Sources */, + DDDB444429F8A8DD00EE2349 /* Float.swift in Sources */, DD5E5211298EE33B00D21B61 /* remote_hardware.pb.swift in Sources */, DD5E5204298EE33B00D21B61 /* xmodem.pb.swift in Sources */, DDC2E15826CE248E0042C5E4 /* MeshtasticApp.swift in Sources */, diff --git a/Meshtastic/Assets.xcassets/alpha.imageset/Contents.json b/Meshtastic/Assets.xcassets/alpha.imageset/Contents.json new file mode 100644 index 00000000..eb9d7ea9 --- /dev/null +++ b/Meshtastic/Assets.xcassets/alpha.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "alpha.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Meshtastic/Assets.xcassets/alpha.imageset/alpha.png b/Meshtastic/Assets.xcassets/alpha.imageset/alpha.png new file mode 100644 index 00000000..ab6681ba Binary files /dev/null and b/Meshtastic/Assets.xcassets/alpha.imageset/alpha.png differ diff --git a/Meshtastic/Enums/AppSettingsEnums.swift b/Meshtastic/Enums/AppSettingsEnums.swift index 35d75707..994a0e90 100644 --- a/Meshtastic/Enums/AppSettingsEnums.swift +++ b/Meshtastic/Enums/AppSettingsEnums.swift @@ -33,16 +33,16 @@ enum KeyboardType: Int, CaseIterable, Identifiable { } } -enum MeshMapType: String, CaseIterable, Identifiable { +enum MeshMapTypes: Int, CaseIterable, Identifiable { - case standard - case mutedStandard - case hybrid - case hybridFlyover - case satellite - case satelliteFlyover + case standard = 0 + case mutedStandard = 5 + case hybrid = 2 + case hybridFlyover = 4 + case satellite = 1 + case satelliteFlyover = 3 - var id: String { self.rawValue } + var id: Int { self.rawValue } var description: String { switch self { @@ -97,6 +97,13 @@ enum UserTrackingModes: Int, CaseIterable, Identifiable { return NSLocalizedString("map.usertrackingmode.followwithheading", comment: "Follow with Heading") } } + var icon: String { + switch self { + case .none: return "location" + case .follow: return "location.fill" + case .followWithHeading: return "location.north.line.fill" + } + } func MKUserTrackingModeValue() -> MKUserTrackingMode { switch self { diff --git a/Meshtastic/Extensions/CLLocationCoordinate2D.swift b/Meshtastic/Extensions/CLLocationCoordinate2D.swift new file mode 100644 index 00000000..32a47774 --- /dev/null +++ b/Meshtastic/Extensions/CLLocationCoordinate2D.swift @@ -0,0 +1,20 @@ +// +// CLLocationCoordinate2D.swift +// Meshtastic +// +// Copyright(c) Garth Vander Houwen 4/25/23. +// + +import Foundation +import MapKit + +extension CLLocationCoordinate2D { + /// Returns distance from coordianate in meters. + /// - Parameter from: coordinate which will be used as end point. + /// - Returns: distance in meters. + func distance(from: CLLocationCoordinate2D) -> CLLocationDistance { + let from = CLLocation(latitude: from.latitude, longitude: from.longitude) + let to = CLLocation(latitude: self.latitude, longitude: self.longitude) + return from.distance(from: to) + } +} diff --git a/Meshtastic/Extensions/Character.swift b/Meshtastic/Extensions/Character.swift new file mode 100644 index 00000000..ea607901 --- /dev/null +++ b/Meshtastic/Extensions/Character.swift @@ -0,0 +1,15 @@ +// +// Character.swift +// Meshtastic +// +// Copyright(c) Garth Vander Houwen 4/25/23. +// + +import Foundation + +extension Character { + var isEmoji: Bool { + guard let scalar = unicodeScalars.first else { return false } + return scalar.properties.isEmoji && (scalar.value >= 0x203C || unicodeScalars.count > 1) + } +} diff --git a/Meshtastic/Extensions/Color.swift b/Meshtastic/Extensions/Color.swift new file mode 100644 index 00000000..1ebd0e44 --- /dev/null +++ b/Meshtastic/Extensions/Color.swift @@ -0,0 +1,55 @@ +// +// Color.swift +// Meshtastic +// +// Created by Garth Vander Houwen on 4/25/23. +// + +import Foundation +import SwiftUI +import UIKit + +extension Color { + /// Returns a boolean for a SwiftUI Color to determine what color of text to use + /// - Returns: true if the color is light + func isLight() -> Bool { + guard let components = cgColor?.components, components.count > 2 else {return false} + let brightness = ((components[0] * 299) + (components[1] * 587) + (components[2] * 114)) / 1000 + return (brightness > 0.5) + } +} + +extension UIColor { + + /// Returns a boolean indicating if a color is light + /// - Returns: true if the color is light + func isLight() -> Bool { + guard let components = cgColor.components, components.count > 2 else {return false} + let brightness = ((components[0] * 299) + (components[1] * 587) + (components[2] * 114)) / 1000 + return (brightness > 0.5) + } + + /// Returns a UInt32 from a UIColor + /// - Returns: UInt32 + var hex: UInt32 { + var red: CGFloat = 0, green: CGFloat = 0, blue: CGFloat = 0, alpha: CGFloat = 0 + getRed(&red, green: &green, blue: &blue, alpha: &alpha) + var value: UInt32 = 0 + value += UInt32(1.0 * 255) << 24 + value += UInt32(red * 255) << 16 + value += UInt32(green * 255) << 8 + value += UInt32(blue * 255) + return value + } + + /// Returns a UIColor from a UInt32 value + /// - Parameter hex: UInt32 value to convert to a color + /// - Returns: UIColor + convenience init(hex: UInt32) { + let red = CGFloat((hex & 0xFF0000) >> 16) + let green = CGFloat((hex & 0x00FF00) >> 8) + let blue = CGFloat((hex & 0x0000FF)) + //print("\(red) - \(green) - \(blue)") + self.init(red: red/255.0, green: green/255.0, blue: blue/255.0, alpha: 1.0) + } +} diff --git a/Meshtastic/Extensions/Data.swift b/Meshtastic/Extensions/Data.swift new file mode 100644 index 00000000..efb7d375 --- /dev/null +++ b/Meshtastic/Extensions/Data.swift @@ -0,0 +1,18 @@ +// +// Data.swift +// Meshtastic +// +// Copyright(c) Garth Vander Houwen 4/25/23. +// + +import Foundation + +extension Data { + var macAddressString: String { + let mac: String = reduce("") {$0 + String(format: "%02x:", $1)} + return String(mac.dropLast()) + } + var hexDescription: String { + return reduce("") {$0 + String(format: "%02x", $1)} + } +} diff --git a/Meshtastic/Extensions/Date.swift b/Meshtastic/Extensions/Date.swift new file mode 100644 index 00000000..bac54654 --- /dev/null +++ b/Meshtastic/Extensions/Date.swift @@ -0,0 +1,20 @@ +// +// Date.swift +// Meshtastic +// +// Copyright(c) Garth Vander Houwen 4/25/23. +// + +import Foundation + +extension Date { + static var currentTimeStamp: Int64 { + return Int64(Date().timeIntervalSince1970 * 1000) + } + + func formattedDate(format: String) -> String { + let dateformat = DateFormatter() + dateformat.dateFormat = format + return dateformat.string(from: self) + } +} diff --git a/Meshtastic/Extensions/Double.swift b/Meshtastic/Extensions/Double.swift new file mode 100644 index 00000000..c63a9abb --- /dev/null +++ b/Meshtastic/Extensions/Double.swift @@ -0,0 +1,19 @@ +// +// Double.swift +// Meshtastic +// +// Copyright(c) Garth Vander Houwen on 4/25/23. +// +import Foundation + +extension Double { + + var toBytes: String { + let formatter = MeasurementFormatter() + let measurement = Measurement(value: self, unit: UnitInformationStorage.bytes) + formatter.unitStyle = .short + formatter.unitOptions = .naturalScale + formatter.numberFormatter.maximumFractionDigits = 0 + return formatter.string(from: measurement.converted(to: .megabytes)) + } +} diff --git a/Meshtastic/Extensions/Float.swift b/Meshtastic/Extensions/Float.swift new file mode 100644 index 00000000..bd217833 --- /dev/null +++ b/Meshtastic/Extensions/Float.swift @@ -0,0 +1,27 @@ +// +// Float.swift +// Meshtastic +// +// Copyright(c) Garth Vander Houwen 4/25/23. +// + +import Foundation + +extension Float { + + func formattedTemperature() -> String { + let temperature = Measurement(value: Double(self), unit: .celsius) + return temperature.formatted(.measurement(width: .abbreviated, usage: .weather)) + } + func localeTemperature() -> Double { + let temperature = Measurement(value: Double(self), unit: .celsius) + let locale = NSLocale.current as NSLocale + let localeUnit = locale.object(forKey: NSLocale.Key(rawValue: "kCFLocaleTemperatureUnitKey")) + var format: UnitTemperature = .celsius + + if localeUnit! as? String == "Fahrenheit" { + format = .fahrenheit + } + return temperature.converted(to: format).value + } +} diff --git a/Meshtastic/Extensions/Int.swift b/Meshtastic/Extensions/Int.swift new file mode 100644 index 00000000..40a12d8f --- /dev/null +++ b/Meshtastic/Extensions/Int.swift @@ -0,0 +1,17 @@ +// +// Int.swift +// Meshtastic +// +// Copyright(c) Garth Vander Houwen 4/25/23. +// + +extension Int { + + func numberOfDigits() -> Int { + if abs(self) < 10 { + return 1 + } else { + return 1 + (self/10).numberOfDigits() + } + } +} diff --git a/Meshtastic/Extensions/String.swift b/Meshtastic/Extensions/String.swift new file mode 100644 index 00000000..802faca0 --- /dev/null +++ b/Meshtastic/Extensions/String.swift @@ -0,0 +1,56 @@ +// +// String.swift +// Meshtastic +// +// Copyright(c) Garth Vander Houwen 4/25/23. +// + +import Foundation +import UIKit + +extension String { + + func base64urlToBase64() -> String { + var base64 = self + .replacingOccurrences(of: "-", with: "+") + .replacingOccurrences(of: "_", with: "/") + if base64.count % 4 != 0 { + base64.append(String(repeating: "==", count: 4 - base64.count % 4)) + } + return base64 + } + + func base64ToBase64url() -> String { + let base64url = self + .replacingOccurrences(of: "+", with: "-") + .replacingOccurrences(of: "/", with: "_") + .replacingOccurrences(of: "=", with: "") + return base64url + } + + func onlyEmojis() -> Bool { + return count > 0 && !contains { !$0.isEmoji } + } + + func image(fontSize: CGFloat = 40, bgColor: UIColor = UIColor.clear, imageSize: CGSize? = nil) -> UIImage? { + let font = UIFont.systemFont(ofSize: fontSize) + let attributes = [NSAttributedString.Key.font: font] + let imageSize = imageSize ?? self.size(withAttributes: attributes) + UIGraphicsBeginImageContextWithOptions(imageSize, false, 0) + bgColor.set() + let rect = CGRect(origin: .zero, size: imageSize) + UIRectFill(rect) + self.draw(in: rect, withAttributes: [.font: font]) + let image = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + return image + } + + func camelCaseToWords() -> String { + return unicodeScalars.dropFirst().reduce(String(prefix(1))) { + return CharacterSet.uppercaseLetters.contains($1) + ? $0 + " " + String($1) + : $0 + String($1) + } + } +} diff --git a/Meshtastic/Extensions/UIImage.swift b/Meshtastic/Extensions/UIImage.swift new file mode 100644 index 00000000..55e96a7d --- /dev/null +++ b/Meshtastic/Extensions/UIImage.swift @@ -0,0 +1,26 @@ +// +// UIImage.swift +// Meshtastic +// +// Copyright(c) Garth Vander Houwen 4/25/23. +// + +import Foundation +import UIKit + +extension UIImage { + func rotate(radians: Float) -> UIImage? { + var newSize = CGRect(origin: CGPoint.zero, size: self.size).applying(CGAffineTransform(rotationAngle: CGFloat(radians))).size + newSize.width = floor(newSize.width) + newSize.height = floor(newSize.height) + UIGraphicsBeginImageContextWithOptions(newSize, false, self.scale) + let context = UIGraphicsGetCurrentContext()! + context.translateBy(x: newSize.width/2, y: newSize.height/2) + context.rotate(by: CGFloat(radians)) + self.draw(in: CGRect(x: -self.size.width/2, y: -self.size.height/2, width: self.size.width, height: self.size.height)) + let newImage = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + + return newImage + } +} diff --git a/Meshtastic/Extensions/UserDefaults.swift b/Meshtastic/Extensions/UserDefaults.swift new file mode 100644 index 00000000..fdfdb575 --- /dev/null +++ b/Meshtastic/Extensions/UserDefaults.swift @@ -0,0 +1,130 @@ +// +// UserDefaults.swift +// Meshtastic +// +// Copyright(c) Garth Vander Houwen 4/24/23. +// + +import Foundation + +extension UserDefaults { + + enum Keys: String, CaseIterable { + case hasBeenLaunched + case meshtasticUsername + case preferredPeripheralId + case provideLocation + case provideLocationInterval + case meshMapType + case meshMapCenteringMode + case meshMapRecentering + case meshMapCustomTileServer + case meshMapShowNodeHistory + case meshMapShowRouteLines + } + + func reset() { + Keys.allCases.forEach { removeObject(forKey: $0.rawValue) } + } + + static var hasBeenLaunched: Bool { + get { + let result = UserDefaults.standard.bool(forKey: "hasBeenLaunched") + UserDefaults.standard.set(true, forKey: "hasBeenLaunched") + return result + } set { + UserDefaults.standard.set(newValue, forKey: "hasBeenLaunched") + } + } + + static var meshtasticUsername: String { + get { + UserDefaults.standard.string(forKey: "meshtasticUsername") ?? "" + } + set { + UserDefaults.standard.set(newValue, forKey: "meshtasticUsername") + } + } + + static var preferredPeripheralId: String { + get { + UserDefaults.standard.string(forKey: "preferredPeripheralId") ?? "" + } + set { + UserDefaults.standard.set(newValue, forKey: "preferredPeripheralId") + } + } + + static var provideLocation: Bool { + get { + let result = UserDefaults.standard.bool(forKey: "provideLocation") + UserDefaults.standard.set(true, forKey: "provideLocation") + return result + } set { + UserDefaults.standard.set(newValue, forKey: "provideLocation") + } + } + + static var provideLocationInterval: Int { + get { + UserDefaults.standard.integer(forKey: "provideLocationInterval") + } + set { + UserDefaults.standard.set(newValue, forKey: "provideLocationInterval") + } + } + + static var mapType: Int { + get { + UserDefaults.standard.integer(forKey: "meshMapType") + } + set { + UserDefaults.standard.set(newValue, forKey: "meshMapType") + } + } + + static var enableMapRecentering: Bool { + get { + UserDefaults.standard.bool(forKey: "meshMapRecentering") + } + set { + UserDefaults.standard.set(newValue, forKey: "meshMapRecentering") + } + } + + static var enableMapNodeHistoryPins: Bool { + get { + UserDefaults.standard.bool(forKey: "meshMapShowNodeHistory") + } + set { + UserDefaults.standard.set(newValue, forKey: "meshMapShowNodeHistory") + } + } + + static var enableMapRouteLines: Bool { + get { + UserDefaults.standard.bool(forKey: "meshMapShowRouteLines") + } + set { + UserDefaults.standard.set(newValue, forKey: "meshMapShowRouteLines") + } + } + + static var enableOfflineMaps: Bool { + get { + UserDefaults.standard.bool(forKey: "enableOfflineMaps") + } + set { + UserDefaults.standard.set(newValue, forKey: "enableOfflineMaps") + } + } + + static var mapTileServer: String { + get { + UserDefaults.standard.string(forKey: "mapTileServer") ?? "" + } + set { + UserDefaults.standard.set(newValue, forKey: "mapTileServer") + } + } +} diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index c37d9757..7bae38a6 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -17,7 +17,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { } } var context: NSManagedObjectContext? - var userSettings: UserSettings? + //var userSettings: UserSettings? private var centralManager: CBCentralManager! private let restoreKey = "Meshtastic.BLE.Manager" @Published var peripherals: [Peripheral] = [] @@ -157,10 +157,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) { isConnecting = false isConnected = true - if userSettings?.preferredPeripheralId.count ?? 0 < 1 { - userSettings?.preferredPeripheralId = peripheral.identifier.uuidString + if UserDefaults.preferredPeripheralId.count < 1 { + UserDefaults.preferredPeripheralId = peripheral.identifier.uuidString } - UserDefaults.standard.synchronize() // Invalidate and reset connection timer count timeoutTimerCount = 0 if timeoutTimer != nil { @@ -579,11 +578,11 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { // MARK: Share Location Position Update Timer // Use context to pass the radio name with the timer // Use a RunLoop to prevent the timer from running on the main UI thread - if userSettings?.provideLocation ?? false { + if UserDefaults.provideLocation ?? false { if positionTimer != nil { positionTimer!.invalidate() } - positionTimer = Timer.scheduledTimer(timeInterval: TimeInterval((userSettings?.provideLocationInterval ?? 900)), target: self, selector: #selector(positionTimerFired), userInfo: context, repeats: true) + positionTimer = Timer.scheduledTimer(timeInterval: TimeInterval((UserDefaults.provideLocationInterval ?? 900)), target: self, selector: #selector(positionTimerFired), userInfo: context, repeats: true) if positionTimer != nil { RunLoop.current.add(positionTimer!, forMode: .common) } @@ -612,7 +611,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { self.startScanning() // Try and connect to the preferredPeripherial first - let preferredPeripheral = peripherals.filter({ $0.peripheral.identifier.uuidString == UserDefaults.standard.object(forKey: "preferredPeripheralId") as? String ?? "" }).first + let preferredPeripheral = peripherals.filter({ $0.peripheral.identifier.uuidString == UserDefaults.preferredPeripheralId as? String ?? "" }).first if preferredPeripheral != nil && preferredPeripheral?.peripheral != nil { connectTo(peripheral: preferredPeripheral!.peripheral) } @@ -829,7 +828,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { // Check for connected node if connectedPeripheral != nil { // Send a position out to the mesh if "share location with the mesh" is enabled in settings - if userSettings!.provideLocation { + if UserDefaults.provideLocation { let _ = sendPosition(destNum: connectedPeripheral.num, wantResponse: false, smartPosition: true) } } diff --git a/Meshtastic/Helpers/Extensions.swift b/Meshtastic/Helpers/Extensions.swift deleted file mode 100644 index 2e5d7794..00000000 --- a/Meshtastic/Helpers/Extensions.swift +++ /dev/null @@ -1,204 +0,0 @@ -import Foundation -import SwiftUI -import MapKit - -extension Character { - var isEmoji: Bool { - guard let scalar = unicodeScalars.first else { return false } - return scalar.properties.isEmoji && (scalar.value >= 0x203C || unicodeScalars.count > 1) - } -} - -extension CLLocationCoordinate2D { - /// Returns distance from coordianate in meters. - /// - Parameter from: coordinate which will be used as end point. - /// - Returns: distance in meters. - func distance(from: CLLocationCoordinate2D) -> CLLocationDistance { - let from = CLLocation(latitude: from.latitude, longitude: from.longitude) - let to = CLLocation(latitude: self.latitude, longitude: self.longitude) - return from.distance(from: to) - } -} - -extension Color { - /// Returns a boolean for a SwiftUI Color to determine what color of text to use - /// - Returns: true if the color is light - func isLight() -> Bool { - guard let components = cgColor?.components, components.count > 2 else {return false} - let brightness = ((components[0] * 299) + (components[1] * 587) + (components[2] * 114)) / 1000 - return (brightness > 0.5) - } -} - -extension UIColor { - - /// Returns a boolean indicating if a color is light - /// - Returns: true if the color is light - func isLight() -> Bool { - guard let components = cgColor.components, components.count > 2 else {return false} - let brightness = ((components[0] * 299) + (components[1] * 587) + (components[2] * 114)) / 1000 - return (brightness > 0.5) - } - - /// Returns a UInt32 from a UIColor - /// - Returns: UInt32 - var hex: UInt32 { - var red: CGFloat = 0, green: CGFloat = 0, blue: CGFloat = 0, alpha: CGFloat = 0 - getRed(&red, green: &green, blue: &blue, alpha: &alpha) - var value: UInt32 = 0 - value += UInt32(1.0 * 255) << 24 - value += UInt32(red * 255) << 16 - value += UInt32(green * 255) << 8 - value += UInt32(blue * 255) - return value - } - - /// Returns a UIColor from a UInt32 value - /// - Parameter hex: UInt32 value to convert to a color - /// - Returns: UIColor - convenience init(hex: UInt32) { - let red = CGFloat((hex & 0xFF0000) >> 16) - let green = CGFloat((hex & 0x00FF00) >> 8) - let blue = CGFloat((hex & 0x0000FF)) - //print("\(red) - \(green) - \(blue)") - self.init(red: red/255.0, green: green/255.0, blue: blue/255.0, alpha: 1.0) - } -} - -extension Data { - var macAddressString: String { - let mac: String = reduce("") {$0 + String(format: "%02x:", $1)} - return String(mac.dropLast()) - } - var hexDescription: String { - return reduce("") {$0 + String(format: "%02x", $1)} - } -} - -extension Date { - static var currentTimeStamp: Int64 { - return Int64(Date().timeIntervalSince1970 * 1000) - } - - func formattedDate(format: String) -> String { - let dateformat = DateFormatter() - dateformat.dateFormat = format - return dateformat.string(from: self) - } -} - -extension Float { - - func formattedTemperature() -> String { - let temperature = Measurement(value: Double(self), unit: .celsius) - return temperature.formatted(.measurement(width: .abbreviated, usage: .weather)) - } - func localeTemperature() -> Double { - let temperature = Measurement(value: Double(self), unit: .celsius) - let locale = NSLocale.current as NSLocale - let localeUnit = locale.object(forKey: NSLocale.Key(rawValue: "kCFLocaleTemperatureUnitKey")) - var format: UnitTemperature = .celsius - - if localeUnit! as? String == "Fahrenheit" { - format = .fahrenheit - } - return temperature.converted(to: format).value - } -} - -extension Int { - - func numberOfDigits() -> Int { - if abs(self) < 10 { - return 1 - } else { - return 1 + (self/10).numberOfDigits() - } - } -} - -extension UIImage { - func rotate(radians: Float) -> UIImage? { - var newSize = CGRect(origin: CGPoint.zero, size: self.size).applying(CGAffineTransform(rotationAngle: CGFloat(radians))).size - newSize.width = floor(newSize.width) - newSize.height = floor(newSize.height) - UIGraphicsBeginImageContextWithOptions(newSize, false, self.scale) - let context = UIGraphicsGetCurrentContext()! - context.translateBy(x: newSize.width/2, y: newSize.height/2) - context.rotate(by: CGFloat(radians)) - self.draw(in: CGRect(x: -self.size.width/2, y: -self.size.height/2, width: self.size.width, height: self.size.height)) - let newImage = UIGraphicsGetImageFromCurrentImageContext() - UIGraphicsEndImageContext() - - return newImage - } -} - -extension String { - - func base64urlToBase64() -> String { - var base64 = self - .replacingOccurrences(of: "-", with: "+") - .replacingOccurrences(of: "_", with: "/") - if base64.count % 4 != 0 { - base64.append(String(repeating: "==", count: 4 - base64.count % 4)) - } - return base64 - } - - func base64ToBase64url() -> String { - let base64url = self - .replacingOccurrences(of: "+", with: "-") - .replacingOccurrences(of: "/", with: "_") - .replacingOccurrences(of: "=", with: "") - return base64url - } - - func onlyEmojis() -> Bool { - return count > 0 && !contains { !$0.isEmoji } - } - - func image(fontSize: CGFloat = 40, bgColor: UIColor = UIColor.clear, imageSize: CGSize? = nil) -> UIImage? { - let font = UIFont.systemFont(ofSize: fontSize) - let attributes = [NSAttributedString.Key.font: font] - let imageSize = imageSize ?? self.size(withAttributes: attributes) - UIGraphicsBeginImageContextWithOptions(imageSize, false, 0) - bgColor.set() - let rect = CGRect(origin: .zero, size: imageSize) - UIRectFill(rect) - self.draw(in: rect, withAttributes: [.font: font]) - let image = UIGraphicsGetImageFromCurrentImageContext() - UIGraphicsEndImageContext() - return image - } - - func camelCaseToWords() -> String { - return unicodeScalars.dropFirst().reduce(String(prefix(1))) { - return CharacterSet.uppercaseLetters.contains($1) - ? $0 + " " + String($1) - : $0 + String($1) - } - } -} - -extension UserDefaults { - - enum Keys: String, CaseIterable { - case meshtasticUsername - case preferredPeripheralId - case provideLocation - case provideLocationInterval - case keyboardType - case meshMapType - case meshMapCenteringMode - case meshMapRecentering - case meshMapCustomTileServer - case meshMapUserTrackingMode - case meshMapShowNodeHistory - case meshMapShowRouteLines - } - - func reset() { - Keys.allCases.forEach { removeObject(forKey: $0.rawValue) } - } -} diff --git a/Meshtastic/Helpers/LocationHelper.swift b/Meshtastic/Helpers/LocationHelper.swift index 26291962..1c8c22a1 100644 --- a/Meshtastic/Helpers/LocationHelper.swift +++ b/Meshtastic/Helpers/LocationHelper.swift @@ -1,55 +1,69 @@ +import Foundation import CoreLocation -class LocationHelper: NSObject, ObservableObject { - +class LocationHelper: NSObject, ObservableObject, CLLocationManagerDelegate { + static let shared = LocationHelper() + var locationManager = CLLocationManager() + + @Published var authorizationStatus: CLAuthorizationStatus? + + override init() { + + super.init() + locationManager.delegate = self + locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters + locationManager.pausesLocationUpdatesAutomatically = true + locationManager.allowsBackgroundLocationUpdates = true + locationManager.activityType = .otherNavigation + } // Apple Park static let DefaultLocation = CLLocationCoordinate2D(latitude: 37.3346, longitude: -122.0090) static let DefaultAltitude = CLLocationDistance(integerLiteral: 0) static let DefaultSpeed = CLLocationSpeed(integerLiteral: 0) static let DefaultHeading = CLLocationDirection(integerLiteral: 0) - + static var currentLocation: CLLocationCoordinate2D { - + guard let location = shared.locationManager.location else { return DefaultLocation } return location.coordinate } - + static var currentAltitude: CLLocationDistance { - + guard let altitude = shared.locationManager.location?.altitude else { return DefaultAltitude } return altitude } - + static var currentSpeed: CLLocationSpeed { - + guard let speed = shared.locationManager.location?.speed else { return DefaultSpeed } return speed } - + static var currentHeading: CLLocationDirection { - + guard let heading = shared.locationManager.location?.course else { return DefaultHeading } return heading } - + static var currentTimestamp: Date { - + guard let timestamp = shared.locationManager.location?.timestamp else { return Date.now } return timestamp } - + static var satsInView: Int { // If we have a position we have a sat var sats = 1 @@ -74,29 +88,32 @@ class LocationHelper: NSObject, ObservableObject { return sats } - private let locationManager = CLLocationManager() - - private override init() { - - super.init() - locationManager.delegate = self - locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters - locationManager.pausesLocationUpdatesAutomatically = true - locationManager.allowsBackgroundLocationUpdates = true - locationManager.activityType = .otherNavigation - locationManager.requestWhenInUseAuthorization() - locationManager.startUpdatingLocation() - } -} - -extension LocationHelper: CLLocationManagerDelegate { - func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { } - - public func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { - print("Location manager failed with error: \(error.localizedDescription)") - } - - public func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) { - print("Location manager changed the status: \(status)") + func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { + switch manager.authorizationStatus { + case .authorizedWhenInUse: + authorizationStatus = .authorizedWhenInUse + locationManager.requestLocation() + break + case .restricted: + authorizationStatus = .restricted + break + case .denied: + authorizationStatus = .denied + break + case .notDetermined: + authorizationStatus = .notDetermined + locationManager.requestWhenInUseAuthorization() + break + default: + break + } + } + + func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { + + } + + func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { + print("Location manager error: \(error.localizedDescription)") } } diff --git a/Meshtastic/Helpers/NetworkManager.swift b/Meshtastic/Helpers/NetworkManager.swift new file mode 100644 index 00000000..61d3a70a --- /dev/null +++ b/Meshtastic/Helpers/NetworkManager.swift @@ -0,0 +1,29 @@ +// +// NetworkManager.swift +// Meshtastic +// +// Copyright(c) Garth Vander Houwen on 4/23/23. +// + +import Foundation +import Network + +class NetworkManager { + + static let shared = NetworkManager() + + // MARK: - Public methods + func runIfNetwork(completion: @escaping ()->() ) { + let pathMonitor = NWPathMonitor() + pathMonitor.pathUpdateHandler = { + guard $0.status == .satisfied else { + // No network available + return pathMonitor.cancel() + } + pathMonitor.cancel() + completion() + } + pathMonitor.start(queue: DispatchQueue.global(qos: .background)) + } + +} diff --git a/Meshtastic/MeshtasticApp.swift b/Meshtastic/MeshtasticApp.swift index c37b214f..49046122 100644 --- a/Meshtastic/MeshtasticApp.swift +++ b/Meshtastic/MeshtasticApp.swift @@ -8,7 +8,6 @@ struct MeshtasticAppleApp: App { let persistenceController = PersistenceController.shared @ObservedObject private var bleManager: BLEManager = BLEManager() - @ObservedObject private var userSettings: UserSettings = UserSettings() @Environment(\.scenePhase) var scenePhase @State var saveChannels = false @@ -20,7 +19,6 @@ struct MeshtasticAppleApp: App { ContentView() .environment(\.managedObjectContext, persistenceController.container.viewContext) .environmentObject(bleManager) - .environmentObject(userSettings) .sheet(isPresented: $saveChannels) { SaveChannelQRCode(channelSetLink: channelSettings ?? "Empty Channel URL", bleManager: bleManager) .presentationDetents([.medium, .large]) diff --git a/Meshtastic/Model/UserSettings.swift b/Meshtastic/Model/UserSettings.swift deleted file mode 100644 index 1c44c965..00000000 --- a/Meshtastic/Model/UserSettings.swift +++ /dev/null @@ -1,98 +0,0 @@ -// -// UserSettings.swift -// MeshtasticApple -// -// Created by Garth Vander Houwen on 6/9/22. -// - -import Foundation - -class UserSettings: ObservableObject { - @Published var meshtasticUsername: String { - didSet { - UserDefaults.standard.set(meshtasticUsername, forKey: "meshtasticusername") - UserDefaults.standard.synchronize() - } - } - @Published var preferredPeripheralId: String { - didSet { - UserDefaults.standard.set(preferredPeripheralId, forKey: "preferredPeripheralId") - UserDefaults.standard.synchronize() - } - } - @Published var provideLocation: Bool { - didSet { - UserDefaults.standard.set(provideLocation, forKey: "provideLocation") - } - } - @Published var provideLocationInterval: Int { - didSet { - UserDefaults.standard.set(provideLocationInterval, forKey: "provideLocationInterval") - UserDefaults.standard.synchronize() - } - } - @Published var keyboardType: Int { - didSet { - UserDefaults.standard.set(keyboardType, forKey: "keyboardType") - UserDefaults.standard.synchronize() - } - } - @Published var meshMapType: String { - didSet { - UserDefaults.standard.set(meshMapType, forKey: "meshMapType") - UserDefaults.standard.synchronize() - } - } - @Published var meshMapCenteringMode: Int { - didSet { - UserDefaults.standard.set(meshMapCenteringMode, forKey: "meshMapCenteringMode") - UserDefaults.standard.synchronize() - } - } - @Published var meshMapRecentering: Bool { - didSet { - UserDefaults.standard.set(meshMapRecentering, forKey: "meshMapRecentering") - UserDefaults.standard.synchronize() - } - } - @Published var meshMapCustomTileServer: String { - didSet { - UserDefaults.standard.set(meshMapCustomTileServer, forKey: "meshMapCustomTileServer") - UserDefaults.standard.synchronize() - } - } - @Published var meshMapUserTrackingMode: Int { - didSet { - UserDefaults.standard.set(meshMapUserTrackingMode, forKey: "meshMapUserTrackingMode") - UserDefaults.standard.synchronize() - } - } - @Published var meshMapShowNodeHistory: Bool { - didSet { - UserDefaults.standard.set(meshMapShowNodeHistory, forKey: "meshMapShowNodeHistory") - UserDefaults.standard.synchronize() - } - } - @Published var meshMapShowRouteLines: Bool { - didSet { - UserDefaults.standard.set(meshMapShowRouteLines, forKey: "meshMapShowRouteLines") - UserDefaults.standard.synchronize() - } - } - - init() { - - self.meshtasticUsername = UserDefaults.standard.object(forKey: "meshtasticusername") as? String ?? "" - self.preferredPeripheralId = UserDefaults.standard.object(forKey: "preferredPeripheralId") as? String ?? "" - self.provideLocation = UserDefaults.standard.object(forKey: "provideLocation") as? Bool ?? false - self.provideLocationInterval = UserDefaults.standard.object(forKey: "provideLocationInterval") as? Int ?? 900 - self.keyboardType = UserDefaults.standard.object(forKey: "keyboardType") as? Int ?? 0 - self.meshMapType = UserDefaults.standard.string(forKey: "meshMapType") ?? "standard" - self.meshMapCenteringMode = UserDefaults.standard.object(forKey: "meshMapCenteringMode") as? Int ?? 0 - self.meshMapRecentering = UserDefaults.standard.object(forKey: "meshMapRecentering") as? Bool ?? false - self.meshMapCustomTileServer = UserDefaults.standard.string(forKey: "meshMapCustomTileServer") ?? "" - self.meshMapUserTrackingMode = UserDefaults.standard.object(forKey: "meshMapUserTrackingMode") as? Int ?? 0 - self.meshMapShowNodeHistory = UserDefaults.standard.object(forKey: "meshMapShowNodeHistory") as? Bool ?? true - self.meshMapShowRouteLines = UserDefaults.standard.object(forKey: "meshMapShowRouteLines") as? Bool ?? false - } -} diff --git a/Meshtastic/Persistence/WaypointEntityExtension.swift b/Meshtastic/Persistence/WaypointEntityExtension.swift index 1744c30e..968381b2 100644 --- a/Meshtastic/Persistence/WaypointEntityExtension.swift +++ b/Meshtastic/Persistence/WaypointEntityExtension.swift @@ -55,3 +55,10 @@ extension WaypointEntity: MKAnnotation { String(expire != nil ? "\nāŒ› Expires \(String(describing: expire?.formatted()))" : "") + String(locked > 0 ? "\nšŸ”’ Locked" : "") } } + +struct WaypointCoordinate: Identifiable { + + let id: UUID + let coordinate: CLLocationCoordinate2D? + let waypointId: Int64 +} diff --git a/Meshtastic/Views/Bluetooth/Connect.swift b/Meshtastic/Views/Bluetooth/Connect.swift index 57594b2f..12af7eba 100644 --- a/Meshtastic/Views/Bluetooth/Connect.swift +++ b/Meshtastic/Views/Bluetooth/Connect.swift @@ -18,7 +18,7 @@ struct Connect: View { @Environment(\.managedObjectContext) var context @EnvironmentObject var bleManager: BLEManager - @EnvironmentObject var userSettings: UserSettings + //@EnvironmentObject var userSettings: UserSettings @State var node: NodeInfoEntity? @State var isUnsetRegion = false @State var invalidFirmwareVersion = false @@ -176,7 +176,7 @@ struct Connect: View { Section(header: Text("available.radios").font(.title)) { ForEach(bleManager.peripherals.filter({ $0.peripheral.state == CBPeripheralState.disconnected }).sorted(by: { $0.name < $1.name })) { peripheral in HStack { - if userSettings.preferredPeripheralId == peripheral.peripheral.identifier.uuidString { + if UserDefaults.preferredPeripheralId == peripheral.peripheral.identifier.uuidString { Image(systemName: "star.fill") .imageScale(.large).foregroundColor(.yellow) .padding(.trailing) @@ -187,7 +187,7 @@ struct Connect: View { } Button(action: { - if userSettings.preferredPeripheralId.count > 0 && peripheral.peripheral.identifier.uuidString != userSettings.preferredPeripheralId { + if UserDefaults.preferredPeripheralId.count > 0 && peripheral.peripheral.identifier.uuidString != UserDefaults.preferredPeripheralId { presentingSwitchPreferredPeripheral = true selectedPeripherialId = peripheral.peripheral.identifier.uuidString } else { @@ -208,7 +208,7 @@ struct Connect: View { Button("Connect to new radio?", role: .destructive) { bleManager.stopScanning() bleManager.connectedPeripheral = nil - userSettings.preferredPeripheralId = "" + UserDefaults.preferredPeripheralId = "" if bleManager.connectedPeripheral != nil && bleManager.connectedPeripheral.peripheral.state == CBPeripheralState.connected { bleManager.disconnectPeripheral() } @@ -267,7 +267,7 @@ struct Connect: View { } .onChange(of: (self.bleManager.isSubscribed)) { sub in - if userSettings.preferredPeripheralId.count > 0 && sub { + if UserDefaults.preferredPeripheralId.count > 0 && sub { let fetchNodeInfoRequest: NSFetchRequest = NSFetchRequest.init(entityName: "NodeInfoEntity") fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(bleManager.connectedPeripheral?.num ?? -1)) @@ -292,7 +292,6 @@ struct Connect: View { } .onAppear(perform: { self.bleManager.context = context - self.bleManager.userSettings = userSettings }) } #if canImport(ActivityKit) diff --git a/Meshtastic/Views/ContentView.swift b/Meshtastic/Views/ContentView.swift index 448d5696..9ca1e2b7 100644 --- a/Meshtastic/Views/ContentView.swift +++ b/Meshtastic/Views/ContentView.swift @@ -6,8 +6,6 @@ import SwiftUI struct ContentView: View { - @EnvironmentObject var userSettings: UserSettings - @State private var selection: Tab = .ble enum Tab { diff --git a/Meshtastic/Views/Map/Custom/LocalMBTileOverlay.swift b/Meshtastic/Views/Map/Custom/LocalMBTileOverlay.swift index 6ab30afb..239a4156 100644 --- a/Meshtastic/Views/Map/Custom/LocalMBTileOverlay.swift +++ b/Meshtastic/Views/Map/Custom/LocalMBTileOverlay.swift @@ -108,3 +108,46 @@ class LocalMBTileOverlay: MKTileOverlay { } } } + +//public class CustomMapOverlaySource: MKTileOverlay { +// +// // requires folder: tiles/{mapName}/z/y/y,{tileType} +// private var parent: MapViewSwiftUI +// private let mapName: String +// private let tileType: String +// private let defaultTile: DefaultTile? +// +// public init( +// parent: MapViewSwiftUI, +// mapName: String, +// tileType: String, +// defaultTile: DefaultTile? +// ) { +// self.parent = parent +// self.mapName = mapName +// self.tileType = tileType +// self.defaultTile = defaultTile +// super.init(urlTemplate: "") +// } +// +// public override func url(forTilePath path: MKTileOverlayPath) -> URL { +// if let tileUrl = Bundle.main.url( +// forResource: "\(path.y)", +// withExtension: self.tileType, +// subdirectory: "tiles/\(self.mapName)/\(path.z)/\(path.x)", +// localization: nil +// ) { +// return tileUrl +// } else if let defaultTile = self.defaultTile, let defaultTileUrl = Bundle.main.url( +// forResource: defaultTile.tileName, +// withExtension: defaultTile.tileType, +// subdirectory: "tiles/\(self.mapName)", +// localization: nil +// ) { +// return defaultTileUrl +// } else { +// let urlstring = self.mapName+"\(path.z)/\(path.x)/\(path.y).png" +// return URL(string: urlstring)! +// } +// } +//} diff --git a/Meshtastic/Views/Map/Custom/MapButtons.swift b/Meshtastic/Views/Map/Custom/MapButtons.swift new file mode 100644 index 00000000..a2dd3e58 --- /dev/null +++ b/Meshtastic/Views/Map/Custom/MapButtons.swift @@ -0,0 +1,67 @@ +// +// MapButtons.swift +// Meshtastic +// +// Copyright Ā© Garth Vander Houwen 4/23/23. +// + +import SwiftUI + +struct MapButtons: View { + let buttonWidth: CGFloat = 22 + let width: CGFloat = 45 + @Binding var tracking: UserTrackingModes + @Binding var isPresentingInfoSheet: Bool + + var body: some View { + VStack() { + let impactLight = UIImpactFeedbackGenerator(style: .light) + Button(action: { + self.isPresentingInfoSheet.toggle() + }) { + Image(systemName: isPresentingInfoSheet ? "info.circle.fill" : "info.circle") + .resizable() + .frame(width: buttonWidth, height: buttonWidth, alignment: .center) + .offset(y: -2) + } + Divider() + Button(action: { + switch self.tracking { + case .none: + self.tracking = .follow + case .follow: + self.tracking = .followWithHeading + case .followWithHeading: + self.tracking = .none + } + impactLight.impactOccurred() + }) { + Image(systemName: tracking.icon) + .frame(width: buttonWidth, height: buttonWidth, alignment: .center) + .offset(y: 3) + } + } + .frame(width: width, height: width*2, alignment: .center) + .background(Color(UIColor.systemBackground)) + .cornerRadius(8) + .shadow(radius: 1) + .offset(x: 3, y: 25) + + } +} + +// MARK: Previews +struct MapControl_Previews: PreviewProvider { + @State static var tracking: UserTrackingModes = .none + @State static var isPresentingInfoSheet = false + static var previews: some View { + Group { + MapButtons(tracking: $tracking, isPresentingInfoSheet: $isPresentingInfoSheet) + .environment(\.colorScheme, .light) + MapButtons(tracking: $tracking, isPresentingInfoSheet: $isPresentingInfoSheet) + .environment(\.colorScheme, .dark) + } + + .previewLayout(.fixed(width: 60, height: 100)) + } +} diff --git a/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift b/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift index b44ce223..d3959646 100644 --- a/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift +++ b/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift @@ -23,14 +23,18 @@ struct MapViewSwiftUI: UIViewRepresentable { let userTrackingMode: MKUserTrackingMode let showNodeHistory: Bool let showRouteLines: Bool - @AppStorage("meshMapRecentering") private var recenter: Bool = false // Offline Map Tiles @AppStorage("lastUpdatedLocalMapFile") private var lastUpdatedLocalMapFile = 0 @State private var loadedLastUpdatedLocalMapFile = 0 var customMapOverlay: CustomMapOverlay? @State private var presentCustomMapOverlayHash: CustomMapOverlay? + // Custom Tile Server + var tileRenderer: MKTileOverlayRenderer? + let tileServer: MapTileServerLinks = .openStreetMaps - func makeUIView(context: Context) -> MKMapView { + // MARK: Private methods + + private func configureMap(mapView: MKMapView) { // Map View Parameters mapView.mapType = mapViewType mapView.addAnnotations(waypoints) @@ -64,108 +68,149 @@ struct MapViewSwiftUI: UIViewRepresentable { mapView.showsBuildings = true mapView.showsScale = true mapView.showsTraffic = true - #if targetEnvironment(macCatalyst) // Show the default always visible compass and the mac only controls mapView.showsCompass = true mapView.showsZoomControls = true mapView.showsPitchControl = true #else - #if os(iOS) - // Hide the default compass that only appears when you are not going north and instead always show the compass in the bottom right corner of the map + // Move the default compass under the mapbuttons control mapView.showsCompass = false - let compassButton = MKCompassButton(mapView: mapView) // Make a new compass - compassButton.compassVisibility = .visible // Make it visible - mapView.addSubview(compassButton) // Add it to the view - compassButton.translatesAutoresizingMaskIntoConstraints = false - compassButton.trailingAnchor.constraint(equalTo: mapView.trailingAnchor, constant: -5).isActive = true - compassButton.bottomAnchor.constraint(equalTo: mapView.bottomAnchor, constant: -25).isActive = true + let compass = MKCompassButton(mapView: mapView) + compass.translatesAutoresizingMaskIntoConstraints = false + compass.compassVisibility = .adaptive + mapView.addSubview(compass) + compass.trailingAnchor.constraint(equalTo: mapView.trailingAnchor, constant: -5).isActive = true + compass.topAnchor.constraint(equalTo: mapView.topAnchor, constant: 145).isActive = true #endif - #endif + } + + func makeUIView(context: Context) -> MKMapView { mapView.delegate = context.coordinator + self.configureMap(mapView: mapView) return mapView } func updateUIView(_ mapView: MKMapView, context: Context) { - mapView.mapType = mapViewType - if self.customMapOverlay != self.presentCustomMapOverlayHash || self.loadedLastUpdatedLocalMapFile != self.lastUpdatedLocalMapFile { - mapView.removeOverlays(mapView.overlays) - if self.customMapOverlay != nil { - - let fileManager = FileManager.default - let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first! - let tilePath = documentsDirectory.appendingPathComponent("offline_map.mbtiles", isDirectory: false).path - if fileManager.fileExists(atPath: tilePath) { - print("Loading local map file") - if let overlay = LocalMBTileOverlay(mbTilePath: tilePath) { - overlay.canReplaceMapContent = false// customMapOverlay.canReplaceMapContent - mapView.addOverlay(overlay) + // Offline maps and tile server settings + if UserDefaults.enableOfflineMaps { + + if UserDefaults.mapTileServer.count > 0 { + tileRenderer?.alpha = 0.0 + let overlays = mapView.overlays + if mapView.mapType == .standard { + let overlay = MKTileOverlay(urlTemplate: UserDefaults.mapTileServer) + if overlays.contains(where: {$0 is MKPolyline}) { + mapView.addOverlay(overlay, level: .aboveLabels) + if let poly_overlay = overlays.filter({$0 is MKPolyline}).first { + mapView.addOverlay(poly_overlay, level: .aboveLabels) + } + } else { + mapView.addOverlay(overlay, level: .aboveLabels) + } } else { - print("Couldn't find a local map file to load") + for overlay in overlays { + if let ove = overlay as? MKTileOverlay { + mapView.removeOverlay(ove) + } + } + } + } else if self.customMapOverlay != self.presentCustomMapOverlayHash || self.loadedLastUpdatedLocalMapFile != self.lastUpdatedLocalMapFile { + mapView.removeOverlays(mapView.overlays) + if self.customMapOverlay != nil { + + let fileManager = FileManager.default + let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first! + let tilePath = documentsDirectory.appendingPathComponent("offline_map.mbtiles", isDirectory: false).path + if fileManager.fileExists(atPath: tilePath) { + print("Loading local map file") + if let overlay = LocalMBTileOverlay(mbTilePath: tilePath) { + overlay.canReplaceMapContent = false// customMapOverlay.canReplaceMapContent + mapView.addOverlay(overlay) + } + } else { + print("Couldn't find a local map file to load") + } + } + DispatchQueue.main.async { + self.presentCustomMapOverlayHash = self.customMapOverlay + self.loadedLastUpdatedLocalMapFile = self.lastUpdatedLocalMapFile } - } - DispatchQueue.main.async { - self.presentCustomMapOverlayHash = self.customMapOverlay - self.loadedLastUpdatedLocalMapFile = self.lastUpdatedLocalMapFile } } - DispatchQueue.main.async { - let latest = positions - .filter { $0.latest == true } - .sorted { $0.nodePosition?.num ?? 0 > $1.nodePosition?.num ?? -1 } - let annotationCount = waypoints.count + (showNodeHistory ? positions.count : latest.count) - - - if annotationCount != mapView.annotations.count { - print("Annotation Count: \(annotationCount) Map Annotations: \(mapView.annotations.count)") - mapView.removeAnnotations(mapView.annotations) - mapView.addAnnotations(waypoints) - if showRouteLines { - // Remove all existing PolyLine Overlays - for overlay in mapView.overlays { - if overlay is MKPolyline { - mapView.removeOverlay(overlay) - } - } - var lineIndex = 0 - for position in latest { - - let nodePositions = positions.filter { $0.nodePosition?.num ?? 0 == position.nodePosition?.num ?? -1 } - let lineCoords = nodePositions.map ({ - (position) -> CLLocationCoordinate2D in - return position.nodeCoordinate! - }) - let polyline = MKPolyline(coordinates: lineCoords, count: nodePositions.count) - polyline.title = "\(String(position.nodePosition?.num ?? 0))" - mapView.addOverlay(polyline) - lineIndex += 1 - // There are 18 colors for lines, start over if we are at index 17 - if lineIndex > 17 { - lineIndex = 0 - } - } + let latest = positions + .filter { $0.latest == true } + .sorted { $0.nodePosition?.num ?? 0 > $1.nodePosition?.num ?? -1 } + + // Node Route Lines + if showRouteLines { + // Remove all existing PolyLine Overlays + for overlay in mapView.overlays { + if overlay is MKPolyline { + mapView.removeOverlay(overlay) } - if userTrackingMode == MKUserTrackingMode.none { - mapView.showsUserLocation = false - mapView.addAnnotations(showNodeHistory ? positions : latest) - if recenter { - mapView.fit(annotations:showNodeHistory || showRouteLines ? positions : latest, andShow: false) - } - } else { - // Centering Done by tracking mode - mapView.addAnnotations(showNodeHistory ? positions : latest) - mapView.showsUserLocation = true + } + var lineIndex = 0 + for position in latest { + + let nodePositions = positions.filter { $0.nodePosition?.num ?? 0 == position.nodePosition?.num ?? -1 } + let lineCoords = nodePositions.map ({ + (position) -> CLLocationCoordinate2D in + return position.nodeCoordinate! + }) + let polyline = MKPolyline(coordinates: lineCoords, count: nodePositions.count) + polyline.title = "\(String(position.nodePosition?.num ?? 0))" + mapView.addOverlay(polyline, level: .aboveLabels) + lineIndex += 1 + // There are 18 colors for lines, start over if we are at index 17 + if lineIndex > 17 { + lineIndex = 0 + } + } + } else { + // Remove all existing PolyLine Overlays + for overlay in mapView.overlays { + if overlay is MKPolyline { + mapView.removeOverlay(overlay) } - mapView.setUserTrackingMode(userTrackingMode, animated: true) } } + + let annotationCount = waypoints.count + (showNodeHistory ? positions.count : latest.count) + if annotationCount != mapView.annotations.count { + print("Annotation Count: \(annotationCount) Map Annotations: \(mapView.annotations.count)") + mapView.removeAnnotations(mapView.annotations) + mapView.addAnnotations(waypoints) + + } + if userTrackingMode == MKUserTrackingMode.none { + mapView.showsUserLocation = false + + if UserDefaults.enableMapRecentering { + if annotationCount != mapView.annotations.count { + mapView.addAnnotations(showNodeHistory ? positions : latest) + } + if latest.count > 1 { + mapView.fitAllAnnotations() + } else { + mapView.fit(annotations:showNodeHistory ? positions : latest, andShow: false) + } + } + } else { + // Centering Done by tracking mode + if annotationCount != mapView.annotations.count { + mapView.addAnnotations(showNodeHistory ? positions : latest) + } + mapView.showsUserLocation = true + } + mapView.setUserTrackingMode(userTrackingMode, animated: true) } func makeCoordinator() -> MapCoordinator { @@ -418,47 +463,4 @@ struct MapViewSwiftUI: UIViewRepresentable { self.defaultTile = defaultTile } } - - public class CustomMapOverlaySource: MKTileOverlay { - - // requires folder: tiles/{mapName}/z/y/y,{tileType} - private var parent: MapViewSwiftUI - private let mapName: String - private let tileType: String - private let defaultTile: DefaultTile? - - public init( - parent: MapViewSwiftUI, - mapName: String, - tileType: String, - defaultTile: DefaultTile? - ) { - self.parent = parent - self.mapName = mapName - self.tileType = tileType - self.defaultTile = defaultTile - super.init(urlTemplate: "") - } - - public override func url(forTilePath path: MKTileOverlayPath) -> URL { - if let tileUrl = Bundle.main.url( - forResource: "\(path.y)", - withExtension: self.tileType, - subdirectory: "tiles/\(self.mapName)/\(path.z)/\(path.x)", - localization: nil - ) { - return tileUrl - } else if let defaultTile = self.defaultTile, let defaultTileUrl = Bundle.main.url( - forResource: defaultTile.tileName, - withExtension: defaultTile.tileType, - subdirectory: "tiles/\(self.mapName)", - localization: nil - ) { - return defaultTileUrl - } else { - let urlstring = self.mapName+"\(path.z)/\(path.x)/\(path.y).png" - return URL(string: urlstring)! - } - } - } } diff --git a/Meshtastic/Views/Map/WaypointFormView.swift b/Meshtastic/Views/Map/WaypointFormView.swift index 0127e6f8..4f1171b6 100644 --- a/Meshtastic/Views/Map/WaypointFormView.swift +++ b/Meshtastic/Views/Map/WaypointFormView.swift @@ -12,32 +12,29 @@ struct WaypointFormView: View { @EnvironmentObject var bleManager: BLEManager @Environment(\.dismiss) private var dismiss - @State var coordinate: CLLocationCoordinate2D - @State var waypointId: Int = 0 - + @State var coordinate: WaypointCoordinate @FocusState private var iconIsFocused: Bool - @State private var name: String = "" @State private var description: String = "" @State private var icon: String = "šŸ“" @State private var latitude: Double = 0 @State private var longitude: Double = 0 @State private var expires: Bool = false - @State private var expire: Date = Date() // = Date.now.addingTimeInterval(60 * 120) // 1 minute * 120 = 2 Hours + @State private var expire: Date = Date.now.addingTimeInterval(60 * 480) // 1 minute * 480 = 8 Hours @State private var locked: Bool = false @State private var lockedTo: Int64 = 0 var body: some View { Form { - let distance = CLLocation(latitude: LocationHelper.currentLocation.latitude, longitude: LocationHelper.currentLocation.longitude).distance(from: CLLocation(latitude: coordinate.latitude, longitude: coordinate.longitude)) - Section(header: Text((waypointId > 0) ? "Editing Waypoint" : "Create Waypoint")) { + let distance = CLLocation(latitude: LocationHelper.currentLocation.latitude, longitude: LocationHelper.currentLocation.longitude).distance(from: CLLocation(latitude: coordinate.coordinate?.latitude ?? 0, longitude: coordinate.coordinate?.longitude ?? 0)) + Section(header: Text((coordinate.waypointId > 0) ? "Editing Waypoint" : "Create Waypoint")) { HStack { Text("Location: \(String(format: "%.5f", latitude) + "," + String(format: "%.5f", longitude))") .textSelection(.enabled) .foregroundColor(Color.gray) .font(.caption2) - if coordinate.latitude != LocationHelper.DefaultLocation.latitude && coordinate.longitude != LocationHelper.DefaultLocation.longitude { + if coordinate.coordinate?.latitude ?? 0 != 0 && coordinate.coordinate?.longitude ?? 0 != 0 { DistanceText(meters: distance) .foregroundColor(Color.gray) .font(.caption2) @@ -128,23 +125,26 @@ struct WaypointFormView: View { Button { var newWaypoint = Waypoint() - - if waypointId > 0 { - newWaypoint.id = UInt32(waypointId) + // Loading a waypoint from edit + if coordinate.waypointId > 0 { + newWaypoint.id = UInt32(coordinate.waypointId) + let waypoint = getWaypoint(id: Int64(coordinate.waypointId), context: bleManager.context!) + newWaypoint.latitudeI = waypoint.latitudeI + newWaypoint.longitudeI = waypoint.longitudeI } else { + // New waypoint newWaypoint.id = UInt32.random(in: UInt32(UInt8.max).. 0 ? name : "Dropped Pin" newWaypoint.description_p = description - newWaypoint.latitudeI = Int32(coordinate.latitude * 1e7) - newWaypoint.longitudeI = Int32(coordinate.longitude * 1e7) // Unicode scalar value for the icon emoji string let unicodeScalers = icon.unicodeScalars // First element as an UInt32 let unicode = unicodeScalers[unicodeScalers.startIndex].value newWaypoint.icon = unicode if locked { - if lockedTo == 0 { newWaypoint.lockedTo = UInt32(bleManager.connectedPeripheral!.num) } else { @@ -157,10 +157,8 @@ struct WaypointFormView: View { newWaypoint.expire = 0 } if bleManager.sendWaypoint(waypoint: newWaypoint) { - waypointId = 0 dismiss() } else { - waypointId = 0 dismiss() print("Send waypoint failed") } @@ -183,11 +181,11 @@ struct WaypointFormView: View { .controlSize(.large) .padding(.bottom) - if waypointId > 0 { + if coordinate.waypointId > 0 { Menu { Button("For me", action: { - let waypoint = getWaypoint(id: Int64(waypointId), context: bleManager.context!) + let waypoint = getWaypoint(id: Int64(coordinate.waypointId), context: bleManager.context!) bleManager.context!.delete(waypoint) do { try bleManager.context!.save() @@ -198,20 +196,19 @@ struct WaypointFormView: View { Button("For everyone", action: { var newWaypoint = Waypoint() - if waypointId > 0 { - newWaypoint.id = UInt32(waypointId) + if coordinate.waypointId > 0 { + newWaypoint.id = UInt32(coordinate.waypointId) } newWaypoint.name = name.count > 0 ? name : "Dropped Pin" newWaypoint.description_p = description - newWaypoint.latitudeI = Int32(coordinate.latitude * 1e7) - newWaypoint.longitudeI = Int32(coordinate.longitude * 1e7) + newWaypoint.latitudeI = Int32(coordinate.coordinate?.latitude ?? 0 * 1e7) + newWaypoint.longitudeI = Int32(coordinate.coordinate?.longitude ?? 0 * 1e7) // Unicode scalar value for the icon emoji string let unicodeScalers = icon.unicodeScalars // First element as an UInt32 let unicode = unicodeScalers[unicodeScalers.startIndex].value newWaypoint.icon = unicode if locked { - if lockedTo == 0 { newWaypoint.lockedTo = UInt32(bleManager.connectedPeripheral!.num) } else { @@ -220,10 +217,8 @@ struct WaypointFormView: View { } newWaypoint.expire = 1 if bleManager.sendWaypoint(waypoint: newWaypoint) { - waypointId = 0 dismiss() } else { - waypointId = 0 dismiss() print("Send waypoint failed") } @@ -239,14 +234,9 @@ struct WaypointFormView: View { .padding(.bottom) } } - .onChange(of: waypointId) { newId in - print(newId) - - } .onAppear { - if waypointId > 0 { - let waypoint = getWaypoint(id: Int64(waypointId), context: bleManager.context!) - waypointId = Int(waypoint.id) + if coordinate.waypointId > 0 { + let waypoint = getWaypoint(id: Int64(coordinate.waypointId), context: bleManager.context!) name = waypoint.name ?? "Dropped Pin" description = waypoint.longDescription ?? "" icon = String(UnicodeScalar(Int(waypoint.icon)) ?? "šŸ“") @@ -267,10 +257,10 @@ struct WaypointFormView: View { description = "" locked = false expires = false - expire = Date.now.addingTimeInterval(60 * 120) + expire = Date.now.addingTimeInterval(60 * 480) icon = "šŸ“" - latitude = coordinate.latitude - longitude = coordinate.longitude + latitude = coordinate.coordinate?.latitude ?? 0 + longitude = coordinate.coordinate?.longitude ?? 0 } } } diff --git a/Meshtastic/Views/Messages/ChannelMessageList.swift b/Meshtastic/Views/Messages/ChannelMessageList.swift index 0934b690..e8d4b14e 100644 --- a/Meshtastic/Views/Messages/ChannelMessageList.swift +++ b/Meshtastic/Views/Messages/ChannelMessageList.swift @@ -12,7 +12,6 @@ struct ChannelMessageList: View { @Environment(\.managedObjectContext) var context @EnvironmentObject var bleManager: BLEManager - @EnvironmentObject var userSettings: UserSettings enum Field: Hashable { case messageText @@ -249,9 +248,9 @@ struct ChannelMessageList: View { Button { let userLongName = bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.longName : "Unknown" sendPositionWithMessage = true - if userSettings.meshtasticUsername.count > 0 { + if UserDefaults.meshtasticUsername.count > 0 { - typingMessage += "šŸ“ " + userSettings.meshtasticUsername + " has shared their position with you from node " + userLongName + typingMessage += "šŸ“ " + UserDefaults.meshtasticUsername + " has shared their position with you from node " + userLongName } else { @@ -275,7 +274,6 @@ struct ChannelMessageList: View { HStack(alignment: .top) { ZStack { - let kbType = UIKeyboardType(rawValue: UserDefaults.standard.object(forKey: "keyboardType") as? Int ?? 0) TextField("message", text: $typingMessage, axis: .vertical) .onChange(of: typingMessage, perform: { value in totalBytes = value.utf8.count @@ -290,7 +288,7 @@ struct ChannelMessageList: View { } } }) - .keyboardType(kbType!) + .keyboardType(.default) .toolbar { ToolbarItemGroup(placement: .keyboard) { Button("dismiss.keyboard") { @@ -313,12 +311,11 @@ struct ChannelMessageList: View { Button { let userLongName = bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.longName : "Unknown" sendPositionWithMessage = true - if userSettings.meshtasticUsername.count > 0 { + if UserDefaults.meshtasticUsername.count > 0 { - typingMessage = "šŸ“ " + userSettings.meshtasticUsername + " has shared their position with you from node " + userLongName + typingMessage = "šŸ“ " + UserDefaults.meshtasticUsername + " has shared their position with you from node " + userLongName } else { - typingMessage = "šŸ“ " + userLongName + " has shared their position with you." } diff --git a/Meshtastic/Views/Messages/Contacts.swift b/Meshtastic/Views/Messages/Contacts.swift index 8292d080..f1f3dc9c 100644 --- a/Meshtastic/Views/Messages/Contacts.swift +++ b/Meshtastic/Views/Messages/Contacts.swift @@ -12,7 +12,6 @@ struct Contacts: View { @Environment(\.managedObjectContext) var context @EnvironmentObject var bleManager: BLEManager - @ObservedObject private var userSettings: UserSettings = UserSettings() @FetchRequest( sortDescriptors: [NSSortDescriptor(key: "longName", ascending: true)], @@ -136,7 +135,6 @@ struct Contacts: View { } } .padding([.top, .bottom]) - } } Section(header: Text("direct.messages")) { @@ -251,9 +249,8 @@ struct Contacts: View { MeshtasticLogo() ) .onAppear { - self.bleManager.userSettings = userSettings self.bleManager.context = context - if userSettings.preferredPeripheralId.count > 0 { + if UserDefaults.preferredPeripheralId.count > 0 { let fetchNodeInfoRequest: NSFetchRequest = NSFetchRequest.init(entityName: "NodeInfoEntity") fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(bleManager.connectedPeripheral?.num ?? -1)) do { diff --git a/Meshtastic/Views/Messages/UserMessageList.swift b/Meshtastic/Views/Messages/UserMessageList.swift index fa2661be..0a654061 100644 --- a/Meshtastic/Views/Messages/UserMessageList.swift +++ b/Meshtastic/Views/Messages/UserMessageList.swift @@ -12,7 +12,6 @@ struct UserMessageList: View { @Environment(\.managedObjectContext) var context @EnvironmentObject var bleManager: BLEManager - @EnvironmentObject var userSettings: UserSettings enum Field: Hashable { case messageText @@ -243,8 +242,8 @@ struct UserMessageList: View { let userLongName = bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.longName : "Unknown" sendPositionWithMessage = true - if userSettings.meshtasticUsername.count > 0 { - typingMessage = "šŸ“ " + userSettings.meshtasticUsername + " has shared their position with you from node " + userLongName + " and requested a response with your position." + if UserDefaults.meshtasticUsername.count > 0 { + typingMessage = "šŸ“ " + UserDefaults.meshtasticUsername + " has shared their position with you from node " + userLongName + " and requested a response with your position." } else { typingMessage = "šŸ“ " + userLongName + " has shared their position and requested a response with your position." } @@ -266,7 +265,6 @@ struct UserMessageList: View { HStack(alignment: .top) { ZStack { - let kbType = UIKeyboardType(rawValue: UserDefaults.standard.object(forKey: "keyboardType") as? Int ?? 0) TextField("message", text: $typingMessage, axis: .vertical) .onChange(of: typingMessage, perform: { value in totalBytes = value.utf8.count @@ -281,7 +279,7 @@ struct UserMessageList: View { } } }) - .keyboardType(kbType!) + .keyboardType(.default) .toolbar { ToolbarItemGroup(placement: .keyboard) { Button("dismiss.keyboard") { @@ -293,8 +291,8 @@ struct UserMessageList: View { let userLongName = bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.longName : "Unknown" sendPositionWithMessage = true - if userSettings.meshtasticUsername.count > 0 { - typingMessage = "šŸ“ " + userSettings.meshtasticUsername + " has shared their position with you from node " + userLongName + " and requested a response with your position." + if UserDefaults.meshtasticUsername.count > 0 { + typingMessage = "šŸ“ " + UserDefaults.meshtasticUsername + " has shared their position with you from node " + userLongName + " and requested a response with your position." } else { typingMessage = "šŸ“ " + userLongName + " has shared their position and requested a response with your position." } diff --git a/Meshtastic/Views/Nodes/DeviceMetricsLog.swift b/Meshtastic/Views/Nodes/DeviceMetricsLog.swift index 5eec57f4..f8f736e8 100644 --- a/Meshtastic/Views/Nodes/DeviceMetricsLog.swift +++ b/Meshtastic/Views/Nodes/DeviceMetricsLog.swift @@ -106,7 +106,7 @@ struct DeviceMetricsLog: View { Text("\(String(format: "%.2f", dm.voltage))") } TableColumn("channel.utilization") { dm in - Text(String(format: "%.2f", dm.channelUtilization)) + Text("\(String(format: "%.2f", dm.channelUtilization))%") } TableColumn("airtime") { dm in Text("\(String(format: "%.2f", dm.airUtilTx))%") @@ -114,6 +114,7 @@ struct DeviceMetricsLog: View { TableColumn("timestamp") { dm in Text(dm.time?.formattedDate(format: dateFormatString) ?? NSLocalizedString("unknown.age", comment: "")) } + .width(min: 180) } } else { ScrollView { diff --git a/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift b/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift index f3a5af1d..a5f1410d 100644 --- a/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift +++ b/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift @@ -103,6 +103,7 @@ struct EnvironmentMetricsLog: View { TableColumn("timestamp") { em in Text(em.time?.formattedDate(format: dateFormatString) ?? NSLocalizedString("unknown.age", comment: "")) } + .width(min: 180) } } else { ScrollView { diff --git a/Meshtastic/Views/Nodes/NodeDetail.swift b/Meshtastic/Views/Nodes/NodeDetail.swift index 8508dfd7..194291d6 100644 --- a/Meshtastic/Views/Nodes/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/NodeDetail.swift @@ -13,18 +13,17 @@ struct NodeDetail: View { @Environment(\.managedObjectContext) var context @EnvironmentObject var bleManager: BLEManager @Environment(\.colorScheme) var colorScheme: ColorScheme - @AppStorage("meshMapType") private var meshMapType = "standard" + @AppStorage("meshMapType") private var meshMapType = 0 @AppStorage("meshMapShowNodeHistory") private var meshMapShowNodeHistory = false @AppStorage("meshMapShowRouteLines") private var meshMapShowRouteLines = false @State private var mapType: MKMapType = .standard - @State var waypointCoordinate: CLLocationCoordinate2D? + @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 presentingWaypointForm = false @State private var showOverlays: Bool = true @State private var customMapOverlay: MapViewSwiftUI.CustomMapOverlay? = MapViewSwiftUI.CustomMapOverlay( mapName: "offlinemap", @@ -64,15 +63,12 @@ struct NodeDetail: View { // let todaysPositions = positionArray.filter { $0.time! >= Calendar.current.startOfDay(for: Date()) } ZStack { MapViewSwiftUI(onLongPress: { coord in - waypointCoordinate = coord - editingWaypoint = 0 - presentingWaypointForm = true - }, onWaypointEdit: { wpId in - if wpId > 0 { - editingWaypoint = wpId - presentingWaypointForm = true - } - }, positions: lastTenThousand, waypoints: Array(waypoints), + waypointCoordinate = WaypointCoordinate(id: .init(), coordinate: coord, waypointId: 0) + }, onWaypointEdit: { wpId in + if wpId > 0 { + waypointCoordinate = WaypointCoordinate(id: .init(), coordinate: nil, waypointId: Int64(wpId)) + } + }, positions: lastTenThousand, waypoints: Array(waypoints), mapViewType: mapType, userTrackingMode: MKUserTrackingMode.none, showNodeHistory: meshMapShowNodeHistory, @@ -84,10 +80,14 @@ struct NodeDetail: View { HStack(alignment: .bottom, spacing: 1) { Picker("Map Type", selection: $mapType) { - ForEach(MeshMapType.allCases) { map in - Text(map.description).tag(map.MKMapTypeValue()) + ForEach(MeshMapTypes.allCases) { map in + Text(map.description) + .tag(map.MKMapTypeValue()) } } + .onChange(of: (mapType)) { newMapType in + UserDefaults.mapType = Int(newMapType.rawValue) + } .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 12, style: .continuous)) .pickerStyle(.menu) .padding(5) @@ -210,11 +210,11 @@ struct NodeDetail: View { } } .edgesIgnoringSafeArea([.leading, .trailing]) - .sheet(isPresented: $presentingWaypointForm ) {// , onDismiss: didDismissSheet) { - WaypointFormView(coordinate: waypointCoordinate ?? LocationHelper.DefaultLocation, waypointId: editingWaypoint) - .presentationDetents([.medium, .large]) - .presentationDragIndicator(.automatic) - } + .sheet(item: $waypointCoordinate, content: { wpc in + WaypointFormView(coordinate: wpc) + .presentationDetents([.medium, .large]) + .presentationDragIndicator(.automatic) + }) .navigationBarTitle(String(node.user?.longName ?? NSLocalizedString("unknown", comment: "")), displayMode: .inline) .navigationBarItems(trailing: ZStack { @@ -225,22 +225,7 @@ struct NodeDetail: View { }) .onAppear { self.bleManager.context = context - switch meshMapType { - case "standard": - mapType = .standard - case "mutedStandard": - mapType = .mutedStandard - case "hybrid": - mapType = .hybrid - case "hybridFlyover": - mapType = .hybridFlyover - case "satellite": - mapType = .satellite - case "satelliteFlyover": - mapType = .satelliteFlyover - default: - mapType = .hybridFlyover - } + mapType = MeshMapTypes(rawValue: meshMapType)?.MKMapTypeValue() ?? .standard } .task(id: node.num) { if !loadedWeather { @@ -269,6 +254,7 @@ struct NodeDetail: View { } } } + .padding(.bottom, 2) } } } diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index ba6922f1..51a9d092 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -15,7 +15,6 @@ struct NodeList: View { @Environment(\.managedObjectContext) var context @EnvironmentObject var bleManager: BLEManager - @EnvironmentObject var userSettings: UserSettings @FetchRequest( sortDescriptors: [NSSortDescriptor(key: "lastHeard", ascending: false)], @@ -93,7 +92,6 @@ struct NodeList: View { MeshtasticLogo() ) .onAppear { - self.bleManager.userSettings = userSettings self.bleManager.context = context } } detail: { diff --git a/Meshtastic/Views/Nodes/NodeMap.swift b/Meshtastic/Views/Nodes/NodeMap.swift index e4e4f213..03c5dc70 100644 --- a/Meshtastic/Views/Nodes/NodeMap.swift +++ b/Meshtastic/Views/Nodes/NodeMap.swift @@ -14,25 +14,13 @@ struct NodeMap: View { @Environment(\.managedObjectContext) var context @EnvironmentObject var bleManager: BLEManager - @EnvironmentObject var userSettings: UserSettings - @AppStorage("meshMapCustomTileServer") var customTileServer: String = "" { - didSet { - if customTileServer == "" { - self.customMapOverlay = nil - } else { - self.customMapOverlay = MapViewSwiftUI.CustomMapOverlay( - mapName: customTileServer, - tileType: "png", - canReplaceMapContent: true - ) - } - } - } - @AppStorage("meshMapType") private var meshMapType = "hybridFlyover" - @AppStorage("meshMapUserTrackingMode") private var meshMapUserTrackingMode = 0 - @AppStorage("meshMapShowNodeHistory") private var meshMapShowNodeHistory = false - @AppStorage("meshMapShowRouteLines") private var meshMapShowRouteLines = false + @AppStorage("meshMapType") private var meshMapType = 0 + @State var enableMapRecentering: Bool = UserDefaults.enableMapRecentering + @State var enableMapRouteLines: Bool = UserDefaults.enableMapRouteLines + @State var enableMapNodeHistoryPins: Bool = UserDefaults.enableMapNodeHistoryPins + @State var enableOfflineMaps: Bool = UserDefaults.enableOfflineMaps + @State var mapTileServer: String = UserDefaults.mapTileServer @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "time", ascending: true)], predicate: NSPredicate(format: "time >= %@ && nodePosition != nil", Calendar.current.startOfDay(for: Date()) as NSDate), animation: .none) @@ -45,10 +33,10 @@ struct NodeMap: View { private var waypoints: FetchedResults @State private var mapType: MKMapType = .standard - @State private var userTrackingMode: MKUserTrackingMode = .none - @State var waypointCoordinate: CLLocationCoordinate2D = LocationHelper.DefaultLocation - @State var editingWaypoint: Int = 0 - @State private var presentingWaypointForm = false + @State var selectedTracking: UserTrackingModes = .none + @State var isPresentingInfoSheet: Bool = false + + @State var waypointCoordinate: WaypointCoordinate? @State private var customMapOverlay: MapViewSwiftUI.CustomMapOverlay? = MapViewSwiftUI.CustomMapOverlay( mapName: "offlinemap", tileType: "png", @@ -60,46 +48,129 @@ struct NodeMap: View { NavigationStack { ZStack { - MapViewSwiftUI(onLongPress: { coord in - waypointCoordinate = coord - editingWaypoint = 0 - if waypointCoordinate.distance(from: LocationHelper.DefaultLocation) == 0.0 { - print("Apple Park") - } else { - presentingWaypointForm = true - } - }, onWaypointEdit: { wpId in - if wpId > 0 { - editingWaypoint = wpId - presentingWaypointForm = true - } - }, positions: Array(positions), + 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)) + } + }, + positions: Array(positions), waypoints: Array(waypoints), mapViewType: mapType, - userTrackingMode: userTrackingMode, - showNodeHistory: meshMapShowNodeHistory, - showRouteLines: meshMapShowRouteLines, + userTrackingMode: selectedTracking.MKUserTrackingModeValue(), + showNodeHistory: enableMapNodeHistoryPins, + showRouteLines: enableMapRouteLines, customMapOverlay: self.customMapOverlay ) - VStack { - Spacer() - Picker("Map Type", selection: $mapType) { - ForEach(MeshMapType.allCases) { map in - Text(map.description).tag(map.MKMapTypeValue()) - } + VStack(alignment: .trailing) { + + HStack(alignment: .top) { + Spacer() + MapButtons(tracking: $selectedTracking, isPresentingInfoSheet: $isPresentingInfoSheet) + .padding(.trailing, 8) + .padding(.top, 16) } - .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 12, style: .continuous)) - .pickerStyle(.menu) - .padding(.bottom, 5) + + Spacer() + } } .ignoresSafeArea(.all, edges: [.top, .leading, .trailing]) .frame(maxHeight: .infinity) - .sheet(isPresented: $presentingWaypointForm ) {// , onDismiss: didDismissSheet) { - WaypointFormView(coordinate: waypointCoordinate, waypointId: editingWaypoint) - .presentationDetents([.medium, .large]) - .presentationDragIndicator(.automatic) - + .sheet(item: $waypointCoordinate, content: { wpc in + WaypointFormView(coordinate: wpc) + .presentationDetents([.medium, .large]) + .presentationDragIndicator(.automatic) + }) + .sheet(isPresented: $isPresentingInfoSheet) { + VStack { + Form { + Section(header: Text("Map Options")) { + Picker("Map Type", selection: $mapType) { + ForEach(MeshMapTypes.allCases) { map in + Text(map.description).tag(map.MKMapTypeValue()) + } + } + .pickerStyle(DefaultPickerStyle()) + .onChange(of: (mapType)) { newMapType in + UserDefaults.mapType = Int(newMapType.rawValue) + } + + Toggle(isOn: $enableMapRecentering) { + + Label("map.recentering", systemImage: "camera.metering.center.weighted") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + .onTapGesture { + self.enableMapRecentering.toggle() + UserDefaults.enableMapRecentering = self.enableMapRecentering + } + + Toggle(isOn: $enableMapNodeHistoryPins) { + + Label("Show Node History", systemImage: "building.columns.fill") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + .onTapGesture { + self.enableMapNodeHistoryPins.toggle() + UserDefaults.enableMapNodeHistoryPins = self.enableMapNodeHistoryPins + } + + Toggle(isOn: $enableMapRouteLines) { + + Label("Show Route Lines", systemImage: "road.lanes") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + .onTapGesture { + self.enableMapRouteLines.toggle() + UserDefaults.enableMapRouteLines = self.enableMapRouteLines + } + } + Section(header: Text("Offline Maps")) { + Toggle(isOn: $enableOfflineMaps) { + Text("Enable Offline Maps") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + .onTapGesture { + self.enableOfflineMaps.toggle() + UserDefaults.enableOfflineMaps = self.enableOfflineMaps + } + if UserDefaults.enableOfflineMaps { + HStack { + + Label("Tile Server", systemImage: "square.grid.3x2") + TextField( + "Tile Server", + text: $mapTileServer, + axis: .vertical + ) + .foregroundColor(.gray) + .font(.caption2) + .onChange(of: (mapTileServer)) { newMapTileServer in + UserDefaults.mapTileServer = newMapTileServer + } + } + .keyboardType(.asciiCapable) + .disableAutocorrection(true) + } + } + } + #if targetEnvironment(macCatalyst) + Button { + isPresentingInfoSheet = false + } label: { + Label("close", systemImage: "xmark") + } + .buttonStyle(.bordered) + .buttonBorderShape(.capsule) + .controlSize(.large) + .padding(.bottom) + #endif + } + .presentationDetents([.medium, .large]) + .presentationDragIndicator(.visible) } } .navigationBarItems(leading: @@ -114,24 +185,8 @@ struct NodeMap: View { .onAppear(perform: { UIApplication.shared.isIdleTimerDisabled = true self.bleManager.context = context - self.bleManager.userSettings = userSettings - userTrackingMode = UserTrackingModes(rawValue: meshMapUserTrackingMode)?.MKUserTrackingModeValue() ?? MKUserTrackingMode.none - switch meshMapType { - case "standard": - mapType = .standard - case "mutedStandard": - mapType = .mutedStandard - case "hybrid": - mapType = .hybrid - case "hybridFlyover": - mapType = .hybridFlyover - case "satellite": - mapType = .satellite - case "satelliteFlyover": - mapType = .satelliteFlyover - default: - mapType = .hybridFlyover - } + mapType = MeshMapTypes(rawValue: meshMapType)?.MKMapTypeValue() ?? .standard + }) .onDisappear(perform: { UIApplication.shared.isIdleTimerDisabled = false diff --git a/Meshtastic/Views/Nodes/PositionLog.swift b/Meshtastic/Views/Nodes/PositionLog.swift index c275e0ad..fcf4fb2f 100644 --- a/Meshtastic/Views/Nodes/PositionLog.swift +++ b/Meshtastic/Views/Nodes/PositionLog.swift @@ -7,34 +7,42 @@ import SwiftUI struct PositionLog: View { - + @Environment(\.managedObjectContext) var context @EnvironmentObject var bleManager: BLEManager - + @Environment(\.verticalSizeClass) var verticalSizeClass: UserInterfaceSizeClass? + @Environment(\.horizontalSizeClass) var horizontalSizeClass: UserInterfaceSizeClass? + var useGrid: Bool { + let result = (verticalSizeClass == .regular || verticalSizeClass == .compact) && horizontalSizeClass == .compact + return result + } + @State var isExporting = false @State var exportString = "" - + var node: NodeInfoEntity - + @State private var isPresentingClearLogConfirm = false - + @State private var sortOrder = [KeyPathComparator(\PositionEntity.latitude)] + 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 || UIDevice.current.userInterfaceIdiom == .mac { + if UIDevice.current.userInterfaceIdiom == .pad && !useGrid || UIDevice.current.userInterfaceIdiom == .mac { // Add a table for mac and ipad - Table(node.positions?.reversed() as? [PositionEntity] ?? []) { - TableColumn("SeqNo") { position in - Text(String(position.seqNo)) - } + var positions = node.positions?.reversed() as? [PositionEntity] ?? [] + + Table(positions) { TableColumn("Latitude") { position in Text(String(format: "%.5f", position.latitude ?? 0)) } + .width(min: 120) TableColumn("Longitude") { position in Text(String(format: "%.5f", position.longitude ?? 0)) } + .width(min: 120) TableColumn("Altitude") { position in let altitude = Measurement(value: Double(position.altitude), unit: UnitLength.meters) Text(String(altitude.formatted())) @@ -55,10 +63,11 @@ struct PositionLog: View { TableColumn("Time Stamp") { position in Text(position.time?.formattedDate(format: dateFormatString) ?? NSLocalizedString("unknown.age", comment: "")) } + .width(min: 180) } - + } else { - + ScrollView { // Use a grid on iOS as a table only shows a single column let columns = [ @@ -69,9 +78,8 @@ struct PositionLog: View { GridItem(spacing: 0) ] LazyVGrid(columns: columns, alignment: .leading, spacing: 1) { - + GridRow { - Text("Latitude") .font(.caption2) .fontWeight(.bold) @@ -107,9 +115,9 @@ struct PositionLog: View { } .padding(.leading) } - + HStack { - + Button(role: .destructive) { isPresentingClearLogConfirm = true } label: { @@ -127,41 +135,41 @@ struct PositionLog: View { Button("Delete all positions?", role: .destructive) { if clearPositions(destNum: node.num, context: context) { print("Successfully Cleared Position Log") - + } else { print("Clear Position Log Failed") } } } - + Button { - + exportString = positionToCsvFile(positions: node.positions!.array as? [PositionEntity] ?? []) isExporting = true - - } label: { - - Label("save", systemImage: "square.and.arrow.down") - } - .buttonStyle(.bordered) - .buttonBorderShape(.capsule) - .controlSize(.large) - .padding() + + } label: { + + Label("save", systemImage: "square.and.arrow.down") } - .fileExporter( + .buttonStyle(.bordered) + .buttonBorderShape(.capsule) + .controlSize(.large) + .padding() + } + .fileExporter( isPresented: $isExporting, document: CsvDocument(emptyCsv: exportString), contentType: .commaSeparatedText, defaultFilename: String("\(node.user?.longName ?? "Node") Position Log"), onCompletion: { result in - + if case .success = result { - + print("Position log download succeeded.") self.isExporting = false - + } else { - + print("Position log download failed: \(result).") } } @@ -169,13 +177,13 @@ struct PositionLog: View { } .navigationTitle("Position Log \(node.positions?.count ?? 0) Points") .navigationBarItems(trailing: - - ZStack { - + + ZStack { + ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "????") }) .onAppear { - + self.bleManager.context = context } } diff --git a/Meshtastic/Views/Settings/AppSettings.swift b/Meshtastic/Views/Settings/AppSettings.swift index 660b889b..6debbe72 100644 --- a/Meshtastic/Views/Settings/AppSettings.swift +++ b/Meshtastic/Views/Settings/AppSettings.swift @@ -8,10 +8,11 @@ struct AppSettings: View { @Environment(\.managedObjectContext) var context @EnvironmentObject var bleManager: BLEManager - @EnvironmentObject var userSettings: UserSettings - + @StateObject var locationHelper = LocationHelper() + @State var meshtasticUsername: String = UserDefaults.meshtasticUsername + @State var provideLocation: Bool = UserDefaults.provideLocation + @State var provideLocationInterval: Int = UserDefaults.provideLocationInterval @State private var isPresentingCoreDataResetConfirm = false - @State private var preferredDeviceConnected = false var body: some View { VStack { @@ -20,82 +21,66 @@ struct AppSettings: View { HStack { Label("Name", systemImage: "person.crop.rectangle.fill") - TextField("Username", text: $userSettings.meshtasticUsername) + TextField("Username", text: $meshtasticUsername) .foregroundColor(.gray) } .keyboardType(.asciiCapable) .disableAutocorrection(true) .listRowSeparator(.visible) } - Section(header: Text("options")) { - - Picker("keyboard.type", selection: $userSettings.keyboardType) { - ForEach(KeyboardType.allCases) { kb in - Text(kb.description) - } - } - .pickerStyle(DefaultPickerStyle()) - - } Section(header: Text("phone.gps")) { + let accuracy = Measurement(value: locationHelper.locationManager.location?.horizontalAccuracy ?? 300, unit: UnitLength.meters) + let altitiude = Measurement(value: locationHelper.locationManager.location?.altitude ?? 0, unit: UnitLength.meters) + let speed = Measurement(value: locationHelper.locationManager.location?.speed ?? 0, unit: UnitSpeed.kilometersPerHour) + HStack { + Label("Accuracy \(accuracy.formatted())", systemImage: "scope") + .font(.callout) + Label("Sats \(LocationHelper.satsInView)", systemImage: "sparkles") + .font(.callout) + } + Label("Coordinates \(String(format: "%.5f", locationHelper.locationManager.location?.coordinate.latitude ?? 0)), \(String(format: "%.5f", locationHelper.locationManager.location?.coordinate.longitude ?? 0))", systemImage: "mappin") + .font(.callout) + .textSelection(.enabled) + if locationHelper.locationManager.location?.verticalAccuracy ?? 0 > 0 { + Label("Altitude \(altitiude.formatted())", systemImage: "mountain.2") + .font(.callout) + } + if locationHelper.locationManager.location?.courseAccuracy ?? 0 > 0 { + Label("Heading \(String(format: "%.2f", locationHelper.locationManager.location?.course ?? 0))°", systemImage: "location.circle") + .font(.callout) + } + if locationHelper.locationManager.location?.speedAccuracy ?? 0 > 0 { + Label("Speed \(speed.formatted())", systemImage: "speedometer") + .font(.callout) + } - Toggle(isOn: $userSettings.provideLocation) { + Toggle(isOn: $provideLocation) { Label("provide.location", systemImage: "location.circle.fill") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - if userSettings.provideLocation { + .onTapGesture { + self.provideLocation.toggle() + UserDefaults.provideLocation = self.provideLocation + } + + if UserDefaults.provideLocation { - Picker("update.interval", selection: $userSettings.provideLocationInterval) { + Picker("update.interval", selection: $provideLocationInterval) { ForEach(LocationUpdateInterval.allCases) { lu in Text(lu.description) } } .pickerStyle(DefaultPickerStyle()) + .onChange(of: (provideLocationInterval)) { newProvideLocationInterval in + UserDefaults.provideLocationInterval = newProvideLocationInterval + } Text("phone.gps.interval.description") .font(.caption) .foregroundColor(.gray) } - Picker("map.usertrackingmode", selection: $userSettings.meshMapUserTrackingMode) { - ForEach(UserTrackingModes.allCases) { utm in - Text(utm.description) - } - } - .pickerStyle(DefaultPickerStyle()) - Text("When follow or follow with heading are selected maps will automatically center on the location of the GPS on the connected phone.") - .font(.caption) - .foregroundColor(.gray) - } - - Section(header: Text("map options")) { - - Picker("map.type", selection: $userSettings.meshMapType) { - ForEach(MeshMapType.allCases) { map in - Text(map.description) - } - } - .pickerStyle(DefaultPickerStyle()) - - if userSettings.meshMapUserTrackingMode == 0 { - - Toggle(isOn: $userSettings.meshMapRecentering) { - - Label("map.recentering", systemImage: "camera.metering.center.weighted") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - } - Toggle(isOn: $userSettings.meshMapShowNodeHistory) { - - Label("Show Node History", systemImage: "building.columns.fill") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - Toggle(isOn: $userSettings.meshMapShowRouteLines) { - - Label("Show Route Lines", systemImage: "road.lanes") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) } } HStack { @@ -131,7 +116,10 @@ struct AppSettings: View { .onAppear { self.bleManager.context = context } - .onChange(of: userSettings.provideLocation) { _ in + .onChange(of: (meshtasticUsername)) { newMeshtasticUsername in + UserDefaults.meshtasticUsername = newMeshtasticUsername + } + .onChange(of: provideLocation) { _ in if bleManager.connectedPeripheral != nil { self.bleManager.sendWantConfig() diff --git a/Meshtastic/Views/Settings/Config/LoRaConfig.swift b/Meshtastic/Views/Settings/Config/LoRaConfig.swift index 0fcb3150..5ac29438 100644 --- a/Meshtastic/Views/Settings/Config/LoRaConfig.swift +++ b/Meshtastic/Views/Settings/Config/LoRaConfig.swift @@ -32,7 +32,7 @@ struct LoRaConfig: View { @State var hasChanges = false @State var region: Int = 0 @State var modemPreset = 0 - @State var hopLimit = 0 + @State var hopLimit = 3 @State var txPower = 0 @State var txEnabled = true @State var usePreset = true @@ -140,7 +140,7 @@ struct LoRaConfig: View { Picker("Number of hops", selection: $hopLimit) { ForEach(1..<8) { Text("\($0)") - .tag($0 == 3 ? 0 : $0) + .tag($0 == 0 ? 3 : $0) } } .pickerStyle(DefaultPickerStyle()) @@ -284,7 +284,7 @@ struct LoRaConfig: View { } } func setLoRaValues() { - self.hopLimit = Int(node?.loRaConfig?.hopLimit ?? 0) + self.hopLimit = Int(node?.loRaConfig?.hopLimit ?? 3) self.region = Int(node?.loRaConfig?.regionCode ?? 0) self.usePreset = node?.loRaConfig?.usePreset ?? true self.modemPreset = Int(node?.loRaConfig?.modemPreset ?? 0) diff --git a/Meshtastic/Views/Settings/Settings.swift b/Meshtastic/Views/Settings/Settings.swift index cacfdb0b..7e525552 100644 --- a/Meshtastic/Views/Settings/Settings.swift +++ b/Meshtastic/Views/Settings/Settings.swift @@ -11,7 +11,6 @@ struct Settings: View { @Environment(\.managedObjectContext) var context @EnvironmentObject var bleManager: BLEManager - @EnvironmentObject var userSettings: UserSettings @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "user.longName", ascending: true)], animation: .default) private var nodes: FetchedResults @State private var selectedNode: Int = 0 @@ -64,45 +63,42 @@ struct Settings: View { } .tag(SettingsSidebar.appSettings) let node = nodes.first(where: { $0.num == connectedNodeNum }) - //if node?.myInfo?.adminIndex ?? 0 > 0 { - Section("Configure") { - Picker("Configuring Node", selection: $selectedNode) { - if selectedNode == 0 { - Text("Connect to a Node").tag(0) - } - ForEach(nodes) { node in - if node.num == bleManager.connectedPeripheral?.num ?? 0 { - Text("BLE Config: \(node.user?.longName ?? NSLocalizedString("unknown", comment: "Unknown"))") - .tag(Int(node.num)) - } else if node.metadata != nil { - Text("Remote Config: \(node.user?.longName ?? NSLocalizedString("unknown", comment: "Unknown"))") - .tag(Int(node.num)) - } else { - Text("Request Admin: \(node.user?.longName ?? NSLocalizedString("unknown", comment: "Unknown"))") - .tag(Int(node.num)) - } + Section("Configure") { + Picker("Configuring Node", selection: $selectedNode) { + if selectedNode == 0 { + Text("Connect to a Node").tag(0) + } + ForEach(nodes) { node in + if node.num == bleManager.connectedPeripheral?.num ?? 0 { + Text("BLE Config: \(node.user?.longName ?? NSLocalizedString("unknown", comment: "Unknown"))") + .tag(Int(node.num)) + } else if node.metadata != nil { + Text("Remote Config: \(node.user?.longName ?? NSLocalizedString("unknown", comment: "Unknown"))") + .tag(Int(node.num)) + } else { + Text("Request Admin: \(node.user?.longName ?? NSLocalizedString("unknown", comment: "Unknown"))") + .tag(Int(node.num)) } } - .pickerStyle(.automatic) - .labelsHidden() - .onChange(of: selectedNode) { newValue in - if selectedNode > 0 { - let node = nodes.first(where: { $0.num == newValue }) - let connectedNode = nodes.first(where: { $0.num == connectedNodeNum }) - connectedNodeNum = Int(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral?.num ?? 0 : 0) + } + .pickerStyle(.automatic) + .labelsHidden() + .onChange(of: selectedNode) { newValue in + if selectedNode > 0 { + let node = nodes.first(where: { $0.num == newValue }) + let connectedNode = nodes.first(where: { $0.num == connectedNodeNum }) + connectedNodeNum = Int(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral?.num ?? 0 : 0) - if node?.metadata == nil { - let adminMessageId = bleManager.requestDeviceMetadata(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode!.myInfo!.adminIndex, context: context) + if node?.metadata == nil { + let adminMessageId = bleManager.requestDeviceMetadata(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode!.myInfo!.adminIndex, context: context) - if adminMessageId > 0 { - print("Sent node metadata request from node details") - } + if adminMessageId > 0 { + print("Sent node metadata request from node details") } } } } - //} - + } Section("radio.configuration") { NavigationLink { @@ -292,13 +288,11 @@ struct Settings: View { } .onAppear { self.bleManager.context = context - self.bleManager.userSettings = userSettings self.connectedNodeNum = Int(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral?.num ?? 0 : 0) if initialLoad { selectedNode = Int(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral?.num ?? 0 : 0) initialLoad = false } - } .listStyle(GroupedListStyle()) .navigationTitle("settings")