mirror of
https://github.com/meshtastic/Meshtastic-Apple.git
synced 2026-04-20 22:13:56 +00:00
Merge pull request #353 from meshtastic/waypoint_form_bugfixes
Waypoint form bugfixes
This commit is contained in:
commit
3d290fc833
40 changed files with 1139 additions and 786 deletions
|
|
@ -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 = "<group>"; };
|
||||
DD2E65252767A01F00E45FC5 /* NodeDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeDetail.swift; sourceTree = "<group>"; };
|
||||
DD3501882852FC3B000FC853 /* Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = "<group>"; };
|
||||
DD35018A2852FC79000FC853 /* UserSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSettings.swift; sourceTree = "<group>"; };
|
||||
DD3CC6B428E33FD100FA9159 /* ShareChannels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareChannels.swift; sourceTree = "<group>"; };
|
||||
DD3CC6BB28E366DF00FA9159 /* MeshtasticDataModel.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModel.xcdatamodel; sourceTree = "<group>"; };
|
||||
DD3CC6BD28E4CD9800FA9159 /* BatteryGauge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatteryGauge.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -263,7 +273,6 @@
|
|||
DDA6B2E828419CF2003E8C16 /* MeshPackets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshPackets.swift; sourceTree = "<group>"; };
|
||||
DDA6B2EA28420A7B003E8C16 /* NodeAnnotation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeAnnotation.swift; sourceTree = "<group>"; };
|
||||
DDAF8C5226EB1DF10058C060 /* BLEManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BLEManager.swift; sourceTree = "<group>"; };
|
||||
DDAF8C6D26ED19040058C060 /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = "<group>"; };
|
||||
DDB6ABD528AE742000384BA1 /* BluetoothConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothConfig.swift; sourceTree = "<group>"; };
|
||||
DDB6ABD828B0A4BA00384BA1 /* BluetoothModes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothModes.swift; sourceTree = "<group>"; };
|
||||
DDB6ABDA28B0AC6000384BA1 /* DistanceText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DistanceText.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -297,6 +306,19 @@
|
|||
DDD6EEAE29BC024700383354 /* Firmware.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Firmware.swift; sourceTree = "<group>"; };
|
||||
DDD94A4F2845C8F5004A87A0 /* DateTimeText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateTimeText.swift; sourceTree = "<group>"; };
|
||||
DDD9E4E3284B208E003777C5 /* UserEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserEntityExtension.swift; sourceTree = "<group>"; };
|
||||
DDDB443529F6287000EE2349 /* MapButtons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapButtons.swift; sourceTree = "<group>"; };
|
||||
DDDB443C29F6592F00EE2349 /* NetworkManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkManager.swift; sourceTree = "<group>"; };
|
||||
DDDB443F29F79AB000EE2349 /* UserDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaults.swift; sourceTree = "<group>"; };
|
||||
DDDB444129F8A88700EE2349 /* Double.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Double.swift; sourceTree = "<group>"; };
|
||||
DDDB444329F8A8DD00EE2349 /* Float.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Float.swift; sourceTree = "<group>"; };
|
||||
DDDB444529F8A96500EE2349 /* Character.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Character.swift; sourceTree = "<group>"; };
|
||||
DDDB444729F8A9C900EE2349 /* String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = "<group>"; };
|
||||
DDDB444929F8AA3A00EE2349 /* CLLocationCoordinate2D.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLLocationCoordinate2D.swift; sourceTree = "<group>"; };
|
||||
DDDB444B29F8AAA600EE2349 /* Color.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Color.swift; sourceTree = "<group>"; };
|
||||
DDDB444D29F8AB0E00EE2349 /* Int.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Int.swift; sourceTree = "<group>"; };
|
||||
DDDB444F29F8AC9C00EE2349 /* UIImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIImage.swift; sourceTree = "<group>"; };
|
||||
DDDB445129F8ACF900EE2349 /* Date.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Date.swift; sourceTree = "<group>"; };
|
||||
DDDB445329F8AD1600EE2349 /* Data.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data.swift; sourceTree = "<group>"; };
|
||||
DDDD527729B5B83F0045BC3C /* MeshtasticDataModelV9.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV9.xcdatamodel; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
|
|
@ -365,6 +387,7 @@
|
|||
C9697F9C279336B700250207 /* LocalMBTileOverlay.swift */,
|
||||
DD964FC32974767D007C176F /* MapViewFitExtension.swift */,
|
||||
DD2AD8A7296D2DF9001FF0E7 /* MapViewSwiftUI.swift */,
|
||||
DDDB443529F6287000EE2349 /* MapButtons.swift */,
|
||||
);
|
||||
path = Custom;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -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 = "<group>";
|
||||
|
|
@ -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 = "<group>";
|
||||
|
|
@ -678,6 +701,24 @@
|
|||
path = Persistence;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
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 = "<group>";
|
||||
};
|
||||
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 */,
|
||||
|
|
|
|||
21
Meshtastic/Assets.xcassets/alpha.imageset/Contents.json
vendored
Normal file
21
Meshtastic/Assets.xcassets/alpha.imageset/Contents.json
vendored
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
BIN
Meshtastic/Assets.xcassets/alpha.imageset/alpha.png
vendored
Normal file
BIN
Meshtastic/Assets.xcassets/alpha.imageset/alpha.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.9 KiB |
|
|
@ -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 {
|
||||
|
|
|
|||
20
Meshtastic/Extensions/CLLocationCoordinate2D.swift
Normal file
20
Meshtastic/Extensions/CLLocationCoordinate2D.swift
Normal file
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
15
Meshtastic/Extensions/Character.swift
Normal file
15
Meshtastic/Extensions/Character.swift
Normal file
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
55
Meshtastic/Extensions/Color.swift
Normal file
55
Meshtastic/Extensions/Color.swift
Normal file
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
18
Meshtastic/Extensions/Data.swift
Normal file
18
Meshtastic/Extensions/Data.swift
Normal file
|
|
@ -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)}
|
||||
}
|
||||
}
|
||||
20
Meshtastic/Extensions/Date.swift
Normal file
20
Meshtastic/Extensions/Date.swift
Normal file
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
19
Meshtastic/Extensions/Double.swift
Normal file
19
Meshtastic/Extensions/Double.swift
Normal file
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
27
Meshtastic/Extensions/Float.swift
Normal file
27
Meshtastic/Extensions/Float.swift
Normal file
|
|
@ -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<UnitTemperature>(value: Double(self), unit: .celsius)
|
||||
return temperature.formatted(.measurement(width: .abbreviated, usage: .weather))
|
||||
}
|
||||
func localeTemperature() -> Double {
|
||||
let temperature = Measurement<UnitTemperature>(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
|
||||
}
|
||||
}
|
||||
17
Meshtastic/Extensions/Int.swift
Normal file
17
Meshtastic/Extensions/Int.swift
Normal file
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
56
Meshtastic/Extensions/String.swift
Normal file
56
Meshtastic/Extensions/String.swift
Normal file
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
26
Meshtastic/Extensions/UIImage.swift
Normal file
26
Meshtastic/Extensions/UIImage.swift
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
130
Meshtastic/Extensions/UserDefaults.swift
Normal file
130
Meshtastic/Extensions/UserDefaults.swift
Normal file
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<UnitTemperature>(value: Double(self), unit: .celsius)
|
||||
return temperature.formatted(.measurement(width: .abbreviated, usage: .weather))
|
||||
}
|
||||
func localeTemperature() -> Double {
|
||||
let temperature = Measurement<UnitTemperature>(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) }
|
||||
}
|
||||
}
|
||||
|
|
@ -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)")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
29
Meshtastic/Helpers/NetworkManager.swift
Normal file
29
Meshtastic/Helpers/NetworkManager.swift
Normal file
|
|
@ -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))
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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])
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<NSFetchRequestResult> = 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)
|
||||
|
|
|
|||
|
|
@ -6,8 +6,6 @@ import SwiftUI
|
|||
|
||||
struct ContentView: View {
|
||||
|
||||
@EnvironmentObject var userSettings: UserSettings
|
||||
|
||||
@State private var selection: Tab = .ble
|
||||
|
||||
enum Tab {
|
||||
|
|
|
|||
|
|
@ -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)!
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
|
|
|||
67
Meshtastic/Views/Map/Custom/MapButtons.swift
Normal file
67
Meshtastic/Views/Map/Custom/MapButtons.swift
Normal file
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
@ -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)!
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)..<UInt32.max)
|
||||
newWaypoint.latitudeI = Int32(Double(coordinate.coordinate?.latitude ?? 0) * 1e7)
|
||||
newWaypoint.longitudeI = Int32(Double(coordinate.coordinate?.longitude ?? 0) * 1e7)
|
||||
}
|
||||
newWaypoint.name = name.count > 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<NSFetchRequestResult> = NSFetchRequest.init(entityName: "NodeInfoEntity")
|
||||
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(bleManager.connectedPeripheral?.num ?? -1))
|
||||
do {
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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<WaypointEntity>
|
||||
|
||||
@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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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<NodeInfoEntity>
|
||||
@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")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue