Merge pull request #353 from meshtastic/waypoint_form_bugfixes

Waypoint form bugfixes
This commit is contained in:
Garth Vander Houwen 2023-04-27 14:14:20 -07:00 committed by GitHub
commit 3d290fc833
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
40 changed files with 1139 additions and 786 deletions

View file

@ -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 */,

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View file

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

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

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

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

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

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

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

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

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

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

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

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

@ -6,8 +6,6 @@ import SwiftUI
struct ContentView: View {
@EnvironmentObject var userSettings: UserSettings
@State private var selection: Tab = .ble
enum Tab {

View file

@ -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)!
// }
// }
//}

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

View file

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

View file

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

View file

@ -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."
}

View file

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

View file

@ -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."
}

View file

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

View file

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

View file

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

View file

@ -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: {

View file

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

View file

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

View file

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

View file

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

View file

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