mirror of
https://github.com/meshtastic/Meshtastic-Apple.git
synced 2026-04-20 22:13:56 +00:00
commit
d4f4aa4d08
79 changed files with 866 additions and 1304 deletions
|
|
@ -11481,9 +11481,6 @@
|
|||
},
|
||||
"Map Tile Data" : {
|
||||
|
||||
},
|
||||
"Map Type" : {
|
||||
|
||||
},
|
||||
"map.centering" : {
|
||||
"extractionState" : "manual",
|
||||
|
|
@ -18948,9 +18945,6 @@
|
|||
},
|
||||
"Select a Trace Route" : {
|
||||
|
||||
},
|
||||
"Select something to view" : {
|
||||
|
||||
},
|
||||
"select.contact" : {
|
||||
"extractionState" : "manual",
|
||||
|
|
@ -19139,6 +19133,9 @@
|
|||
},
|
||||
"Send a message to a certain meshtastic channel" : {
|
||||
|
||||
},
|
||||
"Send a position on the primary channel when the user button is triple clicked." : {
|
||||
|
||||
},
|
||||
"Send a shutdown to the node you are connected to" : {
|
||||
|
||||
|
|
@ -21895,6 +21892,9 @@
|
|||
},
|
||||
"Treat double tap on supported accelerometers as a user button press." : {
|
||||
|
||||
},
|
||||
"Triple Click Ad Hoc Ping" : {
|
||||
|
||||
},
|
||||
"Try Again" : {
|
||||
|
||||
|
|
|
|||
|
|
@ -1165,7 +1165,7 @@
|
|||
attributes = {
|
||||
BuildIndependentTargetsInParallel = YES;
|
||||
LastSwiftUpdateCheck = 1540;
|
||||
LastUpgradeCheck = 1540;
|
||||
LastUpgradeCheck = 1600;
|
||||
TargetAttributes = {
|
||||
25F5D5C62C4375A8008036E3 = {
|
||||
CreatedOnToolsVersion = 15.4;
|
||||
|
|
@ -1680,7 +1680,6 @@
|
|||
DDC2E17F26CE248F0042C5E4 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
|
||||
|
|
@ -1695,7 +1694,7 @@
|
|||
INFOPLIST_FILE = Meshtastic/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = Meshtastic;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
|
|
@ -1715,7 +1714,6 @@
|
|||
DDC2E18026CE248F0042C5E4 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
|
||||
|
|
@ -1730,7 +1728,7 @@
|
|||
INFOPLIST_FILE = Meshtastic/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = Meshtastic;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1540"
|
||||
LastUpgradeVersion = "1600"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1540"
|
||||
LastUpgradeVersion = "1600"
|
||||
version = "2.0">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ class AppIntentErrors {
|
|||
|
||||
var localizedStringResource: LocalizedStringResource {
|
||||
switch self {
|
||||
case let .message(message):
|
||||
case let .message(message):
|
||||
Logger.services.error("App Intent: \(message)")
|
||||
return "Error: \(message)"
|
||||
case .notConnected:
|
||||
|
|
|
|||
|
|
@ -11,10 +11,10 @@ import AppIntents
|
|||
struct FactoryResetNodeIntent: AppIntent {
|
||||
static var title: LocalizedStringResource = "Factory Reset"
|
||||
static var description: IntentDescription = "Perform a factory reset on the node you are connected to"
|
||||
|
||||
|
||||
func perform() async throws -> some IntentResult {
|
||||
// Request user confirmation before performing the factory reset
|
||||
try await requestConfirmation(result: .result(dialog: "Are you sure you want to factory reset the node?"),confirmationActionName: ConfirmationActionName
|
||||
try await requestConfirmation(result: .result(dialog: "Are you sure you want to factory reset the node?"), confirmationActionName: ConfirmationActionName
|
||||
.custom(acceptLabel: "Factory Reset", acceptAlternatives: [], denyLabel: "Cancel", denyAlternatives: [], destructive: true))
|
||||
|
||||
// Ensure the node is connected
|
||||
|
|
@ -27,7 +27,7 @@ struct FactoryResetNodeIntent: AppIntent {
|
|||
let connectedNode = getNodeInfo(id: connectedPeripheralNum, context: PersistenceController.shared.container.viewContext),
|
||||
let fromUser = connectedNode.user,
|
||||
let toUser = connectedNode.user {
|
||||
|
||||
|
||||
// Attempt to send a factory reset command, throw an error if it fails
|
||||
if !BLEManager.shared.sendFactoryReset(fromUser: fromUser, toUser: toUser) {
|
||||
throw AppIntentErrors.AppIntentError.message("Failed to perform factory reset")
|
||||
|
|
|
|||
|
|
@ -15,37 +15,36 @@ struct MessageChannelIntent: AppIntent {
|
|||
|
||||
@Parameter(title: "Message")
|
||||
var messageContent: String
|
||||
|
||||
@Parameter(title: "Channel",controlStyle: .stepper, inclusiveRange: (lowerBound: 0, upperBound: 7))
|
||||
|
||||
@Parameter(title: "Channel", controlStyle: .stepper, inclusiveRange: (lowerBound: 0, upperBound: 7))
|
||||
var channelNumber: Int
|
||||
|
||||
|
||||
|
||||
static var parameterSummary: some ParameterSummary {
|
||||
Summary("Send \(\.$messageContent) to \(\.$channelNumber)")
|
||||
}
|
||||
func perform() async throws -> some IntentResult {
|
||||
if (!BLEManager.shared.isConnected){
|
||||
if !BLEManager.shared.isConnected {
|
||||
throw AppIntentErrors.AppIntentError.notConnected
|
||||
}
|
||||
|
||||
|
||||
// Check if channel number is between 1 and 7
|
||||
guard (0...7).contains(channelNumber) else {
|
||||
throw $channelNumber.needsValueError("Channel number must be between 0 and 7.")
|
||||
}
|
||||
|
||||
|
||||
// Convert messageContent to data and check its length
|
||||
guard let messageData = messageContent.data(using: .utf8) else {
|
||||
throw AppIntentErrors.AppIntentError.message("Failed to encode message content")
|
||||
}
|
||||
|
||||
|
||||
if messageData.count > 200 {
|
||||
throw $messageContent.needsValueError("Message content exceeds 200 bytes.")
|
||||
}
|
||||
|
||||
if(!BLEManager.shared.sendMessage(message: messageContent, toUserNum: 0, channel: Int32(channelNumber), isEmoji: false, replyID: 0)){
|
||||
if !BLEManager.shared.sendMessage(message: messageContent, toUserNum: 0, channel: Int32(channelNumber), isEmoji: false, replyID: 0) {
|
||||
throw AppIntentErrors.AppIntentError.message("Failed to send message")
|
||||
}
|
||||
|
||||
return .result()
|
||||
|
||||
return .result()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,44 +14,38 @@ struct NodePositionIntent: AppIntent {
|
|||
|
||||
@Parameter(title: "Node Number")
|
||||
var nodeNum: Int
|
||||
|
||||
|
||||
static var title: LocalizedStringResource = "Get Node Position"
|
||||
static var description: IntentDescription = "Fetch the latest position of a cetain node"
|
||||
|
||||
|
||||
|
||||
func perform() async throws -> some IntentResult & ReturnsValue<CLPlacemark> {
|
||||
if (!BLEManager.shared.isConnected) {
|
||||
if !BLEManager.shared.isConnected {
|
||||
throw AppIntentErrors.AppIntentError.notConnected
|
||||
}
|
||||
let fetchNodeInfoRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "NodeInfoEntity")
|
||||
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum))
|
||||
do {
|
||||
guard let fetchedNode = try PersistenceController.shared.container.viewContext.fetch(fetchNodeInfoRequest) as? [NodeInfoEntity], fetchedNode.count == 1 else {
|
||||
throw $nodeNum.needsValueError("Could not find node")
|
||||
}
|
||||
|
||||
let nodeInfo = fetchedNode[0]
|
||||
nodeInfo.latestEnvironmentMetrics?.batteryLevel
|
||||
if let latitude = nodeInfo.latestPosition?.coordinate.latitude,
|
||||
let longitude = nodeInfo.latestPosition?.coordinate.longitude {
|
||||
let nodeLocation = CLLocation(latitude: latitude, longitude: longitude)
|
||||
|
||||
// Reverse geocode the CLLocation to get a CLPlacemark
|
||||
let geocoder = CLGeocoder()
|
||||
let placemarks = try await geocoder.reverseGeocodeLocation(nodeLocation)
|
||||
|
||||
if let placemark = placemarks.first {
|
||||
return .result(value: placemark)
|
||||
} else {
|
||||
throw AppIntentErrors.AppIntentError.message("Error Reverse Geocoding Location")
|
||||
}
|
||||
} else {
|
||||
throw AppIntentErrors.AppIntentError.message("Node does not have positions")
|
||||
}
|
||||
} catch {
|
||||
throw AppIntentErrors.AppIntentError.message("Fetch Failure")
|
||||
let fetchNodeInfoRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "NodeInfoEntity")
|
||||
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum))
|
||||
do {
|
||||
guard let fetchedNode = try PersistenceController.shared.container.viewContext.fetch(fetchNodeInfoRequest) as? [NodeInfoEntity], fetchedNode.count == 1 else {
|
||||
throw $nodeNum.needsValueError("Could not find node")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
let nodeInfo = fetchedNode[0]
|
||||
if let latitude = nodeInfo.latestPosition?.coordinate.latitude,
|
||||
let longitude = nodeInfo.latestPosition?.coordinate.longitude {
|
||||
let nodeLocation = CLLocation(latitude: latitude, longitude: longitude)
|
||||
// Reverse geocode the CLLocation to get a CLPlacemark
|
||||
let geocoder = CLGeocoder()
|
||||
let placemarks = try await geocoder.reverseGeocodeLocation(nodeLocation)
|
||||
|
||||
if let placemark = placemarks.first {
|
||||
return .result(value: placemark)
|
||||
} else {
|
||||
throw AppIntentErrors.AppIntentError.message("Error Reverse Geocoding Location")
|
||||
}
|
||||
} else {
|
||||
throw AppIntentErrors.AppIntentError.message("Node does not have positions")
|
||||
}
|
||||
} catch {
|
||||
throw AppIntentErrors.AppIntentError.message("Fetch Failure")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,10 +16,10 @@ struct SendWaypointIntent: AppIntent {
|
|||
|
||||
@Parameter(title: "Name", default: "Dropped Pin")
|
||||
var nameParameter: String?
|
||||
|
||||
|
||||
@Parameter(title: "Description", default: "")
|
||||
var descriptionParameter: String?
|
||||
|
||||
|
||||
@Parameter(title: "Emoji", default: "📍")
|
||||
var emojiParameter: String?
|
||||
|
||||
|
|
@ -27,19 +27,19 @@ struct SendWaypointIntent: AppIntent {
|
|||
var locationParameter: CLPlacemark
|
||||
|
||||
func perform() async throws -> some IntentResult {
|
||||
if (!BLEManager.shared.isConnected){
|
||||
if !BLEManager.shared.isConnected {
|
||||
throw AppIntentErrors.AppIntentError.notConnected
|
||||
}
|
||||
// Provide default values if parameters are nil
|
||||
let name = nameParameter ?? "Dropped Pin"
|
||||
let description = descriptionParameter ?? ""
|
||||
let emoji = emojiParameter ?? "📍"
|
||||
|
||||
|
||||
// Validate name length
|
||||
if name.utf8.count > 30 {
|
||||
throw $nameParameter.needsValueError("Name must be less than 30 bytes")
|
||||
}
|
||||
|
||||
|
||||
// Validate description length
|
||||
if description.utf8.count > 100 {
|
||||
throw $descriptionParameter.needsValueError("Description must be less than 100 bytes")
|
||||
|
|
@ -60,7 +60,6 @@ struct SendWaypointIntent: AppIntent {
|
|||
newWaypoint.longitudeI = Int32(longitude * 10_000_000)
|
||||
}
|
||||
|
||||
|
||||
newWaypoint.id = UInt32.random(in: UInt32(UInt8.max)..<UInt32.max)
|
||||
// Unicode scalar value for the icon emoji string
|
||||
let unicodeScalers = emoji.unicodeScalars
|
||||
|
|
@ -69,12 +68,9 @@ struct SendWaypointIntent: AppIntent {
|
|||
newWaypoint.icon = unicode
|
||||
newWaypoint.name = name
|
||||
newWaypoint.description_p = description
|
||||
if(!BLEManager.shared.sendWaypoint(waypoint: newWaypoint)){
|
||||
if !BLEManager.shared.sendWaypoint(waypoint: newWaypoint) {
|
||||
throw AppIntentErrors.AppIntentError.message("Failed to Send Waypoint")
|
||||
}
|
||||
|
||||
|
||||
|
||||
return .result()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -177,7 +177,6 @@ enum Iaq: Int, CaseIterable, Identifiable {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
// Default of 0 is Client
|
||||
enum MetricsTypes: Int, CaseIterable, Identifiable {
|
||||
|
||||
|
|
|
|||
|
|
@ -54,31 +54,28 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
let LOGRADIO_UUID = CBUUID(string: "0x5a3d6e49-06e6-4423-9944-e9de8cdf9547")
|
||||
|
||||
// MARK: init
|
||||
|
||||
private override init() {
|
||||
// Default initialization should not be used
|
||||
fatalError("Use setup(appState:context:) to initialize the singleton")
|
||||
}
|
||||
// Default initialization should not be used
|
||||
fatalError("Use setup(appState:context:) to initialize the singleton")
|
||||
}
|
||||
|
||||
static func setup(appState: AppState, context: NSManagedObjectContext) {
|
||||
guard shared == nil else {
|
||||
print("BLEManager already initialized")
|
||||
return
|
||||
}
|
||||
shared = BLEManager(appState: appState, context: context)
|
||||
static func setup(appState: AppState, context: NSManagedObjectContext) {
|
||||
guard shared == nil else {
|
||||
Logger.services.warning("[BLE] BLEManager already initialized")
|
||||
return
|
||||
}
|
||||
shared = BLEManager(appState: appState, context: context)
|
||||
}
|
||||
|
||||
private init(appState: AppState, context: NSManagedObjectContext) {
|
||||
self.appState = appState
|
||||
self.context = context
|
||||
self.lastConnectionError = ""
|
||||
self.connectedVersion = "0.0.0"
|
||||
super.init()
|
||||
centralManager = CBCentralManager(delegate: self, queue: nil)
|
||||
mqttManager.delegate = self
|
||||
}
|
||||
|
||||
|
||||
private init(appState: AppState, context: NSManagedObjectContext) {
|
||||
self.appState = appState
|
||||
self.context = context
|
||||
self.lastConnectionError = ""
|
||||
self.connectedVersion = "0.0.0"
|
||||
super.init()
|
||||
centralManager = CBCentralManager(delegate: self, queue: nil)
|
||||
mqttManager.delegate = self
|
||||
}
|
||||
|
||||
// MARK: Scanning for BLE Devices
|
||||
// Scan for nearby BLE devices using the Meshtastic BLE service ID
|
||||
|
|
@ -464,7 +461,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
do {
|
||||
let fetchedNodes = try context.fetch(nodes)
|
||||
let receivingNode = fetchedNodes.first(where: { $0.num == destNum })
|
||||
let connectedNode = fetchedNodes.first(where: { $0.num == self.connectedPeripheral.num })
|
||||
traceRoute.id = Int64(meshPacket.id)
|
||||
traceRoute.time = Date()
|
||||
traceRoute.node = receivingNode
|
||||
|
|
@ -1202,65 +1198,37 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
@MainActor
|
||||
public func getPositionFromPhoneGPS(destNum: Int64, fixedPosition: Bool) -> Position? {
|
||||
var positionPacket = Position()
|
||||
if #available(iOS 17.0, macOS 14.0, *) {
|
||||
|
||||
guard let lastLocation = LocationsHandler.shared.locationsArray.last else {
|
||||
return nil
|
||||
}
|
||||
guard let lastLocation = LocationsHandler.shared.locationsArray.last else {
|
||||
return nil
|
||||
}
|
||||
|
||||
if lastLocation == CLLocation(latitude: 0, longitude: 0) {
|
||||
return nil
|
||||
}
|
||||
|
||||
positionPacket.latitudeI = Int32(lastLocation.coordinate.latitude * 1e7)
|
||||
positionPacket.longitudeI = Int32(lastLocation.coordinate.longitude * 1e7)
|
||||
let timestamp = lastLocation.timestamp
|
||||
positionPacket.time = UInt32(timestamp.timeIntervalSince1970)
|
||||
positionPacket.timestamp = UInt32(timestamp.timeIntervalSince1970)
|
||||
positionPacket.altitude = Int32(lastLocation.altitude)
|
||||
positionPacket.satsInView = UInt32(LocationsHandler.satsInView)
|
||||
let currentSpeed = lastLocation.speed
|
||||
if currentSpeed > 0 && (!currentSpeed.isNaN || !currentSpeed.isInfinite) {
|
||||
positionPacket.groundSpeed = UInt32(currentSpeed)
|
||||
}
|
||||
let currentHeading = lastLocation.course
|
||||
if (currentHeading > 0 && currentHeading <= 360) && (!currentHeading.isNaN || !currentHeading.isInfinite) {
|
||||
positionPacket.groundTrack = UInt32(currentHeading)
|
||||
}
|
||||
/// Set location source for time
|
||||
if !fixedPosition {
|
||||
/// From GPS treat time as good
|
||||
positionPacket.locationSource = Position.LocSource.locExternal
|
||||
} else {
|
||||
/// From GPS, but time can be old and have drifted
|
||||
positionPacket.locationSource = Position.LocSource.locManual
|
||||
}
|
||||
if lastLocation == CLLocation(latitude: 0, longitude: 0) {
|
||||
return nil
|
||||
}
|
||||
|
||||
positionPacket.latitudeI = Int32(lastLocation.coordinate.latitude * 1e7)
|
||||
positionPacket.longitudeI = Int32(lastLocation.coordinate.longitude * 1e7)
|
||||
let timestamp = lastLocation.timestamp
|
||||
positionPacket.time = UInt32(timestamp.timeIntervalSince1970)
|
||||
positionPacket.timestamp = UInt32(timestamp.timeIntervalSince1970)
|
||||
positionPacket.altitude = Int32(lastLocation.altitude)
|
||||
positionPacket.satsInView = UInt32(LocationsHandler.satsInView)
|
||||
let currentSpeed = lastLocation.speed
|
||||
if currentSpeed > 0 && (!currentSpeed.isNaN || !currentSpeed.isInfinite) {
|
||||
positionPacket.groundSpeed = UInt32(currentSpeed)
|
||||
}
|
||||
let currentHeading = lastLocation.course
|
||||
if (currentHeading > 0 && currentHeading <= 360) && (!currentHeading.isNaN || !currentHeading.isInfinite) {
|
||||
positionPacket.groundTrack = UInt32(currentHeading)
|
||||
}
|
||||
/// Set location source for time
|
||||
if !fixedPosition {
|
||||
/// From GPS treat time as good
|
||||
positionPacket.locationSource = Position.LocSource.locExternal
|
||||
} else {
|
||||
|
||||
positionPacket.latitudeI = Int32(LocationHelper.currentLocation.latitude * 1e7)
|
||||
positionPacket.longitudeI = Int32(LocationHelper.currentLocation.longitude * 1e7)
|
||||
let timestamp = LocationHelper.shared.locationManager.location?.timestamp ?? Date()
|
||||
positionPacket.time = UInt32(timestamp.timeIntervalSince1970)
|
||||
positionPacket.timestamp = UInt32(timestamp.timeIntervalSince1970)
|
||||
positionPacket.altitude = Int32(LocationHelper.shared.locationManager.location?.altitude ?? 0)
|
||||
positionPacket.satsInView = UInt32(LocationHelper.satsInView)
|
||||
let currentSpeed = LocationHelper.shared.locationManager.location?.speed ?? 0
|
||||
if currentSpeed > 0 && (!currentSpeed.isNaN || !currentSpeed.isInfinite) {
|
||||
positionPacket.groundSpeed = UInt32(currentSpeed)
|
||||
}
|
||||
let currentHeading = LocationHelper.shared.locationManager.location?.course ?? 0
|
||||
if (currentHeading > 0 && currentHeading <= 360) && (!currentHeading.isNaN || !currentHeading.isInfinite) {
|
||||
positionPacket.groundTrack = UInt32(currentHeading)
|
||||
}
|
||||
/// Set location source for time
|
||||
if !fixedPosition {
|
||||
/// From GPS treat time as good
|
||||
positionPacket.locationSource = Position.LocSource.locExternal
|
||||
} else {
|
||||
/// From GPS, but time can be old and have drifted
|
||||
positionPacket.locationSource = Position.LocSource.locManual
|
||||
}
|
||||
/// From GPS, but time can be old and have drifted
|
||||
positionPacket.locationSource = Position.LocSource.locManual
|
||||
}
|
||||
return positionPacket
|
||||
}
|
||||
|
|
@ -1645,7 +1613,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
let decodedString = base64UrlString.base64urlToBase64()
|
||||
if let decodedData = Data(base64Encoded: decodedString) {
|
||||
do {
|
||||
let channelSet: ChannelSet = try ChannelSet(serializedData: decodedData)
|
||||
let channelSet: ChannelSet = try ChannelSet(serializedBytes: decodedData)
|
||||
for cs in channelSet.settings {
|
||||
if addChannels {
|
||||
// We are trying to add a channel so lets get the last index
|
||||
|
|
@ -2017,10 +1985,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
let messageDescription = "🛟 Saved LoRa Config for \(toUser.longName ?? "unknown".localized)"
|
||||
|
||||
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
|
||||
upsertLoRaConfigPacket(config: config, nodeNum: toUser.num, sessionPasskey: toUser.userNode?.sessionPasskey,context: context)
|
||||
upsertLoRaConfigPacket(config: config, nodeNum: toUser.num, sessionPasskey: toUser.userNode?.sessionPasskey, context: context)
|
||||
return Int64(meshPacket.id)
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
|
|
@ -3223,7 +3190,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
}
|
||||
|
||||
func storeAndForwardPacket(packet: MeshPacket, connectedNodeNum: Int64, context: NSManagedObjectContext) {
|
||||
if let storeAndForwardMessage = try? StoreAndForward(serializedData: packet.decoded.payload) {
|
||||
if let storeAndForwardMessage = try? StoreAndForward(serializedBytes: packet.decoded.payload) {
|
||||
// Handle each of the store and forward request / response messages
|
||||
switch storeAndForwardMessage.rr {
|
||||
case .unset:
|
||||
|
|
@ -3349,7 +3316,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
|
||||
// MARK: - CB Central Manager implmentation
|
||||
extension BLEManager: CBCentralManagerDelegate {
|
||||
|
||||
|
||||
// MARK: Bluetooth enabled/disabled
|
||||
func centralManagerDidUpdateState(_ central: CBCentralManager) {
|
||||
if central.state == CBManagerState.poweredOn {
|
||||
|
|
@ -3359,9 +3326,8 @@ extension BLEManager: CBCentralManagerDelegate {
|
|||
} else {
|
||||
isSwitchedOn = false
|
||||
}
|
||||
|
||||
|
||||
var status = ""
|
||||
|
||||
switch central.state {
|
||||
case .poweredOff:
|
||||
status = "BLE is powered off"
|
||||
|
|
@ -3380,10 +3346,9 @@ extension BLEManager: CBCentralManagerDelegate {
|
|||
}
|
||||
Logger.services.info("📜 [BLE] Bluetooth status: \(status)")
|
||||
}
|
||||
|
||||
|
||||
// Called each time a peripheral is discovered
|
||||
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber) {
|
||||
|
||||
if self.automaticallyReconnect && peripheral.identifier.uuidString == UserDefaults.standard.object(forKey: "preferredPeripheralId") as? String ?? "" {
|
||||
self.connectTo(peripheral: peripheral)
|
||||
Logger.services.info("✅ [BLE] Reconnecting to prefered peripheral: \(peripheral.name ?? "Unknown", privacy: .public)")
|
||||
|
|
@ -3391,7 +3356,6 @@ extension BLEManager: CBCentralManagerDelegate {
|
|||
let name = advertisementData[CBAdvertisementDataLocalNameKey] as? String
|
||||
let device = Peripheral(id: peripheral.identifier.uuidString, num: 0, name: name ?? "Unknown", shortName: "?", longName: name ?? "Unknown", firmwareVersion: "Unknown", rssi: RSSI.intValue, lastUpdate: Date(), peripheral: peripheral)
|
||||
let index = peripherals.map { $0.peripheral }.firstIndex(of: peripheral)
|
||||
|
||||
if let peripheralIndex = index {
|
||||
peripherals[peripheralIndex] = device
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -5,16 +5,9 @@ import OSLog
|
|||
class LocalNotificationManager {
|
||||
|
||||
var notifications = [Notification]()
|
||||
|
||||
let thumbsUpAction = UNNotificationAction(identifier: "messageNotification.thumbsUpAction", title:
|
||||
"👍 \(Tapbacks.thumbsUp.description)", options: [])
|
||||
let thumbsDownAction = UNNotificationAction(identifier: "messageNotification.thumbsDownAction", title:
|
||||
"👎 \(Tapbacks.thumbsDown.description)", options: [])
|
||||
let replyInputAction = UNTextInputNotificationAction(
|
||||
identifier: "messageNotification.replyInputAction",
|
||||
title: "reply".localized,
|
||||
options: [])
|
||||
|
||||
let thumbsUpAction = UNNotificationAction(identifier: "messageNotification.thumbsUpAction", title: "👍 \(Tapbacks.thumbsUp.description)", options: [])
|
||||
let thumbsDownAction = UNNotificationAction(identifier: "messageNotification.thumbsDownAction", title: "👎 \(Tapbacks.thumbsDown.description)", options: [])
|
||||
let replyInputAction = UNTextInputNotificationAction(identifier: "messageNotification.replyInputAction", title: "reply".localized, options: [])
|
||||
|
||||
// Step 1 Request Permissions for notifications
|
||||
private func requestAuthorization() {
|
||||
|
|
@ -43,13 +36,13 @@ class LocalNotificationManager {
|
|||
private func scheduleNotifications() {
|
||||
let messageNotificationCategory = UNNotificationCategory(
|
||||
identifier: "messageNotificationCategory",
|
||||
actions: [thumbsUpAction, thumbsDownAction,replyInputAction],
|
||||
actions: [thumbsUpAction, thumbsDownAction, replyInputAction],
|
||||
intentIdentifiers: [],
|
||||
options: .customDismissAction
|
||||
)
|
||||
|
||||
UNUserNotificationCenter.current().setNotificationCategories([messageNotificationCategory])
|
||||
|
||||
|
||||
for notification in notifications {
|
||||
let content = UNMutableNotificationContent()
|
||||
content.subtitle = notification.subtitle
|
||||
|
|
@ -75,7 +68,6 @@ class LocalNotificationManager {
|
|||
content.userInfo["userNum"] = notification.userNum
|
||||
}
|
||||
|
||||
|
||||
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
|
||||
let request = UNNotificationRequest(identifier: notification.id, content: content, trigger: trigger)
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import CoreLocation
|
|||
import OSLog
|
||||
|
||||
// Shared state that manages the `CLLocationManager` and `CLBackgroundActivitySession`.
|
||||
@available(iOS 17.0, macOS 14.0, *)
|
||||
@MainActor class LocationsHandler: ObservableObject {
|
||||
|
||||
static let shared = LocationsHandler() // Create a single, shared instance of the object.
|
||||
|
|
|
|||
|
|
@ -50,7 +50,6 @@ func generateMessageMarkdown (message: String) -> String {
|
|||
|
||||
func localConfig (config: Config, context: NSManagedObjectContext, nodeNum: Int64, nodeLongName: String) {
|
||||
|
||||
let remote = nodeNum != UserDefaults.preferredPeripheralNum
|
||||
if config.payloadVariant == Config.OneOf_PayloadVariant.bluetooth(config.bluetooth) {
|
||||
upsertBluetoothConfigPacket(config: config.bluetooth, nodeNum: nodeNum, context: context)
|
||||
} else if config.payloadVariant == Config.OneOf_PayloadVariant.device(config.device) {
|
||||
|
|
@ -454,11 +453,11 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje
|
|||
|
||||
func adminAppPacket (packet: MeshPacket, context: NSManagedObjectContext) {
|
||||
|
||||
if let adminMessage = try? AdminMessage(serializedData: packet.decoded.payload) {
|
||||
if let adminMessage = try? AdminMessage(serializedBytes: packet.decoded.payload) {
|
||||
|
||||
if adminMessage.payloadVariant == AdminMessage.OneOf_PayloadVariant.getCannedMessageModuleMessagesResponse(adminMessage.getCannedMessageModuleMessagesResponse) {
|
||||
|
||||
if let cmmc = try? CannedMessageModuleConfig(serializedData: packet.decoded.payload) {
|
||||
if let cmmc = try? CannedMessageModuleConfig(serializedBytes: packet.decoded.payload) {
|
||||
|
||||
if !cmmc.messages.isEmpty {
|
||||
|
||||
|
|
@ -581,7 +580,7 @@ func paxCounterPacket (packet: MeshPacket, context: NSManagedObjectContext) {
|
|||
do {
|
||||
let fetchedNode = try context.fetch(fetchNodeInfoRequest)
|
||||
|
||||
if let paxMessage = try? Paxcount(serializedData: packet.decoded.payload) {
|
||||
if let paxMessage = try? Paxcount(serializedBytes: packet.decoded.payload) {
|
||||
|
||||
let newPax = PaxCounterEntity(context: context)
|
||||
newPax.ble = Int32(truncatingIfNeeded: paxMessage.ble)
|
||||
|
|
@ -611,7 +610,7 @@ func paxCounterPacket (packet: MeshPacket, context: NSManagedObjectContext) {
|
|||
|
||||
func routingPacket (packet: MeshPacket, connectedNodeNum: Int64, context: NSManagedObjectContext) {
|
||||
|
||||
if let routingMessage = try? Routing(serializedData: packet.decoded.payload) {
|
||||
if let routingMessage = try? Routing(serializedBytes: packet.decoded.payload) {
|
||||
|
||||
let routingError = RoutingError(rawValue: routingMessage.errorReason.rawValue)
|
||||
|
||||
|
|
@ -833,7 +832,7 @@ func textMessageAppPacket(
|
|||
}
|
||||
var storeForwardBroadcast = false
|
||||
if storeForward {
|
||||
if let storeAndForwardMessage = try? StoreAndForward(serializedData: packet.decoded.payload) {
|
||||
if let storeAndForwardMessage = try? StoreAndForward(serializedBytes: packet.decoded.payload) {
|
||||
messageText = String(bytes: storeAndForwardMessage.text, encoding: .utf8)
|
||||
if storeAndForwardMessage.rr == .routerTextBroadcast {
|
||||
storeForwardBroadcast = true
|
||||
|
|
@ -993,7 +992,6 @@ func textMessageAppPacket(
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
func waypointPacket (packet: MeshPacket, context: NSManagedObjectContext) {
|
||||
|
||||
let logString = String.localizedStringWithFormat("mesh.log.waypoint.received %@".localized, String(packet.from))
|
||||
|
|
@ -1004,7 +1002,7 @@ func waypointPacket (packet: MeshPacket, context: NSManagedObjectContext) {
|
|||
|
||||
do {
|
||||
|
||||
if let waypointMessage = try? Waypoint(serializedData: packet.decoded.payload) {
|
||||
if let waypointMessage = try? Waypoint(serializedBytes: packet.decoded.payload) {
|
||||
let fetchedWaypoint = try context.fetch(fetchWaypointRequest)
|
||||
if fetchedWaypoint.isEmpty {
|
||||
let waypoint = WaypointEntity(context: context)
|
||||
|
|
|
|||
|
|
@ -69,6 +69,7 @@
|
|||
<attribute name="rebroadcastMode" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="role" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="serialEnabled" optional="YES" attributeType="Boolean" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="tripleClickAsAdHocPing" optional="YES" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<attribute name="tzdef" optional="YES" attributeType="String"/>
|
||||
<relationship name="deviceConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="deviceConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
|
|
|
|||
|
|
@ -3,11 +3,8 @@
|
|||
import SwiftUI
|
||||
import CoreData
|
||||
import OSLog
|
||||
#if canImport(TipKit)
|
||||
import TipKit
|
||||
#endif
|
||||
|
||||
@available(iOS 17.0, *)
|
||||
@main
|
||||
struct MeshtasticAppleApp: App {
|
||||
|
||||
|
|
@ -111,26 +108,24 @@ struct MeshtasticAppleApp: App {
|
|||
}
|
||||
})
|
||||
.task {
|
||||
if #available(iOS 17.0, macOS 14.0, *) {
|
||||
#if DEBUG
|
||||
/// Optionally, call `Tips.resetDatastore()` before `Tips.configure()` to reset the state of all tips. This will allow tips to re-appear even after they have been dismissed by the user.
|
||||
/// This is for testing only, and should not be enabled in release builds.
|
||||
try? Tips.resetDatastore()
|
||||
#endif
|
||||
#if DEBUG
|
||||
/// Optionally, call `Tips.resetDatastore()` before `Tips.configure()` to reset the state of all tips. This will allow tips to re-appear even after they have been dismissed by the user.
|
||||
/// This is for testing only, and should not be enabled in release builds.
|
||||
try? Tips.resetDatastore()
|
||||
#endif
|
||||
|
||||
try? Tips.configure(
|
||||
[
|
||||
// Reset which tips have been shown and what parameters have been tracked, useful during testing and for this sample project
|
||||
.datastoreLocation(.applicationDefault),
|
||||
// When should the tips be presented? If you use .immediate, they'll all be presented whenever a screen with a tip appears.
|
||||
// You can adjust this on per tip level as well
|
||||
.displayFrequency(.immediate)
|
||||
]
|
||||
)
|
||||
}
|
||||
try? Tips.configure(
|
||||
[
|
||||
// Reset which tips have been shown and what parameters have been tracked, useful during testing and for this sample project
|
||||
.datastoreLocation(.applicationDefault),
|
||||
// When should the tips be presented? If you use .immediate, they'll all be presented whenever a screen with a tip appears.
|
||||
// You can adjust this on per tip level as well
|
||||
.displayFrequency(.immediate)
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
||||
.onChange(of: scenePhase) { (newScenePhase) in
|
||||
.onChange(of: scenePhase) { (_, newScenePhase) in
|
||||
switch newScenePhase {
|
||||
case .background:
|
||||
Logger.services.info("🎬 [App] Scene is in the background")
|
||||
|
|
|
|||
|
|
@ -9,9 +9,9 @@ import SwiftUI
|
|||
import OSLog
|
||||
|
||||
class MeshtasticAppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate, ObservableObject {
|
||||
|
||||
|
||||
var router: Router?
|
||||
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
|
||||
Logger.services.info("🚀 [App] Meshtstic Apple App launched!")
|
||||
// Default User Default Values
|
||||
|
|
@ -19,14 +19,11 @@ class MeshtasticAppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificat
|
|||
UserDefaults.standard.register(defaults: ["meshMapShowNodeHistory": true])
|
||||
UserDefaults.standard.register(defaults: ["meshMapShowRouteLines": true])
|
||||
UNUserNotificationCenter.current().delegate = self
|
||||
if #available(iOS 17.0, macOS 14.0, *) {
|
||||
let locationsHandler = LocationsHandler.shared
|
||||
locationsHandler.startLocationUpdates()
|
||||
|
||||
// If a background activity session was previously active, reinstantiate it after the background launch.
|
||||
if locationsHandler.backgroundActivity {
|
||||
locationsHandler.backgroundActivity = true
|
||||
}
|
||||
let locationsHandler = LocationsHandler.shared
|
||||
locationsHandler.startLocationUpdates()
|
||||
// If a background activity session was previously active, reinstantiate it after the background launch.
|
||||
if locationsHandler.backgroundActivity {
|
||||
locationsHandler.backgroundActivity = true
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
|
@ -38,7 +35,7 @@ class MeshtasticAppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificat
|
|||
) {
|
||||
completionHandler([.list, .banner, .sound])
|
||||
}
|
||||
|
||||
|
||||
// This method is called when a user clicks on the notification
|
||||
func userNotificationCenter(
|
||||
_ center: UNUserNotificationCenter,
|
||||
|
|
@ -46,11 +43,10 @@ class MeshtasticAppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificat
|
|||
withCompletionHandler completionHandler: @escaping () -> Void
|
||||
) {
|
||||
let userInfo = response.notification.request.content.userInfo
|
||||
|
||||
|
||||
switch response.actionIdentifier {
|
||||
case UNNotificationDefaultActionIdentifier:
|
||||
break
|
||||
|
||||
case "messageNotification.thumbsUpAction":
|
||||
if let channel = userInfo["channel"] as? Int32,
|
||||
let replyID = userInfo["messageId"] as? Int64 {
|
||||
|
|
@ -65,8 +61,6 @@ class MeshtasticAppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificat
|
|||
} else {
|
||||
Logger.services.error("Failed to retrieve channel or messageId from userInfo")
|
||||
}
|
||||
break
|
||||
|
||||
case "messageNotification.thumbsDownAction":
|
||||
if let channel = userInfo["channel"] as? Int32,
|
||||
let replyID = userInfo["messageId"] as? Int64 {
|
||||
|
|
@ -81,8 +75,6 @@ class MeshtasticAppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificat
|
|||
} else {
|
||||
Logger.services.error("Failed to retrieve channel or messageId from userInfo")
|
||||
}
|
||||
break
|
||||
|
||||
case "messageNotification.replyInputAction":
|
||||
if let userInput = (response as? UNTextInputNotificationResponse)?.userText,
|
||||
let channel = userInfo["channel"] as? Int32,
|
||||
|
|
@ -98,12 +90,10 @@ class MeshtasticAppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificat
|
|||
} else {
|
||||
Logger.services.error("Failed to retrieve user input, channel, or messageId from userInfo")
|
||||
}
|
||||
break
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
|
||||
if let targetValue = userInfo["target"] as? String,
|
||||
let deepLink = userInfo["path"] as? String,
|
||||
let url = URL(string: deepLink) {
|
||||
|
|
@ -112,7 +102,6 @@ class MeshtasticAppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificat
|
|||
} else {
|
||||
Logger.services.error("Failed to handle notification response: \(userInfo)")
|
||||
}
|
||||
|
||||
completionHandler()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -159,12 +159,12 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext)
|
|||
if packet.to == Constants.maximumNodeNum || packet.to == UserDefaults.preferredPeripheralNum {
|
||||
newNode.channel = Int32(packet.channel)
|
||||
}
|
||||
if let nodeInfoMessage = try? NodeInfo(serializedData: packet.decoded.payload) {
|
||||
if let nodeInfoMessage = try? NodeInfo(serializedBytes: packet.decoded.payload) {
|
||||
newNode.hopsAway = Int32(nodeInfoMessage.hopsAway)
|
||||
newNode.favorite = nodeInfoMessage.isFavorite
|
||||
}
|
||||
|
||||
if let newUserMessage = try? User(serializedData: packet.decoded.payload) {
|
||||
if let newUserMessage = try? User(serializedBytes: packet.decoded.payload) {
|
||||
|
||||
if newUserMessage.id.isEmpty {
|
||||
if packet.from > Constants.minimumNodeNum {
|
||||
|
|
@ -254,7 +254,7 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext)
|
|||
fetchedNode[0].channel = Int32(packet.channel)
|
||||
}
|
||||
|
||||
if let nodeInfoMessage = try? NodeInfo(serializedData: packet.decoded.payload) {
|
||||
if let nodeInfoMessage = try? NodeInfo(serializedBytes: packet.decoded.payload) {
|
||||
|
||||
fetchedNode[0].hopsAway = Int32(nodeInfoMessage.hopsAway)
|
||||
fetchedNode[0].favorite = nodeInfoMessage.isFavorite
|
||||
|
|
@ -320,7 +320,7 @@ func upsertPositionPacket (packet: MeshPacket, context: NSManagedObjectContext)
|
|||
|
||||
do {
|
||||
|
||||
if let positionMessage = try? Position(serializedData: packet.decoded.payload) {
|
||||
if let positionMessage = try? Position(serializedBytes: packet.decoded.payload) {
|
||||
|
||||
/// Don't save empty position packets from null island or apple park
|
||||
if (positionMessage.longitudeI != 0 && positionMessage.latitudeI != 0) && (positionMessage.latitudeI != 373346000 && positionMessage.longitudeI != -1220090000) {
|
||||
|
|
@ -462,24 +462,24 @@ func upsertDeviceConfigPacket(config: Config.DeviceConfig, nodeNum: Int64, sessi
|
|||
if fetchedNode[0].deviceConfig == nil {
|
||||
let newDeviceConfig = DeviceConfigEntity(context: context)
|
||||
newDeviceConfig.role = Int32(config.role.rawValue)
|
||||
newDeviceConfig.serialEnabled = config.serialEnabled
|
||||
newDeviceConfig.buttonGpio = Int32(config.buttonGpio)
|
||||
newDeviceConfig.buzzerGpio = Int32(config.buzzerGpio)
|
||||
newDeviceConfig.rebroadcastMode = Int32(config.rebroadcastMode.rawValue)
|
||||
newDeviceConfig.nodeInfoBroadcastSecs = Int32(truncating: config.nodeInfoBroadcastSecs as NSNumber)
|
||||
newDeviceConfig.doubleTapAsButtonPress = config.doubleTapAsButtonPress
|
||||
newDeviceConfig.tripleClickAsAdHocPing = !config.disableTripleClick
|
||||
newDeviceConfig.ledHeartbeatEnabled = !config.ledHeartbeatDisabled
|
||||
newDeviceConfig.isManaged = config.isManaged
|
||||
newDeviceConfig.tzdef = config.tzdef
|
||||
fetchedNode[0].deviceConfig = newDeviceConfig
|
||||
} else {
|
||||
fetchedNode[0].deviceConfig?.role = Int32(config.role.rawValue)
|
||||
fetchedNode[0].deviceConfig?.serialEnabled = config.serialEnabled
|
||||
fetchedNode[0].deviceConfig?.buttonGpio = Int32(config.buttonGpio)
|
||||
fetchedNode[0].deviceConfig?.buzzerGpio = Int32(config.buzzerGpio)
|
||||
fetchedNode[0].deviceConfig?.rebroadcastMode = Int32(config.rebroadcastMode.rawValue)
|
||||
fetchedNode[0].deviceConfig?.nodeInfoBroadcastSecs = Int32(truncating: config.nodeInfoBroadcastSecs as NSNumber)
|
||||
fetchedNode[0].deviceConfig?.doubleTapAsButtonPress = config.doubleTapAsButtonPress
|
||||
fetchedNode[0].deviceConfig?.tripleClickAsAdHocPing = !config.disableTripleClick
|
||||
fetchedNode[0].deviceConfig?.ledHeartbeatEnabled = !config.ledHeartbeatDisabled
|
||||
fetchedNode[0].deviceConfig?.isManaged = config.isManaged
|
||||
fetchedNode[0].deviceConfig?.tzdef = config.tzdef
|
||||
|
|
|
|||
|
|
@ -5,11 +5,8 @@
|
|||
// Copyright(c) Garth Vander Houwen 8/31/23.
|
||||
//
|
||||
import SwiftUI
|
||||
#if canImport(TipKit)
|
||||
import TipKit
|
||||
#endif
|
||||
|
||||
@available(iOS 17.0, macOS 14.0, *)
|
||||
struct BluetoothConnectionTip: Tip {
|
||||
|
||||
var id: String {
|
||||
|
|
|
|||
|
|
@ -5,11 +5,8 @@
|
|||
// Copyright(c) Garth Vander Houwen 8/31/23.
|
||||
//
|
||||
import SwiftUI
|
||||
#if canImport(TipKit)
|
||||
import TipKit
|
||||
#endif
|
||||
|
||||
@available(iOS 17.0, macOS 14.0, *)
|
||||
struct ShareChannelsTip: Tip {
|
||||
|
||||
var id: String {
|
||||
|
|
@ -26,7 +23,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
@available(iOS 17.0, macOS 14.0, *)
|
||||
struct CreateChannelsTip: Tip {
|
||||
|
||||
var id: String {
|
||||
|
|
@ -43,7 +39,6 @@ struct CreateChannelsTip: Tip {
|
|||
}
|
||||
}
|
||||
|
||||
@available(iOS 17.0, macOS 14.0, *)
|
||||
struct AdminChannelTip: Tip {
|
||||
|
||||
var id: String {
|
||||
|
|
|
|||
|
|
@ -5,11 +5,8 @@
|
|||
// Copyright(c) Garth Vander Houwen 9/15/23.
|
||||
//
|
||||
import SwiftUI
|
||||
#if canImport(TipKit)
|
||||
import TipKit
|
||||
#endif
|
||||
|
||||
@available(iOS 17.0, macOS 14.0, *)
|
||||
struct MessagesTip: Tip {
|
||||
|
||||
var id: String {
|
||||
|
|
|
|||
|
|
@ -11,9 +11,7 @@ import CoreData
|
|||
import CoreLocation
|
||||
import CoreBluetooth
|
||||
import OSLog
|
||||
#if canImport(TipKit)
|
||||
import TipKit
|
||||
#endif
|
||||
#if canImport(ActivityKit)
|
||||
import ActivityKit
|
||||
#endif
|
||||
|
|
@ -49,9 +47,7 @@ struct Connect: View {
|
|||
if bleManager.isSwitchedOn {
|
||||
Section(header: Text("connected.radio").font(.title)) {
|
||||
if let connectedPeripheral = bleManager.connectedPeripheral, connectedPeripheral.peripheral.state == .connected {
|
||||
if #available(iOS 17.0, macOS 14.0, *) {
|
||||
TipView(BluetoothConnectionTip(), arrowEdge: .bottom)
|
||||
}
|
||||
TipView(BluetoothConnectionTip(), arrowEdge: .bottom)
|
||||
VStack(alignment: .leading) {
|
||||
HStack {
|
||||
VStack(alignment: .center) {
|
||||
|
|
@ -78,12 +74,10 @@ struct Connect: View {
|
|||
.foregroundColor(.green)
|
||||
} else {
|
||||
HStack {
|
||||
if #available(iOS 17.0, macOS 14.0, *) {
|
||||
Image(systemName: "square.stack.3d.down.forward")
|
||||
.symbolRenderingMode(.multicolor)
|
||||
.symbolEffect(.variableColor.reversing.cumulative, options: .repeat(20).speed(3))
|
||||
.foregroundColor(.orange)
|
||||
}
|
||||
Image(systemName: "square.stack.3d.down.forward")
|
||||
.symbolRenderingMode(.multicolor)
|
||||
.symbolEffect(.variableColor.reversing.cumulative, options: .repeat(20).speed(3))
|
||||
.foregroundColor(.orange)
|
||||
Text("communicating").font(.callout)
|
||||
.foregroundColor(.orange)
|
||||
}
|
||||
|
|
@ -307,10 +301,10 @@ struct Connect: View {
|
|||
.presentationDetents([.large])
|
||||
.presentationDragIndicator(.automatic)
|
||||
}
|
||||
.onChange(of: (self.bleManager.invalidVersion)) { _ in
|
||||
.onChange(of: self.bleManager.invalidVersion) {
|
||||
invalidFirmwareVersion = self.bleManager.invalidVersion
|
||||
}
|
||||
.onChange(of: (self.bleManager.isSubscribed)) { sub in
|
||||
.onChange(of: self.bleManager.isSubscribed) { _, sub in
|
||||
|
||||
if UserDefaults.preferredPeripheralId.count > 0 && sub {
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@
|
|||
|
||||
import SwiftUI
|
||||
|
||||
@available(iOS 17.0, *)
|
||||
struct ContentView: View {
|
||||
@ObservedObject
|
||||
var appState: AppState
|
||||
|
|
@ -39,19 +38,11 @@ struct ContentView: View {
|
|||
}
|
||||
.tag(NavigationState.Tab.nodes)
|
||||
|
||||
if #available(iOS 17.0, macOS 14.0, *), !UserDefaults.mapUseLegacy {
|
||||
MeshMap(router: appState.router)
|
||||
.tabItem {
|
||||
Label("map", systemImage: "map")
|
||||
}
|
||||
.tag(NavigationState.Tab.map)
|
||||
} else {
|
||||
NodeMap(router: appState.router)
|
||||
.tabItem {
|
||||
Label("map", systemImage: "map")
|
||||
}
|
||||
.tag(NavigationState.Tab.map)
|
||||
}
|
||||
MeshMap(router: appState.router)
|
||||
.tabItem {
|
||||
Label("map", systemImage: "map")
|
||||
}
|
||||
.tag(NavigationState.Tab.map)
|
||||
|
||||
Settings(
|
||||
router: appState.router
|
||||
|
|
|
|||
|
|
@ -1,164 +1,164 @@
|
|||
////
|
||||
//// NodeMapControl.swift
|
||||
//// Meshtastic
|
||||
////
|
||||
//// Created by Garth Vander Houwen on 9/9/23.
|
||||
////
|
||||
//import SwiftUI
|
||||
//import CoreLocation
|
||||
//import MapKit
|
||||
//import WeatherKit
|
||||
//import OSLog
|
||||
//
|
||||
// NodeMapControl.swift
|
||||
// Meshtastic
|
||||
//struct NodeMapMapkit: View {
|
||||
//
|
||||
// Created by Garth Vander Houwen on 9/9/23.
|
||||
// @Environment(\.managedObjectContext) var context
|
||||
// @EnvironmentObject var bleManager: BLEManager
|
||||
// /// Weather
|
||||
// /// The current weather condition for the city.
|
||||
// @State private var condition: WeatherCondition?
|
||||
// @State private var temperature: Measurement<UnitTemperature>?
|
||||
// @State private var humidity: Int?
|
||||
// @State private var symbolName: String = "cloud.fill"
|
||||
// @State private var attributionLink: URL?
|
||||
// @State private var attributionLogo: URL?
|
||||
//
|
||||
import SwiftUI
|
||||
import CoreLocation
|
||||
import MapKit
|
||||
import WeatherKit
|
||||
import OSLog
|
||||
|
||||
struct NodeMapMapkit: View {
|
||||
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@EnvironmentObject var bleManager: BLEManager
|
||||
/// Weather
|
||||
/// The current weather condition for the city.
|
||||
@State private var condition: WeatherCondition?
|
||||
@State private var temperature: Measurement<UnitTemperature>?
|
||||
@State private var humidity: Int?
|
||||
@State private var symbolName: String = "cloud.fill"
|
||||
@State private var attributionLink: URL?
|
||||
@State private var attributionLogo: URL?
|
||||
|
||||
@Environment(\.colorScheme) var colorScheme: ColorScheme
|
||||
@AppStorage("meshMapType") private var meshMapType = 0
|
||||
@AppStorage("meshMapShowNodeHistory") private var meshMapShowNodeHistory = false
|
||||
@AppStorage("meshMapShowRouteLines") private var meshMapShowRouteLines = false
|
||||
@State private var selectedMapLayer: MapLayer = .standard
|
||||
@State var waypointCoordinate: WaypointCoordinate?
|
||||
@State var editingWaypoint: Int = 0
|
||||
@State private var customMapOverlay: MapViewSwiftUI.CustomMapOverlay? = MapViewSwiftUI.CustomMapOverlay(
|
||||
mapName: "offlinemap",
|
||||
tileType: "png",
|
||||
canReplaceMapContent: true
|
||||
)
|
||||
@FetchRequest(sortDescriptors: [NSSortDescriptor(key: "name", ascending: false)],
|
||||
predicate: NSPredicate(
|
||||
format: "expire == nil || expire >= %@", Date() as NSDate
|
||||
), animation: .none)
|
||||
private var waypoints: FetchedResults<WaypointEntity>
|
||||
@ObservedObject var node: NodeInfoEntity
|
||||
|
||||
var body: some View {
|
||||
|
||||
NavigationStack {
|
||||
GeometryReader { bounds in
|
||||
VStack {
|
||||
if node.hasPositions {
|
||||
ZStack {
|
||||
let positionArray = node.positions?.array as? [PositionEntity] ?? []
|
||||
let lastTenThousand = Array(positionArray.prefix(10000))
|
||||
// let todaysPositions = positionArray.filter { $0.time! >= Calendar.current.startOfDay(for: Date()) }
|
||||
ZStack {
|
||||
MapViewSwiftUI(onLongPress: { coord in
|
||||
waypointCoordinate = WaypointCoordinate(id: .init(), coordinate: coord, waypointId: 0)
|
||||
}, onWaypointEdit: { wpId in
|
||||
if wpId > 0 {
|
||||
waypointCoordinate = WaypointCoordinate(id: .init(), coordinate: nil, waypointId: Int64(wpId))
|
||||
}
|
||||
},
|
||||
selectedMapLayer: selectedMapLayer,
|
||||
positions: lastTenThousand,
|
||||
waypoints: Array(waypoints),
|
||||
userTrackingMode: MKUserTrackingMode.none,
|
||||
showNodeHistory: meshMapShowNodeHistory,
|
||||
showRouteLines: meshMapShowRouteLines,
|
||||
customMapOverlay: self.customMapOverlay
|
||||
)
|
||||
VStack(alignment: .leading) {
|
||||
Spacer()
|
||||
HStack(alignment: .bottom, spacing: 1) {
|
||||
Picker("Map Type", selection: $selectedMapLayer) {
|
||||
ForEach(MapLayer.allCases, id: \.self) { layer in
|
||||
if layer == MapLayer.offline && UserDefaults.enableOfflineMaps {
|
||||
Text(layer.localized)
|
||||
} else if layer != MapLayer.offline {
|
||||
Text(layer.localized)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: (selectedMapLayer)) { newMapLayer in
|
||||
UserDefaults.mapLayer = newMapLayer
|
||||
}
|
||||
.background(.thinMaterial, in: RoundedRectangle(cornerRadius: 12, style: .continuous))
|
||||
.pickerStyle(.menu)
|
||||
.padding(5)
|
||||
VStack {
|
||||
VStack {
|
||||
Label(temperature?.formatted(.measurement(width: .narrow)) ?? "??", systemImage: symbolName)
|
||||
.font(.caption)
|
||||
|
||||
Label("\(humidity ?? 0)%", systemImage: "humidity")
|
||||
.font(.caption2)
|
||||
|
||||
AsyncImage(url: attributionLogo) { image in
|
||||
image
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
} placeholder: {
|
||||
ProgressView()
|
||||
.controlSize(.mini)
|
||||
}
|
||||
.frame(height: 10)
|
||||
|
||||
Link("Other data sources", destination: attributionLink ?? URL(string: "https://weather-data.apple.com/legal-attribution.html")!)
|
||||
.font(.caption2)
|
||||
}
|
||||
.padding(5)
|
||||
|
||||
}
|
||||
.background(.thinMaterial, in: RoundedRectangle(cornerRadius: 12, style: .continuous))
|
||||
.padding(5)
|
||||
.task {
|
||||
do {
|
||||
if node.hasPositions {
|
||||
let mostRecent = node.positions?.lastObject as? PositionEntity
|
||||
let weather = try await WeatherService.shared.weather(for: mostRecent?.nodeLocation ?? CLLocation(latitude: LocationHelper.currentLocation.latitude, longitude: LocationHelper.currentLocation.longitude))
|
||||
condition = weather.currentWeather.condition
|
||||
temperature = weather.currentWeather.temperature
|
||||
humidity = Int(weather.currentWeather.humidity * 100)
|
||||
symbolName = weather.currentWeather.symbolName
|
||||
let attribution = try await WeatherService.shared.attribution
|
||||
attributionLink = attribution.legalPageURL
|
||||
attributionLogo = colorScheme == .light ? attribution.combinedMarkLightURL : attribution.combinedMarkDarkURL
|
||||
}
|
||||
} catch {
|
||||
Logger.services.error("Could not gather weather information: \(error.localizedDescription)")
|
||||
condition = .clear
|
||||
symbolName = "cloud.fill"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.ignoresSafeArea(.all, edges: [.top, .leading, .trailing])
|
||||
.frame(idealWidth: bounds.size.width, minHeight: bounds.size.height / 1.65)
|
||||
}
|
||||
} else {
|
||||
HStack {
|
||||
}
|
||||
.padding([.top], 20)
|
||||
}
|
||||
}
|
||||
.edgesIgnoringSafeArea([.leading, .trailing])
|
||||
.sheet(item: $waypointCoordinate, content: { wpc in
|
||||
WaypointFormMapKit(coordinate: wpc)
|
||||
.presentationDetents([.medium, .large])
|
||||
.presentationDragIndicator(.automatic)
|
||||
})
|
||||
.navigationBarTitle(String(node.user?.longName ?? "unknown".localized), displayMode: .inline)
|
||||
.navigationBarItems(trailing:
|
||||
ZStack {
|
||||
ConnectedDevice(
|
||||
bluetoothOn: bleManager.isSwitchedOn,
|
||||
deviceConnected: bleManager.connectedPeripheral != nil,
|
||||
name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?")
|
||||
})
|
||||
}
|
||||
.padding(.bottom, 2)
|
||||
}
|
||||
}
|
||||
}
|
||||
// @Environment(\.colorScheme) var colorScheme: ColorScheme
|
||||
// @AppStorage("meshMapType") private var meshMapType = 0
|
||||
// @AppStorage("meshMapShowNodeHistory") private var meshMapShowNodeHistory = false
|
||||
// @AppStorage("meshMapShowRouteLines") private var meshMapShowRouteLines = false
|
||||
// @State private var selectedMapLayer: MapLayer = .standard
|
||||
// @State var waypointCoordinate: WaypointCoordinate?
|
||||
// @State var editingWaypoint: Int = 0
|
||||
// @State private var customMapOverlay: MapViewSwiftUI.CustomMapOverlay? = MapViewSwiftUI.CustomMapOverlay(
|
||||
// mapName: "offlinemap",
|
||||
// tileType: "png",
|
||||
// canReplaceMapContent: true
|
||||
// )
|
||||
// @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "name", ascending: false)],
|
||||
// predicate: NSPredicate(
|
||||
// format: "expire == nil || expire >= %@", Date() as NSDate
|
||||
// ), animation: .none)
|
||||
// private var waypoints: FetchedResults<WaypointEntity>
|
||||
// @ObservedObject var node: NodeInfoEntity
|
||||
//
|
||||
// var body: some View {
|
||||
//
|
||||
// NavigationStack {
|
||||
// GeometryReader { bounds in
|
||||
// VStack {
|
||||
// if node.hasPositions {
|
||||
// ZStack {
|
||||
// let positionArray = node.positions?.array as? [PositionEntity] ?? []
|
||||
// let lastTenThousand = Array(positionArray.prefix(10000))
|
||||
// // let todaysPositions = positionArray.filter { $0.time! >= Calendar.current.startOfDay(for: Date()) }
|
||||
// ZStack {
|
||||
// MapViewSwiftUI(onLongPress: { coord in
|
||||
// waypointCoordinate = WaypointCoordinate(id: .init(), coordinate: coord, waypointId: 0)
|
||||
// }, onWaypointEdit: { wpId in
|
||||
// if wpId > 0 {
|
||||
// waypointCoordinate = WaypointCoordinate(id: .init(), coordinate: nil, waypointId: Int64(wpId))
|
||||
// }
|
||||
// },
|
||||
// selectedMapLayer: selectedMapLayer,
|
||||
// positions: lastTenThousand,
|
||||
// waypoints: Array(waypoints),
|
||||
// userTrackingMode: MKUserTrackingMode.none,
|
||||
// showNodeHistory: meshMapShowNodeHistory,
|
||||
// showRouteLines: meshMapShowRouteLines,
|
||||
// customMapOverlay: self.customMapOverlay
|
||||
// )
|
||||
// VStack(alignment: .leading) {
|
||||
// Spacer()
|
||||
// HStack(alignment: .bottom, spacing: 1) {
|
||||
// Picker("Map Type", selection: $selectedMapLayer) {
|
||||
// ForEach(MapLayer.allCases, id: \.self) { layer in
|
||||
// if layer == MapLayer.offline && UserDefaults.enableOfflineMaps {
|
||||
// Text(layer.localized)
|
||||
// } else if layer != MapLayer.offline {
|
||||
// Text(layer.localized)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// .onChange(of: (selectedMapLayer)) { newMapLayer in
|
||||
// UserDefaults.mapLayer = newMapLayer
|
||||
// }
|
||||
// .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 12, style: .continuous))
|
||||
// .pickerStyle(.menu)
|
||||
// .padding(5)
|
||||
// VStack {
|
||||
// VStack {
|
||||
// Label(temperature?.formatted(.measurement(width: .narrow)) ?? "??", systemImage: symbolName)
|
||||
// .font(.caption)
|
||||
//
|
||||
// Label("\(humidity ?? 0)%", systemImage: "humidity")
|
||||
// .font(.caption2)
|
||||
//
|
||||
// AsyncImage(url: attributionLogo) { image in
|
||||
// image
|
||||
// .resizable()
|
||||
// .scaledToFit()
|
||||
// } placeholder: {
|
||||
// ProgressView()
|
||||
// .controlSize(.mini)
|
||||
// }
|
||||
// .frame(height: 10)
|
||||
//
|
||||
// Link("Other data sources", destination: attributionLink ?? URL(string: "https://weather-data.apple.com/legal-attribution.html")!)
|
||||
// .font(.caption2)
|
||||
// }
|
||||
// .padding(5)
|
||||
//
|
||||
// }
|
||||
// .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 12, style: .continuous))
|
||||
// .padding(5)
|
||||
// .task {
|
||||
// do {
|
||||
// if node.hasPositions {
|
||||
// let mostRecent = node.positions?.lastObject as? PositionEntity
|
||||
// let weather = try await WeatherService.shared.weather(for: mostRecent?.nodeLocation ?? CLLocation(latitude: LocationHelper.currentLocation.latitude, longitude: LocationHelper.currentLocation.longitude))
|
||||
// condition = weather.currentWeather.condition
|
||||
// temperature = weather.currentWeather.temperature
|
||||
// humidity = Int(weather.currentWeather.humidity * 100)
|
||||
// symbolName = weather.currentWeather.symbolName
|
||||
// let attribution = try await WeatherService.shared.attribution
|
||||
// attributionLink = attribution.legalPageURL
|
||||
// attributionLogo = colorScheme == .light ? attribution.combinedMarkLightURL : attribution.combinedMarkDarkURL
|
||||
// }
|
||||
// } catch {
|
||||
// Logger.services.error("Could not gather weather information: \(error.localizedDescription)")
|
||||
// condition = .clear
|
||||
// symbolName = "cloud.fill"
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// .ignoresSafeArea(.all, edges: [.top, .leading, .trailing])
|
||||
// .frame(idealWidth: bounds.size.width, minHeight: bounds.size.height / 1.65)
|
||||
// }
|
||||
// } else {
|
||||
// HStack {
|
||||
// }
|
||||
// .padding([.top], 20)
|
||||
// }
|
||||
// }
|
||||
// .edgesIgnoringSafeArea([.leading, .trailing])
|
||||
// .sheet(item: $waypointCoordinate, content: { wpc in
|
||||
// WaypointFormMapKit(coordinate: wpc)
|
||||
// .presentationDetents([.medium, .large])
|
||||
// .presentationDragIndicator(.automatic)
|
||||
// })
|
||||
// .navigationBarTitle(String(node.user?.longName ?? "unknown".localized), displayMode: .inline)
|
||||
// .navigationBarItems(trailing:
|
||||
// ZStack {
|
||||
// ConnectedDevice(
|
||||
// bluetoothOn: bleManager.isSwitchedOn,
|
||||
// deviceConnected: bleManager.connectedPeripheral != nil,
|
||||
// name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?")
|
||||
// })
|
||||
// }
|
||||
// .padding(.bottom, 2)
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ struct WaypointFormMapKit: View {
|
|||
axis: .vertical
|
||||
)
|
||||
.foregroundColor(Color.gray)
|
||||
.onChange(of: name, perform: { _ in
|
||||
.onChange(of: name) {
|
||||
var totalBytes = name.utf8.count
|
||||
// Only mess with the value if it is too big
|
||||
while totalBytes > 30 {
|
||||
|
|
@ -61,7 +61,7 @@ struct WaypointFormMapKit: View {
|
|||
if totalBytes > 30 {
|
||||
name = String(name.dropLast())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
HStack {
|
||||
Text("Description")
|
||||
|
|
@ -72,14 +72,14 @@ struct WaypointFormMapKit: View {
|
|||
axis: .vertical
|
||||
)
|
||||
.foregroundColor(Color.gray)
|
||||
.onChange(of: description, perform: { _ in
|
||||
.onChange(of: description) {
|
||||
var totalBytes = description.utf8.count
|
||||
// Only mess with the value if it is too big
|
||||
while totalBytes > 100 {
|
||||
description = String(description.dropLast())
|
||||
totalBytes = description.utf8.count
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
HStack {
|
||||
Text("Icon")
|
||||
|
|
@ -87,7 +87,7 @@ struct WaypointFormMapKit: View {
|
|||
EmojiOnlyTextField(text: $icon, placeholder: "Select an emoji")
|
||||
.font(.title)
|
||||
.focused($iconIsFocused)
|
||||
.onChange(of: icon) { value in
|
||||
.onChange(of: icon) { _, value in
|
||||
|
||||
// If you have anything other than emojis in your string make it empty
|
||||
if !value.onlyEmojis() {
|
||||
|
|
|
|||
|
|
@ -134,11 +134,11 @@ struct ChannelMessageList: View {
|
|||
scrollView.scrollTo(channel.allPrivateMessages.last?.messageId ?? 0, anchor: .bottom)
|
||||
}
|
||||
}
|
||||
.onChange(of: channel.allPrivateMessages, perform: { _ in
|
||||
.onChange(of: channel.allPrivateMessages) {
|
||||
withAnimation {
|
||||
scrollView.scrollTo(channel.allPrivateMessages.last?.messageId ?? 0, anchor: .bottom)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
TextMessageField(
|
||||
|
|
|
|||
|
|
@ -47,23 +47,14 @@ struct MessageText: View {
|
|||
let isDetectionSensorMessage = message.portNum == Int32(PortNum.detectionSensorApp.rawValue)
|
||||
if tapBackDestination.overlaySensorMessage {
|
||||
VStack {
|
||||
if #available(iOS 17.0, macOS 14.0, *) {
|
||||
isDetectionSensorMessage ? Image(systemName: "sensor.fill")
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topTrailing)
|
||||
.foregroundStyle(Color.orange)
|
||||
.symbolRenderingMode(.multicolor)
|
||||
.symbolEffect(.variableColor.reversing.cumulative, options: .repeat(20).speed(3))
|
||||
.offset(x: 20, y: -20)
|
||||
: nil
|
||||
} else {
|
||||
isDetectionSensorMessage ? Image(systemName: "sensor.fill")
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topTrailing)
|
||||
.foregroundStyle(Color.orange)
|
||||
.offset(x: 20, y: -20)
|
||||
: nil
|
||||
}
|
||||
isDetectionSensorMessage ? Image(systemName: "sensor.fill")
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topTrailing)
|
||||
.foregroundStyle(Color.orange)
|
||||
.symbolRenderingMode(.multicolor)
|
||||
.symbolEffect(.variableColor.reversing.cumulative, options: .repeat(20).speed(3))
|
||||
.offset(x: 20, y: -20)
|
||||
: nil
|
||||
}
|
||||
} else {
|
||||
EmptyView()
|
||||
|
|
|
|||
|
|
@ -8,9 +8,7 @@
|
|||
import SwiftUI
|
||||
import CoreData
|
||||
import OSLog
|
||||
#if canImport(TipKit)
|
||||
import TipKit
|
||||
#endif
|
||||
|
||||
struct Messages: View {
|
||||
|
||||
|
|
@ -65,9 +63,7 @@ struct Messages: View {
|
|||
}
|
||||
}
|
||||
|
||||
if #available(iOS 17.0, macOS 14.0, *) {
|
||||
TipView(MessagesTip(), arrowEdge: .top)
|
||||
}
|
||||
TipView(MessagesTip(), arrowEdge: .top)
|
||||
}
|
||||
.navigationTitle("messages")
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
|
|
@ -91,7 +87,7 @@ struct Messages: View {
|
|||
} else if case .directMessages = router.navigationState.messages {
|
||||
Text("Select a conversation")
|
||||
}
|
||||
}.onChange(of: router.navigationState) { _ in
|
||||
}.onChange(of: router.navigationState) {
|
||||
setupNavigationState()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,14 +30,14 @@ struct TextMessageField: View {
|
|||
HStack(alignment: .top) {
|
||||
ZStack {
|
||||
TextField("message", text: $typingMessage, axis: .vertical)
|
||||
.onChange(of: typingMessage, perform: { value in
|
||||
.onChange(of: typingMessage) { _, value in
|
||||
totalBytes = value.utf8.count
|
||||
// Only mess with the value if it is too big
|
||||
while totalBytes > Self.maxbytes {
|
||||
typingMessage = String(typingMessage.dropLast())
|
||||
totalBytes = typingMessage.utf8.count
|
||||
}
|
||||
})
|
||||
}
|
||||
.keyboardType(.default)
|
||||
.toolbar {
|
||||
ToolbarItemGroup(placement: .keyboard) {
|
||||
|
|
|
|||
|
|
@ -8,9 +8,7 @@
|
|||
import SwiftUI
|
||||
import CoreData
|
||||
import OSLog
|
||||
#if canImport(TipKit)
|
||||
import TipKit
|
||||
#endif
|
||||
|
||||
struct UserList: View {
|
||||
|
||||
|
|
@ -201,12 +199,12 @@ struct UserList: View {
|
|||
.sheet(isPresented: $showingHelp) {
|
||||
DirectMessagesHelp()
|
||||
}
|
||||
.onChange(of: searchText) { _ in
|
||||
.onChange(of: searchText) {
|
||||
Task {
|
||||
await searchUserList()
|
||||
}
|
||||
}
|
||||
.onChange(of: viaLora) { _ in
|
||||
.onChange(of: viaLora) {
|
||||
if !viaLora && !viaMqtt {
|
||||
viaMqtt = true
|
||||
}
|
||||
|
|
@ -214,7 +212,7 @@ struct UserList: View {
|
|||
await searchUserList()
|
||||
}
|
||||
}
|
||||
.onChange(of: viaMqtt) { _ in
|
||||
.onChange(of: viaMqtt) {
|
||||
if !viaLora && !viaMqtt {
|
||||
viaLora = true
|
||||
}
|
||||
|
|
@ -222,27 +220,27 @@ struct UserList: View {
|
|||
await searchUserList()
|
||||
}
|
||||
}
|
||||
.onChange(of: [deviceRoles]) { _ in
|
||||
.onChange(of: [deviceRoles]) {
|
||||
Task {
|
||||
await searchUserList()
|
||||
}
|
||||
}
|
||||
.onChange(of: hopsAway) { _ in
|
||||
.onChange(of: hopsAway) {
|
||||
Task {
|
||||
await searchUserList()
|
||||
}
|
||||
}
|
||||
.onChange(of: [boolFilters]) { _ in
|
||||
.onChange(of: [boolFilters]) {
|
||||
Task {
|
||||
await searchUserList()
|
||||
}
|
||||
}
|
||||
.onChange(of: maxDistance) { _ in
|
||||
.onChange(of: maxDistance) {
|
||||
Task {
|
||||
await searchUserList()
|
||||
}
|
||||
}
|
||||
.onChange(of: isPkiEncrypted) { _ in
|
||||
.onChange(of: isPkiEncrypted) {
|
||||
Task {
|
||||
await searchUserList()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -122,11 +122,11 @@ struct UserMessageList: View {
|
|||
scrollView.scrollTo(user.messageList.last?.messageId ?? 0, anchor: .bottom)
|
||||
}
|
||||
}
|
||||
.onChange(of: user.messageList, perform: { _ in
|
||||
.onChange(of: user.messageList) {
|
||||
withAnimation {
|
||||
scrollView.scrollTo(user.messageList.last?.messageId ?? 0, anchor: .bottom)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
TextMessageField(
|
||||
|
|
|
|||
|
|
@ -36,32 +36,31 @@ struct DeviceMetricsLog: View {
|
|||
.sorted { $0.time! < $1.time! }
|
||||
if chartData.count > 0 {
|
||||
GroupBox(label: Label("\(deviceMetrics.count) Readings Total", systemImage: "chart.xyaxis.line")) {
|
||||
if #available(iOS 17.0, macOS 14.0, *) {
|
||||
Chart {
|
||||
ForEach(chartData, id: \.self) { point in
|
||||
Plot {
|
||||
LineMark(
|
||||
x: .value("x", point.time!),
|
||||
y: .value("y", point.batteryLevel)
|
||||
)
|
||||
}
|
||||
.accessibilityLabel("Line Series")
|
||||
.accessibilityValue("X: \(point.time!), Y: \(point.batteryLevel)")
|
||||
.foregroundStyle(batteryChartColor)
|
||||
.interpolationMethod(.linear)
|
||||
Plot {
|
||||
PointMark(
|
||||
x: .value("x", point.time!),
|
||||
y: .value("y", point.channelUtilization)
|
||||
)
|
||||
.symbolSize(25)
|
||||
}
|
||||
.accessibilityLabel("Line Series")
|
||||
.accessibilityValue("X: \(point.time!), Y: \(point.channelUtilization)")
|
||||
.foregroundStyle(channelUtilizationChartColor)
|
||||
if let chartSelection {
|
||||
RuleMark(x: .value("Second", chartSelection, unit: .second))
|
||||
.foregroundStyle(.tertiary.opacity(0.5))
|
||||
Chart {
|
||||
ForEach(chartData, id: \.self) { point in
|
||||
Plot {
|
||||
LineMark(
|
||||
x: .value("x", point.time!),
|
||||
y: .value("y", point.batteryLevel)
|
||||
)
|
||||
}
|
||||
.accessibilityLabel("Line Series")
|
||||
.accessibilityValue("X: \(point.time!), Y: \(point.batteryLevel)")
|
||||
.foregroundStyle(batteryChartColor)
|
||||
.interpolationMethod(.linear)
|
||||
Plot {
|
||||
PointMark(
|
||||
x: .value("x", point.time!),
|
||||
y: .value("y", point.channelUtilization)
|
||||
)
|
||||
.symbolSize(25)
|
||||
}
|
||||
.accessibilityLabel("Line Series")
|
||||
.accessibilityValue("X: \(point.time!), Y: \(point.channelUtilization)")
|
||||
.foregroundStyle(channelUtilizationChartColor)
|
||||
if let chartSelection {
|
||||
RuleMark(x: .value("Second", chartSelection, unit: .second))
|
||||
.foregroundStyle(.tertiary.opacity(0.5))
|
||||
// .annotation(
|
||||
// position: .automatic,
|
||||
// overflowResolution: .init(x: .fit, y: .disabled)
|
||||
|
|
@ -75,91 +74,37 @@ struct DeviceMetricsLog: View {
|
|||
// .foregroundStyle(Color.accentColor.opacity(0.2))
|
||||
// }
|
||||
// }
|
||||
}
|
||||
RuleMark(y: .value("Network Status Orange", 25))
|
||||
.lineStyle(StrokeStyle(lineWidth: 1, dash: [5, 10]))
|
||||
.foregroundStyle(.orange)
|
||||
RuleMark(y: .value("Network Status Red", 50))
|
||||
.lineStyle(StrokeStyle(lineWidth: 1, dash: [5, 10]))
|
||||
.foregroundStyle(.red)
|
||||
Plot {
|
||||
PointMark(
|
||||
x: .value("x", point.time!),
|
||||
y: .value("y", point.airUtilTx)
|
||||
)
|
||||
.symbolSize(25)
|
||||
}
|
||||
.accessibilityLabel("Line Series")
|
||||
.accessibilityValue("X: \(point.time!), Y: \(point.airUtilTx)")
|
||||
.foregroundStyle(airtimeChartColor)
|
||||
}
|
||||
}
|
||||
.chartXAxis(content: {
|
||||
AxisMarks(position: .top)
|
||||
})
|
||||
.chartXAxis(.automatic)
|
||||
.chartXSelection(value: $chartSelection)
|
||||
.chartYScale(domain: 0...100)
|
||||
.chartForegroundStyleScale([
|
||||
idiom == .phone ? "Battery" : "Battery Level": batteryChartColor,
|
||||
"Channel Utilization": channelUtilizationChartColor,
|
||||
"Airtime": airtimeChartColor
|
||||
])
|
||||
.chartLegend(position: .automatic, alignment: .bottom)
|
||||
} else {
|
||||
// Fallback on earlier versions
|
||||
Chart {
|
||||
ForEach(chartData, id: \.self) { point in
|
||||
Plot {
|
||||
LineMark(
|
||||
x: .value("x", point.time!),
|
||||
y: .value("y", point.batteryLevel)
|
||||
)
|
||||
}
|
||||
.accessibilityLabel("Line Series")
|
||||
.accessibilityValue("X: \(point.time!), Y: \(point.batteryLevel)")
|
||||
.foregroundStyle(batteryChartColor)
|
||||
.interpolationMethod(.linear)
|
||||
Plot {
|
||||
PointMark(
|
||||
x: .value("x", point.time!),
|
||||
y: .value("y", point.channelUtilization)
|
||||
)
|
||||
.symbolSize(25)
|
||||
}
|
||||
.accessibilityLabel("Line Series")
|
||||
.accessibilityValue("X: \(point.time!), Y: \(point.channelUtilization)")
|
||||
.foregroundStyle(channelUtilizationChartColor)
|
||||
RuleMark(y: .value("Network Status Orange", 25))
|
||||
.lineStyle(StrokeStyle(lineWidth: 1, dash: [5, 10]))
|
||||
.foregroundStyle(.orange)
|
||||
RuleMark(y: .value("Network Status Red", 50))
|
||||
.lineStyle(StrokeStyle(lineWidth: 1, dash: [5, 10]))
|
||||
.foregroundStyle(.red)
|
||||
Plot {
|
||||
PointMark(
|
||||
x: .value("x", point.time!),
|
||||
y: .value("y", point.airUtilTx)
|
||||
)
|
||||
.symbolSize(25)
|
||||
}
|
||||
.accessibilityLabel("Line Series")
|
||||
.accessibilityValue("X: \(point.time!), Y: \(point.airUtilTx)")
|
||||
.foregroundStyle(airtimeChartColor)
|
||||
RuleMark(y: .value("Network Status Orange", 25))
|
||||
.lineStyle(StrokeStyle(lineWidth: 1, dash: [5, 10]))
|
||||
.foregroundStyle(.orange)
|
||||
RuleMark(y: .value("Network Status Red", 50))
|
||||
.lineStyle(StrokeStyle(lineWidth: 1, dash: [5, 10]))
|
||||
.foregroundStyle(.red)
|
||||
Plot {
|
||||
PointMark(
|
||||
x: .value("x", point.time!),
|
||||
y: .value("y", point.airUtilTx)
|
||||
)
|
||||
.symbolSize(25)
|
||||
}
|
||||
.accessibilityLabel("Line Series")
|
||||
.accessibilityValue("X: \(point.time!), Y: \(point.airUtilTx)")
|
||||
.foregroundStyle(airtimeChartColor)
|
||||
}
|
||||
.chartXAxis(content: {
|
||||
AxisMarks(position: .top)
|
||||
})
|
||||
.chartXAxis(.automatic)
|
||||
.chartYScale(domain: 0...100)
|
||||
.chartForegroundStyleScale([
|
||||
idiom == .phone ? "Battery" : "Battery Level": batteryChartColor,
|
||||
"Channel Utilization": channelUtilizationChartColor,
|
||||
"Airtime": airtimeChartColor
|
||||
])
|
||||
.chartLegend(position: .automatic, alignment: .bottom)
|
||||
}
|
||||
.chartXAxis(content: {
|
||||
AxisMarks(position: .top)
|
||||
})
|
||||
.chartXAxis(.automatic)
|
||||
.chartXSelection(value: $chartSelection)
|
||||
.chartYScale(domain: 0...100)
|
||||
.chartForegroundStyleScale([
|
||||
idiom == .phone ? "Battery" : "Battery Level": batteryChartColor,
|
||||
"Channel Utilization": channelUtilizationChartColor,
|
||||
"Airtime": airtimeChartColor
|
||||
])
|
||||
.chartLegend(position: .automatic, alignment: .bottom)
|
||||
}
|
||||
.frame(minHeight: 240)
|
||||
}
|
||||
|
|
@ -262,18 +207,14 @@ struct DeviceMetricsLog: View {
|
|||
.padding(.bottom)
|
||||
.padding(.trailing)
|
||||
}
|
||||
.onChange(of: selection) { newSelection in
|
||||
.onChange(of: selection) { _, newSelection in
|
||||
guard let metrics = deviceMetrics.first(where: { $0.id == newSelection }) else {
|
||||
return
|
||||
}
|
||||
chartSelection = metrics.time
|
||||
}
|
||||
} else {
|
||||
if #available (iOS 17, *) {
|
||||
ContentUnavailableView("No Device Metrics", systemImage: "slash.circle")
|
||||
} else {
|
||||
Text("No Device Metrics")
|
||||
}
|
||||
ContentUnavailableView("No Device Metrics", systemImage: "slash.circle")
|
||||
}
|
||||
}
|
||||
.navigationTitle("device.metrics.log")
|
||||
|
|
|
|||
|
|
@ -193,11 +193,7 @@ struct EnvironmentMetricsLog: View {
|
|||
}
|
||||
|
||||
} else {
|
||||
if #available (iOS 17, *) {
|
||||
ContentUnavailableView("No Environment Metrics", systemImage: "slash.circle")
|
||||
} else {
|
||||
Text("No Environment Metrics")
|
||||
}
|
||||
ContentUnavailableView("No Environment Metrics", systemImage: "slash.circle")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@
|
|||
import SwiftUI
|
||||
import MapKit
|
||||
|
||||
@available(iOS 17.0, macOS 14.0, *)
|
||||
struct MeshMapContent: MapContent {
|
||||
|
||||
/// Parameters
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import SwiftUI
|
|||
import MapKit
|
||||
import CoreData
|
||||
|
||||
@available(iOS 17.0, macOS 14.0, *)
|
||||
struct NodeMapContent: MapContent {
|
||||
|
||||
@ObservedObject var node: NodeInfoEntity
|
||||
|
|
|
|||
|
|
@ -6,11 +6,8 @@
|
|||
//
|
||||
|
||||
import SwiftUI
|
||||
#if canImport(MapKit)
|
||||
import MapKit
|
||||
#endif
|
||||
|
||||
@available(iOS 17.0, macOS 14.0, *)
|
||||
struct MapSettingsForm: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@State private var currentDetent = PresentationDetent.medium
|
||||
|
|
@ -39,7 +36,7 @@ struct MapSettingsForm: View {
|
|||
.pickerStyle(SegmentedPickerStyle())
|
||||
.padding(.top, 5)
|
||||
.padding(.bottom, 5)
|
||||
.onChange(of: mapLayer) { newMapLayer in
|
||||
.onChange(of: mapLayer) { _, newMapLayer in
|
||||
UserDefaults.mapLayer = newMapLayer
|
||||
}
|
||||
if meshMap {
|
||||
|
|
@ -53,7 +50,7 @@ struct MapSettingsForm: View {
|
|||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
}
|
||||
.onChange(of: meshMapDistance) { newMeshMapDistance in
|
||||
.onChange(of: meshMapDistance) { _, newMeshMapDistance in
|
||||
UserDefaults.meshMapDistance = newMeshMapDistance
|
||||
}
|
||||
Toggle(isOn: $waypoints) {
|
||||
|
|
|
|||
|
|
@ -7,11 +7,8 @@
|
|||
|
||||
import SwiftUI
|
||||
import CoreLocation
|
||||
#if canImport(MapKit)
|
||||
import MapKit
|
||||
#endif
|
||||
|
||||
@available(iOS 17.0, macOS 14.0, *)
|
||||
struct NodeMapSwiftUI: View {
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@EnvironmentObject var bleManager: BLEManager
|
||||
|
|
@ -83,7 +80,7 @@ struct NodeMapSwiftUI: View {
|
|||
}
|
||||
.sheet(isPresented: $isEditingSettings) {
|
||||
MapSettingsForm(traffic: $showTraffic, pointsOfInterest: $showPointsOfInterest, mapLayer: $selectedMapLayer, meshMap: $isMeshMap)
|
||||
.onChange(of: (selectedMapLayer)) { newMapLayer in
|
||||
.onChange(of: (selectedMapLayer)) { _, newMapLayer in
|
||||
switch selectedMapLayer {
|
||||
case .standard:
|
||||
UserDefaults.mapLayer = newMapLayer
|
||||
|
|
|
|||
|
|
@ -7,16 +7,13 @@
|
|||
|
||||
import SwiftUI
|
||||
import Charts
|
||||
#if canImport(MapKit)
|
||||
import MapKit
|
||||
#endif
|
||||
|
||||
struct PositionAltitude {
|
||||
let time: Date
|
||||
var altitude: Measurement<UnitLength>
|
||||
}
|
||||
|
||||
@available(iOS 17.0, macOS 14.0, *)
|
||||
struct PositionAltitudeChart: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@ObservedObject var node: NodeInfoEntity
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@
|
|||
import SwiftUI
|
||||
import MapKit
|
||||
|
||||
@available(iOS 17.0, macOS 14.0, *)
|
||||
struct PositionPopover: View {
|
||||
|
||||
@ObservedObject var locationsHandler = LocationsHandler.shared
|
||||
|
|
@ -96,7 +95,6 @@ struct PositionPopover: View {
|
|||
}
|
||||
/// Altitude
|
||||
Label {
|
||||
let formatter = MeasurementFormatter()
|
||||
let distanceInMeters = Measurement(value: Double(position.altitude), unit: UnitLength.meters)
|
||||
let distanceInFeet = distanceInMeters.converted(to: UnitLength.feet)
|
||||
if Locale.current.measurementSystem == .metric {
|
||||
|
|
@ -215,20 +213,12 @@ struct PositionPopover: View {
|
|||
.padding(.bottom)
|
||||
}
|
||||
if position.nodePosition?.hasDetectionSensorMetrics ?? false {
|
||||
if #available(iOS 17.0, macOS 14.0, *) {
|
||||
Image(systemName: "sensor.fill")
|
||||
.symbolEffect(.variableColor.reversing.cumulative, options: .repeat(20).speed(3))
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.foregroundColor(.accentColor)
|
||||
.font(.largeTitle)
|
||||
.padding(.bottom)
|
||||
} else {
|
||||
Image(systemName: "sensor.fill")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.foregroundColor(.accentColor)
|
||||
.font(.largeTitle)
|
||||
.padding(.bottom)
|
||||
}
|
||||
Image(systemName: "sensor.fill")
|
||||
.symbolEffect(.variableColor.reversing.cumulative, options: .repeat(20).speed(3))
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.foregroundColor(.accentColor)
|
||||
.font(.largeTitle)
|
||||
.padding(.bottom)
|
||||
}
|
||||
BatteryGauge(node: position.nodePosition!)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,14 +65,14 @@ struct WaypointForm: View {
|
|||
axis: .vertical
|
||||
)
|
||||
.foregroundColor(Color.gray)
|
||||
.onChange(of: name, perform: { _ in
|
||||
.onChange(of: name) {
|
||||
var totalBytes = name.utf8.count
|
||||
// Only mess with the value if it is too big
|
||||
while totalBytes > 30 {
|
||||
name = String(name.dropLast())
|
||||
totalBytes = name.utf8.count
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
HStack {
|
||||
Text("Description")
|
||||
|
|
@ -83,14 +83,14 @@ struct WaypointForm: View {
|
|||
axis: .vertical
|
||||
)
|
||||
.foregroundColor(Color.gray)
|
||||
.onChange(of: description, perform: { _ in
|
||||
.onChange(of: description) {
|
||||
var totalBytes = description.utf8.count
|
||||
// Only mess with the value if it is too big
|
||||
while totalBytes > 100 {
|
||||
description = String(description.dropLast())
|
||||
totalBytes = description.utf8.count
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
HStack {
|
||||
Text("Icon")
|
||||
|
|
@ -98,7 +98,7 @@ struct WaypointForm: View {
|
|||
EmojiOnlyTextField(text: $icon, placeholder: "Select an emoji")
|
||||
.font(.title)
|
||||
.focused($iconIsFocused)
|
||||
.onChange(of: icon) { value in
|
||||
.onChange(of: icon) { _, value in
|
||||
|
||||
// If you have anything other than emojis in your string make it empty
|
||||
if !value.onlyEmojis() {
|
||||
|
|
|
|||
|
|
@ -221,11 +221,7 @@ struct NodeDetail: View {
|
|||
.disabled(!node.hasDeviceMetrics)
|
||||
|
||||
NavigationLink {
|
||||
if #available (iOS 17, macOS 14, *) {
|
||||
NodeMapSwiftUI(node: node, showUserLocation: connectedNode?.num ?? 0 == node.num)
|
||||
} else {
|
||||
NodeMapMapkit(node: node)
|
||||
}
|
||||
NodeMapSwiftUI(node: node, showUserLocation: connectedNode?.num ?? 0 == node.num)
|
||||
} label: {
|
||||
Label {
|
||||
Text("Node Map")
|
||||
|
|
@ -260,19 +256,17 @@ struct NodeDetail: View {
|
|||
}
|
||||
.disabled(!node.hasEnvironmentMetrics)
|
||||
|
||||
if #available(iOS 17.0, macOS 14.0, *) {
|
||||
NavigationLink {
|
||||
TraceRouteLog(node: node)
|
||||
} label: {
|
||||
Label {
|
||||
Text("Trace Route Log")
|
||||
} icon: {
|
||||
Image(systemName: "signpost.right.and.left")
|
||||
.symbolRenderingMode(.multicolor)
|
||||
}
|
||||
NavigationLink {
|
||||
TraceRouteLog(node: node)
|
||||
} label: {
|
||||
Label {
|
||||
Text("Trace Route Log")
|
||||
} icon: {
|
||||
Image(systemName: "signpost.right.and.left")
|
||||
.symbolRenderingMode(.multicolor)
|
||||
}
|
||||
.disabled(node.traceRoutes?.count ?? 0 == 0)
|
||||
}
|
||||
.disabled(node.traceRoutes?.count ?? 0 == 0)
|
||||
|
||||
NavigationLink {
|
||||
DetectionSensorLog(node: node)
|
||||
|
|
|
|||
|
|
@ -101,36 +101,9 @@ struct NodeListItem: View {
|
|||
if node.positions?.count ?? 0 > 0 && connectedNode != node.num {
|
||||
HStack {
|
||||
if let lastPostion = node.positions?.lastObject as? PositionEntity {
|
||||
if #available(iOS 17.0, macOS 14.0, *) {
|
||||
if let currentLocation = LocationsHandler.shared.locationsArray.last {
|
||||
let myCoord = CLLocation(latitude: currentLocation.coordinate.latitude, longitude: currentLocation.coordinate.longitude)
|
||||
if lastPostion.nodeCoordinate != nil && myCoord.coordinate.longitude != LocationsHandler.DefaultLocation.longitude && myCoord.coordinate.latitude != LocationsHandler.DefaultLocation.latitude {
|
||||
let nodeCoord = CLLocation(latitude: lastPostion.nodeCoordinate!.latitude, longitude: lastPostion.nodeCoordinate!.longitude)
|
||||
let metersAway = nodeCoord.distance(from: myCoord)
|
||||
Image(systemName: "lines.measurement.horizontal")
|
||||
.font(.callout)
|
||||
.symbolRenderingMode(.multicolor)
|
||||
.frame(width: 30)
|
||||
DistanceText(meters: metersAway)
|
||||
.font(UIDevice.current.userInterfaceIdiom == .phone ? .callout : .caption)
|
||||
.foregroundColor(.gray)
|
||||
let trueBearing = getBearingBetweenTwoPoints(point1: myCoord, point2: nodeCoord)
|
||||
let headingDegrees = Angle.degrees(trueBearing)
|
||||
Image(systemName: "location.north")
|
||||
.font(.callout)
|
||||
.symbolRenderingMode(.multicolor)
|
||||
.clipShape(Circle())
|
||||
.rotationEffect(headingDegrees)
|
||||
let heading = Measurement(value: trueBearing, unit: UnitAngle.degrees)
|
||||
Text("\(heading.formatted(.measurement(width: .narrow, numberFormatStyle: .number.precision(.fractionLength(0)))))")
|
||||
.font(UIDevice.current.userInterfaceIdiom == .phone ? .callout : .caption)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
let myCoord = CLLocation(latitude: LocationHelper.currentLocation.latitude, longitude: LocationHelper.currentLocation.longitude)
|
||||
if lastPostion.nodeCoordinate != nil && myCoord.coordinate.longitude != LocationHelper.DefaultLocation.longitude && myCoord.coordinate.latitude != LocationHelper.DefaultLocation.latitude {
|
||||
if let currentLocation = LocationsHandler.shared.locationsArray.last {
|
||||
let myCoord = CLLocation(latitude: currentLocation.coordinate.latitude, longitude: currentLocation.coordinate.longitude)
|
||||
if lastPostion.nodeCoordinate != nil && myCoord.coordinate.longitude != LocationsHandler.DefaultLocation.longitude && myCoord.coordinate.latitude != LocationsHandler.DefaultLocation.latitude {
|
||||
let nodeCoord = CLLocation(latitude: lastPostion.nodeCoordinate!.latitude, longitude: lastPostion.nodeCoordinate!.longitude)
|
||||
let metersAway = nodeCoord.distance(from: myCoord)
|
||||
Image(systemName: "lines.measurement.horizontal")
|
||||
|
|
@ -139,7 +112,7 @@ struct NodeListItem: View {
|
|||
.frame(width: 30)
|
||||
DistanceText(meters: metersAway)
|
||||
.font(UIDevice.current.userInterfaceIdiom == .phone ? .callout : .caption)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundColor(.gray)
|
||||
let trueBearing = getBearingBetweenTwoPoints(point1: myCoord, point2: nodeCoord)
|
||||
let headingDegrees = Angle.degrees(trueBearing)
|
||||
Image(systemName: "location.north")
|
||||
|
|
@ -211,13 +184,11 @@ struct NodeListItem: View {
|
|||
.font(.callout)
|
||||
.frame(width: 30)
|
||||
}
|
||||
if #available(iOS 17.0, macOS 14.0, *) {
|
||||
if node.hasTraceRoutes {
|
||||
Image(systemName: "signpost.right.and.left")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.font(.callout)
|
||||
.frame(width: 30)
|
||||
}
|
||||
if node.hasTraceRoutes {
|
||||
Image(systemName: "signpost.right.and.left")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.font(.callout)
|
||||
.frame(width: 30)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,11 +10,8 @@ import CoreData
|
|||
import CoreLocation
|
||||
import Foundation
|
||||
import OSLog
|
||||
#if canImport(MapKit)
|
||||
import MapKit
|
||||
#endif
|
||||
|
||||
@available(iOS 17.0, macOS 14.0, *)
|
||||
struct MeshMap: View {
|
||||
|
||||
@Environment(\.managedObjectContext) var context
|
||||
|
|
@ -142,7 +139,7 @@ struct MeshMap: View {
|
|||
guard case .map = router.navigationState.selectedTab else { return }
|
||||
// TODO: handle deep link for waypoints
|
||||
}
|
||||
.onChange(of: selectedMapLayer) { newMapLayer in
|
||||
.onChange(of: selectedMapLayer) { _, newMapLayer in
|
||||
switch selectedMapLayer {
|
||||
case .standard:
|
||||
UserDefaults.mapLayer = newMapLayer
|
||||
|
|
@ -186,17 +183,6 @@ struct MeshMap: View {
|
|||
.tint(Color(UIColor.secondarySystemBackground))
|
||||
.foregroundColor(.accentColor)
|
||||
.buttonStyle(.borderedProminent)
|
||||
// Button(action: {
|
||||
// withAnimation {
|
||||
// editingFilters = !editingFilters
|
||||
// }
|
||||
// }) {
|
||||
// Image(systemName: !editingFilters ? "line.3.horizontal.decrease.circle" : "line.3.horizontal.decrease.circle.fill")
|
||||
// .padding(.vertical, 5)
|
||||
// }
|
||||
// .tint(Color(UIColor.secondarySystemBackground))
|
||||
// .foregroundColor(.accentColor)
|
||||
// .buttonStyle(.borderedProminent)
|
||||
}
|
||||
.controlSize(.regular)
|
||||
.padding(5)
|
||||
|
|
|
|||
|
|
@ -274,26 +274,18 @@ struct NodeList: View {
|
|||
)
|
||||
}
|
||||
} else {
|
||||
if #available (iOS 17, *) {
|
||||
ContentUnavailableView("select.node", systemImage: "flipphone")
|
||||
} else {
|
||||
Text("select.node")
|
||||
}
|
||||
ContentUnavailableView("select.node", systemImage: "flipphone")
|
||||
}
|
||||
} detail: {
|
||||
if #available (iOS 17, *) {
|
||||
ContentUnavailableView("", systemImage: "line.3.horizontal")
|
||||
} else {
|
||||
Text("Select something to view")
|
||||
}
|
||||
ContentUnavailableView("", systemImage: "line.3.horizontal")
|
||||
}
|
||||
.navigationSplitViewStyle(.balanced)
|
||||
.onChange(of: searchText) { _ in
|
||||
.onChange(of: searchText) {
|
||||
Task {
|
||||
await searchNodeList()
|
||||
}
|
||||
}
|
||||
.onChange(of: viaLora) { _ in
|
||||
.onChange(of: viaLora) {
|
||||
if !viaLora && !viaMqtt {
|
||||
viaMqtt = true
|
||||
}
|
||||
|
|
@ -301,7 +293,7 @@ struct NodeList: View {
|
|||
await searchNodeList()
|
||||
}
|
||||
}
|
||||
.onChange(of: viaMqtt) { _ in
|
||||
.onChange(of: viaMqtt) {
|
||||
if !viaLora && !viaMqtt {
|
||||
viaLora = true
|
||||
}
|
||||
|
|
@ -309,32 +301,32 @@ struct NodeList: View {
|
|||
await searchNodeList()
|
||||
}
|
||||
}
|
||||
.onChange(of: [boolFilters]) { _ in
|
||||
.onChange(of: [boolFilters]) {
|
||||
Task {
|
||||
await searchNodeList()
|
||||
}
|
||||
}
|
||||
.onChange(of: [deviceRoles]) { _ in
|
||||
.onChange(of: [deviceRoles]) {
|
||||
Task {
|
||||
await searchNodeList()
|
||||
}
|
||||
}
|
||||
.onChange(of: hopsAway) { _ in
|
||||
.onChange(of: hopsAway) {
|
||||
Task {
|
||||
await searchNodeList()
|
||||
}
|
||||
}
|
||||
.onChange(of: maxDistance) { _ in
|
||||
.onChange(of: maxDistance) {
|
||||
Task {
|
||||
await searchNodeList()
|
||||
}
|
||||
}
|
||||
.onChange(of: distanceFilter) { _ in
|
||||
.onChange(of: distanceFilter) {
|
||||
Task {
|
||||
await searchNodeList()
|
||||
}
|
||||
}
|
||||
.onChange(of: router.navigationState) { _ in
|
||||
.onChange(of: router.navigationState) {
|
||||
if let selected = router.navigationState.nodeListSelectedNodeNum {
|
||||
self.selectedNode = getNodeInfo(id: selected, context: context)
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -26,8 +26,6 @@ struct NodeMap: View {
|
|||
@State var selectedOverlayServer: MapOverlayServer = UserDefaults.mapOverlayServer
|
||||
@State var mapTilesAboveLabels: Bool = UserDefaults.mapTilesAboveLabels
|
||||
let fromDate: NSDate = Calendar.current.date(byAdding: .month, value: -1, to: Date())! as NSDate
|
||||
// @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "time", ascending: true)],
|
||||
// predicate: NSPredicate(format: "time >= %@ && nodePosition != nil", Calendar.current.date(byAdding: .day, value: -7, to: Date())! as NSDate), animation: .none)
|
||||
@FetchRequest(sortDescriptors: [NSSortDescriptor(key: "time", ascending: true)],
|
||||
predicate: NSPredicate(format: "nodePosition != nil", Calendar.current.date(byAdding: .day, value: -7, to: Date())! as NSDate), animation: .none)
|
||||
private var positions: FetchedResults<PositionEntity>
|
||||
|
|
@ -97,7 +95,7 @@ struct NodeMap: View {
|
|||
}
|
||||
}
|
||||
.pickerStyle(SegmentedPickerStyle())
|
||||
.onChange(of: (selectedMapLayer)) { newMapLayer in
|
||||
.onChange(of: selectedMapLayer) { _, newMapLayer in
|
||||
UserDefaults.mapLayer = newMapLayer
|
||||
}
|
||||
.padding(.top, 5)
|
||||
|
|
@ -144,10 +142,10 @@ struct NodeMap: View {
|
|||
.font(.footnote)
|
||||
}
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
.onChange(of: (selectedOverlayServer)) { newSelectedOverlayServer in
|
||||
UserDefaults.mapOverlayServer = newSelectedOverlayServer
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
.onChange(of: (selectedOverlayServer)) { _, newSelectedOverlayServer in
|
||||
UserDefaults.mapOverlayServer = newSelectedOverlayServer
|
||||
}
|
||||
Text(LocalizedStringKey(selectedOverlayServer.attribution))
|
||||
.font(.footnote)
|
||||
.foregroundColor(.gray)
|
||||
|
|
@ -160,7 +158,7 @@ struct NodeMap: View {
|
|||
Text("Enable Offline Maps")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
.onChange(of: enableOfflineMaps) { newEnableOfflineMaps in
|
||||
.onChange(of: enableOfflineMaps) { _, newEnableOfflineMaps in
|
||||
UserDefaults.enableOfflineMaps = newEnableOfflineMaps
|
||||
if !enableOfflineMaps {
|
||||
if self.selectedMapLayer == .offline {
|
||||
|
|
@ -176,10 +174,10 @@ struct NodeMap: View {
|
|||
Text(tsl.description)
|
||||
}
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
.onChange(of: (selectedTileServer)) { newSelectedTileServer in
|
||||
UserDefaults.mapTileServer = newSelectedTileServer
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
.onChange(of: (selectedTileServer)) { _, newSelectedTileServer in
|
||||
UserDefaults.mapTileServer = newSelectedTileServer
|
||||
}
|
||||
Text("Attribution:")
|
||||
.fontWeight(.semibold)
|
||||
.font(.footnote)
|
||||
|
|
|
|||
|
|
@ -196,11 +196,7 @@ struct PaxCounterLog: View {
|
|||
.padding(.trailing)
|
||||
}
|
||||
} else {
|
||||
if #available (iOS 17, *) {
|
||||
ContentUnavailableView("paxcounter.content.unavailable", systemImage: "slash.circle")
|
||||
} else {
|
||||
Text("paxcounter.content.unavailable")
|
||||
}
|
||||
ContentUnavailableView("paxcounter.content.unavailable", systemImage: "slash.circle")
|
||||
}
|
||||
}
|
||||
.navigationTitle("paxcounter.log")
|
||||
|
|
|
|||
|
|
@ -166,11 +166,7 @@ struct PositionLog: View {
|
|||
)
|
||||
|
||||
} else {
|
||||
if #available (iOS 17, *) {
|
||||
ContentUnavailableView("No Positions", systemImage: "mappin.slash")
|
||||
} else {
|
||||
Text("No Positions")
|
||||
}
|
||||
ContentUnavailableView("No Positions", systemImage: "mappin.slash")
|
||||
}
|
||||
}
|
||||
.navigationTitle("Position Log \(node.positions?.count ?? 0) Points")
|
||||
|
|
|
|||
|
|
@ -8,11 +8,8 @@
|
|||
import SwiftUI
|
||||
import CoreData
|
||||
import OSLog
|
||||
#if canImport(MapKit)
|
||||
import MapKit
|
||||
#endif
|
||||
|
||||
@available(iOS 17.0, macOS 14.0, *)
|
||||
struct TraceRouteLog: View {
|
||||
private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom }
|
||||
@ObservedObject var locationsHandler = LocationsHandler.shared
|
||||
|
|
|
|||
|
|
@ -22,9 +22,7 @@ struct AppData: View {
|
|||
VStack {
|
||||
|
||||
Section(header: Text("phone.gps")) {
|
||||
if #available(iOS 17.0, macOS 14.0, *) {
|
||||
GPSStatus()
|
||||
}
|
||||
GPSStatus()
|
||||
}
|
||||
Divider()
|
||||
Button(action: {
|
||||
|
|
@ -69,8 +67,6 @@ struct AppData: View {
|
|||
let container = NSPersistentContainer(name: "Meshtastic")
|
||||
do {
|
||||
try container.restorePersistentStore(from: file.absoluteURL)
|
||||
let request = MyInfoEntity.fetchRequest()
|
||||
try context.fetch(request)
|
||||
UserDefaults.preferredPeripheralId = ""
|
||||
UserDefaults.preferredPeripheralNum = Int(file.pathComponents[(idiom == .phone || idiom == .pad) ? 9 : 10]) ?? 0
|
||||
Logger.data.notice("🗂️ Restored a core data backup to backup/\(UserDefaults.preferredPeripheralNum, privacy: .public)")
|
||||
|
|
|
|||
|
|
@ -8,8 +8,6 @@
|
|||
import SwiftUI
|
||||
import OSLog
|
||||
|
||||
/// Needed for TableColumnForEach
|
||||
@available(iOS 17.0, macOS 14.0, *)
|
||||
struct AppLog: View {
|
||||
|
||||
@State private var logs: [OSLogEntryLog] = []
|
||||
|
|
@ -133,25 +131,25 @@ struct AppLog: View {
|
|||
logs.sort(using: sortOrder)
|
||||
}
|
||||
}
|
||||
.onChange(of: searchText) { _ in
|
||||
.onChange(of: searchText) {
|
||||
Task {
|
||||
await logs = searchAppLogs()
|
||||
logs.sort(using: sortOrder)
|
||||
}
|
||||
}
|
||||
.onChange(of: [categories]) { _ in
|
||||
.onChange(of: [categories]) {
|
||||
Task {
|
||||
await logs = searchAppLogs()
|
||||
logs.sort(using: sortOrder)
|
||||
}
|
||||
}
|
||||
.onChange(of: [levels]) { _ in
|
||||
.onChange(of: [levels]) {
|
||||
Task {
|
||||
await logs = searchAppLogs()
|
||||
logs.sort(using: sortOrder)
|
||||
}
|
||||
}
|
||||
.onChange(of: selection) { newSelection in
|
||||
.onChange(of: selection) { _, newSelection in
|
||||
presentingErrorDetails = true
|
||||
let log = logs.first {
|
||||
$0.id == newSelection
|
||||
|
|
@ -216,7 +214,6 @@ struct AppLog: View {
|
|||
}
|
||||
}
|
||||
|
||||
@available(iOS 17.0, macOS 14.0, *)
|
||||
extension AppLog {
|
||||
@MainActor
|
||||
private func searchAppLogs() async -> [OSLogEntryLog] {
|
||||
|
|
|
|||
|
|
@ -10,9 +10,7 @@ import MapKit
|
|||
import MeshtasticProtobufs
|
||||
import OSLog
|
||||
import SwiftUI
|
||||
#if canImport(TipKit)
|
||||
import TipKit
|
||||
#endif
|
||||
|
||||
func generateChannelKey(size: Int) -> String {
|
||||
var keyData = Data(count: size)
|
||||
|
|
@ -62,9 +60,7 @@ struct Channels: View {
|
|||
|
||||
VStack {
|
||||
List {
|
||||
if #available(iOS 17.0, macOS 14.0, *) {
|
||||
TipView(CreateChannelsTip(), arrowEdge: .bottom)
|
||||
}
|
||||
TipView(CreateChannelsTip(), arrowEdge: .bottom)
|
||||
if node != nil && node?.myInfo != nil {
|
||||
ForEach(node?.myInfo?.channels?.array as? [ChannelEntity] ?? [], id: \.self) { (channel: ChannelEntity) in
|
||||
Button(action: {
|
||||
|
|
|
|||
|
|
@ -6,9 +6,7 @@
|
|||
//
|
||||
|
||||
import SwiftUI
|
||||
#if canImport(MapKit)
|
||||
import MapKit
|
||||
#endif
|
||||
|
||||
struct ChannelForm: View {
|
||||
|
||||
|
|
@ -41,7 +39,7 @@ struct ChannelForm: View {
|
|||
.disableAutocorrection(true)
|
||||
.keyboardType(.alphabet)
|
||||
.foregroundColor(Color.gray)
|
||||
.onChange(of: channelName, perform: { _ in
|
||||
.onChange(of: channelName) {
|
||||
channelName = channelName.replacing(" ", with: "")
|
||||
var totalBytes = channelName.utf8.count
|
||||
// Only mess with the value if it is too big
|
||||
|
|
@ -50,7 +48,7 @@ struct ChannelForm: View {
|
|||
totalBytes = channelName.utf8.count
|
||||
}
|
||||
hasChanges = true
|
||||
})
|
||||
}
|
||||
}
|
||||
HStack {
|
||||
Picker("Key Size", selection: $channelKeySize) {
|
||||
|
|
@ -99,7 +97,7 @@ struct ChannelForm: View {
|
|||
, lineWidth: 2.0)
|
||||
|
||||
)
|
||||
.onChange(of: channelKey, perform: { _ in
|
||||
.onChange(of: channelKey) {
|
||||
|
||||
let tempKey = Data(base64Encoded: channelKey) ?? Data()
|
||||
if tempKey.count == channelKeySize || channelKeySize == -1 {
|
||||
|
|
@ -108,7 +106,7 @@ struct ChannelForm: View {
|
|||
hasValidKey = false
|
||||
}
|
||||
hasChanges = true
|
||||
})
|
||||
}
|
||||
.disabled(channelKeySize <= 0)
|
||||
}
|
||||
HStack {
|
||||
|
|
@ -148,7 +146,7 @@ struct ChannelForm: View {
|
|||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
.disabled(!supportedVersion)
|
||||
.listRowSeparator(.visible)
|
||||
.onChange(of: preciseLocation) { pl in
|
||||
.onChange(of: preciseLocation) { _, pl in
|
||||
if pl == false {
|
||||
positionPrecision = 14
|
||||
}
|
||||
|
|
@ -186,10 +184,10 @@ struct ChannelForm: View {
|
|||
.listRowSeparator(.visible)
|
||||
}
|
||||
}
|
||||
.onChange(of: channelName) { _ in
|
||||
.onChange(of: channelName) {
|
||||
hasChanges = true
|
||||
}
|
||||
.onChange(of: channelKeySize) { _ in
|
||||
.onChange(of: channelKeySize) {
|
||||
if channelKeySize == -1 {
|
||||
channelKey = "AQ=="
|
||||
} else {
|
||||
|
|
@ -198,10 +196,10 @@ struct ChannelForm: View {
|
|||
}
|
||||
hasChanges = true
|
||||
}
|
||||
.onChange(of: channelKey) { _ in
|
||||
.onChange(of: channelKey) {
|
||||
hasChanges = true
|
||||
}
|
||||
.onChange(of: channelKeySize) { _ in
|
||||
.onChange(of: channelKeySize) {
|
||||
if channelKeySize == -1 {
|
||||
if channelRole == 0 {
|
||||
preciseLocation = false
|
||||
|
|
@ -209,10 +207,10 @@ struct ChannelForm: View {
|
|||
channelKey = "AQ=="
|
||||
}
|
||||
}
|
||||
.onChange(of: channelRole) { _ in
|
||||
.onChange(of: channelRole) {
|
||||
hasChanges = true
|
||||
}
|
||||
.onChange(of: preciseLocation) { loc in
|
||||
.onChange(of: preciseLocation) { _, loc in
|
||||
if loc == true {
|
||||
if channelKey == "AQ==" {
|
||||
preciseLocation = false
|
||||
|
|
@ -225,10 +223,10 @@ struct ChannelForm: View {
|
|||
}
|
||||
hasChanges = true
|
||||
}
|
||||
.onChange(of: positionPrecision) { _ in
|
||||
.onChange(of: positionPrecision) {
|
||||
hasChanges = true
|
||||
}
|
||||
.onChange(of: positionsEnabled) { pe in
|
||||
.onChange(of: positionsEnabled) { _, pe in
|
||||
if pe {
|
||||
if positionPrecision == 0 {
|
||||
positionPrecision = 14
|
||||
|
|
@ -238,10 +236,10 @@ struct ChannelForm: View {
|
|||
}
|
||||
hasChanges = true
|
||||
}
|
||||
.onChange(of: uplink) { _ in
|
||||
.onChange(of: uplink) {
|
||||
hasChanges = true
|
||||
}
|
||||
.onChange(of: downlink) { _ in
|
||||
.onChange(of: downlink) {
|
||||
hasChanges = true
|
||||
}
|
||||
.onFirstAppear {
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ struct BluetoothConfig: View {
|
|||
Label("bluetooth.mode.fixedpin", systemImage: "wallet.pass")
|
||||
TextField("bluetooth.mode.fixedpin", text: $fixedPin)
|
||||
.foregroundColor(.gray)
|
||||
.onChange(of: fixedPin, perform: { _ in
|
||||
.onChange(of: fixedPin) {
|
||||
// Don't let the first character be 0 because it will get stripped when saving a UInt32
|
||||
if fixedPin.first == "0" {
|
||||
fixedPin = fixedPin.replacing("0", with: "")
|
||||
|
|
@ -59,7 +59,7 @@ struct BluetoothConfig: View {
|
|||
} else if fixedPin.utf8.count < pinLength {
|
||||
shortPin = true
|
||||
}
|
||||
})
|
||||
}
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
.keyboardType(.decimalPad)
|
||||
|
|
@ -121,14 +121,14 @@ struct BluetoothConfig: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: enabled) {
|
||||
if $0 != node?.bluetoothConfig?.enabled { hasChanges = true }
|
||||
.onChange(of: enabled) { oldEnabled, newEnabled in
|
||||
if oldEnabled != newEnabled && newEnabled != node?.bluetoothConfig?.enabled { hasChanges = true }
|
||||
}
|
||||
.onChange(of: mode) {
|
||||
if $0 != node?.bluetoothConfig?.mode ?? -1 { hasChanges = true }
|
||||
.onChange(of: mode) { oldNode, newNode in
|
||||
if oldNode != newNode && newNode != node?.bluetoothConfig?.mode ?? -1 { hasChanges = true }
|
||||
}
|
||||
.onChange(of: fixedPin) { newFixedPin in
|
||||
if newFixedPin != String(node?.bluetoothConfig?.fixedPin ?? -1) { hasChanges = true }
|
||||
.onChange(of: fixedPin) { oldFixedPin, newFixedPin in
|
||||
if oldFixedPin != newFixedPin && newFixedPin != String(node?.bluetoothConfig?.fixedPin ?? -1) { hasChanges = true }
|
||||
}
|
||||
}
|
||||
func setBluetoothValues() {
|
||||
|
|
|
|||
|
|
@ -23,11 +23,11 @@ struct DeviceConfig: View {
|
|||
@State var deviceRole = 0
|
||||
@State var buzzerGPIO = 0
|
||||
@State var buttonGPIO = 0
|
||||
@State var serialEnabled = true
|
||||
@State var rebroadcastMode = 0
|
||||
@State var nodeInfoBroadcastSecs = 10800
|
||||
@State var doubleTapAsButtonPress = false
|
||||
@State var ledHeartbeatEnabled = true
|
||||
@State var tripleClickAsAdHocPing = true
|
||||
@State var tzdef = ""
|
||||
|
||||
var body: some View {
|
||||
|
|
@ -77,6 +77,12 @@ struct DeviceConfig: View {
|
|||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
|
||||
Toggle(isOn: $tripleClickAsAdHocPing) {
|
||||
Label("Triple Click Ad Hoc Ping", systemImage: "map.pin")
|
||||
Text("Send a position on the primary channel when the user button is triple clicked.")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
|
||||
Toggle(isOn: $ledHeartbeatEnabled) {
|
||||
Label("LED Heartbeat", systemImage: "waveform.path.ecg")
|
||||
Text("Controls the blinking LED on the device. For most devices this will control one of the up to 4 LEDS, the charger and GPS LEDs are not controllable.")
|
||||
|
|
@ -84,23 +90,19 @@ struct DeviceConfig: View {
|
|||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
}
|
||||
Section(header: Text("Debug")) {
|
||||
Toggle(isOn: $serialEnabled) {
|
||||
Label("Serial Console", systemImage: "terminal")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
VStack(alignment: .leading) {
|
||||
HStack {
|
||||
Label("Time Zone", systemImage: "clock.badge.exclamationmark")
|
||||
TextField("Time Zone", text: $tzdef, axis: .vertical)
|
||||
.foregroundColor(.gray)
|
||||
.onChange(of: tzdef, perform: { _ in
|
||||
.onChange(of: tzdef) {
|
||||
var totalBytes = tzdef.utf8.count
|
||||
// Only mess with the value if it is too big
|
||||
while totalBytes > 63 {
|
||||
tzdef = String(tzdef.dropLast())
|
||||
totalBytes = tzdef.utf8.count
|
||||
}
|
||||
})
|
||||
}
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
.keyboardType(.default)
|
||||
|
|
@ -194,12 +196,12 @@ struct DeviceConfig: View {
|
|||
if connectedNode != nil {
|
||||
var dc = Config.DeviceConfig()
|
||||
dc.role = DeviceRoles(rawValue: deviceRole)!.protoEnumValue()
|
||||
dc.serialEnabled = serialEnabled
|
||||
dc.buttonGpio = UInt32(buttonGPIO)
|
||||
dc.buzzerGpio = UInt32(buzzerGPIO)
|
||||
dc.rebroadcastMode = RebroadcastModes(rawValue: rebroadcastMode)?.protoEnumValue() ?? RebroadcastModes.all.protoEnumValue()
|
||||
dc.nodeInfoBroadcastSecs = UInt32(nodeInfoBroadcastSecs)
|
||||
dc.doubleTapAsButtonPress = doubleTapAsButtonPress
|
||||
dc.disableTripleClick = !tripleClickAsAdHocPing
|
||||
dc.tzdef = tzdef
|
||||
dc.ledHeartbeatDisabled = !ledHeartbeatEnabled
|
||||
let adminMessageId = bleManager.saveDeviceConfig(config: dc, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0)
|
||||
|
|
@ -247,39 +249,36 @@ struct DeviceConfig: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: deviceRole) {
|
||||
if $0 != node?.deviceConfig?.role ?? -1 { hasChanges = true }
|
||||
.onChange(of: deviceRole) { oldRole, newRole in
|
||||
if oldRole != newRole && newRole != node?.deviceConfig?.role ?? -1 { hasChanges = true }
|
||||
}
|
||||
.onChange(of: serialEnabled) {
|
||||
if $0 != node?.deviceConfig?.serialEnabled { hasChanges = true }
|
||||
.onChange(of: buttonGPIO) { oldButtonGPIO, newButtonGPIO in
|
||||
if oldButtonGPIO != newButtonGPIO && newButtonGPIO != node?.deviceConfig?.buttonGpio ?? -1 { hasChanges = true }
|
||||
}
|
||||
.onChange(of: buttonGPIO) { newButtonGPIO in
|
||||
if newButtonGPIO != node?.deviceConfig?.buttonGpio ?? -1 { hasChanges = true }
|
||||
.onChange(of: buzzerGPIO) { oldBuzzerGPIO, newBuzzerGPIO in
|
||||
if oldBuzzerGPIO != newBuzzerGPIO && newBuzzerGPIO != node?.deviceConfig?.buzzerGpio ?? -1 { hasChanges = true }
|
||||
}
|
||||
.onChange(of: buzzerGPIO) { newBuzzerGPIO in
|
||||
if newBuzzerGPIO != node?.deviceConfig?.buttonGpio ?? -1 { hasChanges = true }
|
||||
.onChange(of: rebroadcastMode) { oldRebroadcastMode, newRebroadcastMode in
|
||||
if oldRebroadcastMode != newRebroadcastMode && newRebroadcastMode != node?.deviceConfig?.rebroadcastMode ?? -1 { hasChanges = true }
|
||||
}
|
||||
.onChange(of: rebroadcastMode) { newRebroadcastMode in
|
||||
if newRebroadcastMode != node?.deviceConfig?.rebroadcastMode ?? -1 { hasChanges = true }
|
||||
.onChange(of: nodeInfoBroadcastSecs) { oldNodeInfoBroadcastSecs, newNodeInfoBroadcastSecs in
|
||||
if oldNodeInfoBroadcastSecs != newNodeInfoBroadcastSecs && newNodeInfoBroadcastSecs != node?.deviceConfig?.nodeInfoBroadcastSecs ?? -1 { hasChanges = true }
|
||||
}
|
||||
.onChange(of: nodeInfoBroadcastSecs) { newNodeInfoBroadcastSecs in
|
||||
if newNodeInfoBroadcastSecs != node?.deviceConfig?.nodeInfoBroadcastSecs ?? -1 { hasChanges = true }
|
||||
.onChange(of: doubleTapAsButtonPress) { oldDoubleTapAsButtonPress, newDoubleTapAsButtonPress in
|
||||
if oldDoubleTapAsButtonPress != newDoubleTapAsButtonPress && newDoubleTapAsButtonPress != node?.deviceConfig?.doubleTapAsButtonPress ?? false { hasChanges = true }
|
||||
}
|
||||
.onChange(of: doubleTapAsButtonPress) {
|
||||
if $0 != node?.deviceConfig?.doubleTapAsButtonPress { hasChanges = true }
|
||||
.onChange(of: tripleClickAsAdHocPing) { oldTripleClickAsAdHocPing, newTripleClickAsAdHocPing in
|
||||
if oldTripleClickAsAdHocPing != newTripleClickAsAdHocPing && newTripleClickAsAdHocPing != node?.deviceConfig?.tripleClickAsAdHocPing ?? false { hasChanges = true }
|
||||
}
|
||||
.onChange(of: tzdef) { newTzdef in
|
||||
if newTzdef != node?.deviceConfig?.tzdef { hasChanges = true }
|
||||
.onChange(of: tzdef) { oldTzdef, newTzdef in
|
||||
if oldTzdef != newTzdef && newTzdef != node?.deviceConfig?.tzdef { hasChanges = true }
|
||||
}
|
||||
.onChange(of: ledHeartbeatEnabled) { newLedHeartbeatEnabled in
|
||||
if node != nil && node?.deviceConfig != nil {
|
||||
if newLedHeartbeatEnabled != node!.deviceConfig!.ledHeartbeatEnabled { hasChanges = true }
|
||||
}
|
||||
.onChange(of: ledHeartbeatEnabled) { oldLedHeartbeatEnabled, newLedHeartbeatEnabled in
|
||||
if oldLedHeartbeatEnabled != newLedHeartbeatEnabled && newLedHeartbeatEnabled != node?.deviceConfig?.ledHeartbeatEnabled ?? false { hasChanges = true }
|
||||
}
|
||||
}
|
||||
func setDeviceValues() {
|
||||
self.deviceRole = Int(node?.deviceConfig?.role ?? 0)
|
||||
self.serialEnabled = (node?.deviceConfig?.serialEnabled ?? true)
|
||||
self.buttonGPIO = Int(node?.deviceConfig?.buttonGpio ?? 0)
|
||||
self.buzzerGPIO = Int(node?.deviceConfig?.buzzerGpio ?? 0)
|
||||
self.rebroadcastMode = Int(node?.deviceConfig?.rebroadcastMode ?? 0)
|
||||
|
|
@ -288,6 +287,7 @@ struct DeviceConfig: View {
|
|||
nodeInfoBroadcastSecs = 3600
|
||||
}
|
||||
self.doubleTapAsButtonPress = node?.deviceConfig?.doubleTapAsButtonPress ?? false
|
||||
self.tripleClickAsAdHocPing = node?.deviceConfig?.tripleClickAsAdHocPing ?? false
|
||||
self.ledHeartbeatEnabled = node?.deviceConfig?.ledHeartbeatEnabled ?? true
|
||||
self.tzdef = node?.deviceConfig?.tzdef ?? ""
|
||||
if self.tzdef.isEmpty {
|
||||
|
|
|
|||
|
|
@ -184,32 +184,32 @@ struct DisplayConfig: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: screenOnSeconds) { newScreenSecs in
|
||||
if newScreenSecs != node?.displayConfig?.screenOnSeconds ?? -1 { hasChanges = true }
|
||||
.onChange(of: screenOnSeconds) { oldScreenSecs, newScreenSecs in
|
||||
if oldScreenSecs != newScreenSecs && newScreenSecs != node?.displayConfig?.screenOnSeconds ?? -1 { hasChanges = true }
|
||||
}
|
||||
.onChange(of: screenCarouselInterval) { newCarouselSecs in
|
||||
if newCarouselSecs != node?.displayConfig?.screenCarouselInterval ?? -1 { hasChanges = true }
|
||||
.onChange(of: screenCarouselInterval) { oldCarouselSecs, newCarouselSecs in
|
||||
if oldCarouselSecs != newCarouselSecs && newCarouselSecs != node?.displayConfig?.screenCarouselInterval ?? -1 { hasChanges = true }
|
||||
}
|
||||
.onChange(of: compassNorthTop) {
|
||||
if $0 != node?.displayConfig?.compassNorthTop { hasChanges = true }
|
||||
.onChange(of: compassNorthTop) { oldCompassNorthTop, newCompassNorthTop in
|
||||
if oldCompassNorthTop != newCompassNorthTop && newCompassNorthTop != node?.displayConfig?.compassNorthTop { hasChanges = true }
|
||||
}
|
||||
.onChange(of: wakeOnTapOrMotion) {
|
||||
if $0 != node?.displayConfig?.wakeOnTapOrMotion { hasChanges = true }
|
||||
.onChange(of: wakeOnTapOrMotion) { oldWakeOnTapOrMotion, newWakeOnTapOrMotion in
|
||||
if oldWakeOnTapOrMotion != newWakeOnTapOrMotion && newWakeOnTapOrMotion != node?.displayConfig?.wakeOnTapOrMotion { hasChanges = true }
|
||||
}
|
||||
.onChange(of: gpsFormat) { newGpsFormat in
|
||||
if newGpsFormat != node?.displayConfig?.gpsFormat ?? -1 { hasChanges = true }
|
||||
.onChange(of: gpsFormat) { oldGpsFormat, newGpsFormat in
|
||||
if oldGpsFormat != newGpsFormat && newGpsFormat != node?.displayConfig?.gpsFormat ?? -1 { hasChanges = true }
|
||||
}
|
||||
.onChange(of: flipScreen) {
|
||||
if $0 != node?.displayConfig?.flipScreen { hasChanges = true }
|
||||
.onChange(of: flipScreen) { oldFlipScreen, newFlipScreen in
|
||||
if oldFlipScreen != newFlipScreen && newFlipScreen != node?.displayConfig?.flipScreen { hasChanges = true }
|
||||
}
|
||||
.onChange(of: oledType) { newOledType in
|
||||
if newOledType != node?.displayConfig?.oledType ?? -1 { hasChanges = true }
|
||||
.onChange(of: oledType) { oldOledType, newOledType in
|
||||
if oldOledType != newOledType && newOledType != node?.displayConfig?.oledType ?? -1 { hasChanges = true }
|
||||
}
|
||||
.onChange(of: displayMode) { newDisplayMode in
|
||||
if newDisplayMode != node?.displayConfig?.displayMode ?? -1 { hasChanges = true }
|
||||
.onChange(of: displayMode) { oldDisplayMode, newDisplayMode in
|
||||
if oldDisplayMode != newDisplayMode && newDisplayMode != node?.displayConfig?.displayMode ?? -1 { hasChanges = true }
|
||||
}
|
||||
.onChange(of: units) { newUnits in
|
||||
if newUnits != node?.displayConfig?.units ?? -1 { hasChanges = true }
|
||||
.onChange(of: units) { oldUnits, newUnits in
|
||||
if oldUnits != newUnits && newUnits != node?.displayConfig?.units ?? -1 { hasChanges = true }
|
||||
}
|
||||
}
|
||||
func setDisplayValues() {
|
||||
|
|
|
|||
|
|
@ -259,47 +259,47 @@ struct LoRaConfig: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: region) { newRegion in
|
||||
.onChange(of: region) { _, newRegion in
|
||||
if newRegion != node?.loRaConfig?.regionCode ?? -1 { hasChanges = true }
|
||||
}
|
||||
.onChange(of: usePreset) {
|
||||
if $0 != node?.loRaConfig?.usePreset { hasChanges = true }
|
||||
.onChange(of: usePreset) { _, newPreset in
|
||||
if newPreset != node?.loRaConfig?.usePreset { hasChanges = true }
|
||||
}
|
||||
.onChange(of: modemPreset) { newModemPreset in
|
||||
.onChange(of: modemPreset) { _, newModemPreset in
|
||||
if newModemPreset != node?.loRaConfig?.modemPreset ?? -1 { hasChanges = true }
|
||||
}
|
||||
.onChange(of: hopLimit) { newHopLimit in
|
||||
.onChange(of: hopLimit) { _, newHopLimit in
|
||||
if newHopLimit != node?.loRaConfig?.hopLimit ?? -1 { hasChanges = true }
|
||||
}
|
||||
.onChange(of: channelNum) { newChannelNum in
|
||||
.onChange(of: channelNum) { _, newChannelNum in
|
||||
if newChannelNum != node?.loRaConfig?.channelNum ?? -1 { hasChanges = true }
|
||||
}
|
||||
.onChange(of: bandwidth) { newBandwidth in
|
||||
.onChange(of: bandwidth) { _, newBandwidth in
|
||||
if newBandwidth != node?.loRaConfig?.bandwidth ?? -1 { hasChanges = true }
|
||||
}
|
||||
.onChange(of: codingRate) { newCodingRate in
|
||||
.onChange(of: codingRate) { _, newCodingRate in
|
||||
if newCodingRate != node?.loRaConfig?.codingRate ?? -1 { hasChanges = true }
|
||||
}
|
||||
.onChange(of: spreadFactor) { newSpreadFactor in
|
||||
.onChange(of: spreadFactor) { _, newSpreadFactor in
|
||||
if newSpreadFactor != node?.loRaConfig?.spreadFactor ?? -1 { hasChanges = true }
|
||||
}
|
||||
.onChange(of: rxBoostedGain) {
|
||||
if $0 != node?.loRaConfig?.sx126xRxBoostedGain { hasChanges = true }
|
||||
.onChange(of: rxBoostedGain) { _, newRxBoostedGain in
|
||||
if newRxBoostedGain != node?.loRaConfig?.sx126xRxBoostedGain { hasChanges = true }
|
||||
}
|
||||
.onChange(of: overrideFrequency) { newOverrideFrequency in
|
||||
.onChange(of: overrideFrequency) { _, newOverrideFrequency in
|
||||
if newOverrideFrequency != node?.loRaConfig?.overrideFrequency { hasChanges = true }
|
||||
}
|
||||
.onChange(of: txPower) { newTxPower in
|
||||
.onChange(of: txPower) { _, newTxPower in
|
||||
if newTxPower != node?.loRaConfig?.txPower ?? -1 { hasChanges = true }
|
||||
}
|
||||
.onChange(of: txEnabled) {
|
||||
if $0 != node?.loRaConfig?.txEnabled { hasChanges = true }
|
||||
.onChange(of: txEnabled) { _, newTxEnabled in
|
||||
if newTxEnabled != node?.loRaConfig?.txEnabled { hasChanges = true }
|
||||
}
|
||||
.onChange(of: ignoreMqtt) {
|
||||
if $0 != node?.loRaConfig?.ignoreMqtt { hasChanges = true }
|
||||
.onChange(of: ignoreMqtt) { _, newIgnoreMqtt in
|
||||
if newIgnoreMqtt != node?.loRaConfig?.ignoreMqtt { hasChanges = true }
|
||||
}
|
||||
.onChange(of: okToMqtt) {
|
||||
if $0 != node?.loRaConfig?.okToMqtt { hasChanges = true }
|
||||
.onChange(of: okToMqtt) { _, newOkToMqtt in
|
||||
if newOkToMqtt != node?.loRaConfig?.okToMqtt { hasChanges = true }
|
||||
}
|
||||
}
|
||||
func setLoRaValues() {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import MeshtasticProtobufs
|
|||
import SwiftUI
|
||||
import OSLog
|
||||
|
||||
@available(iOS 17.0, macOS 14.0, *)
|
||||
struct AmbientLightingConfig: View {
|
||||
@Environment(\.self) var environment
|
||||
@Environment(\.managedObjectContext) var context
|
||||
|
|
@ -107,20 +106,14 @@ struct AmbientLightingConfig: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: ledState) {
|
||||
if let val = node?.ambientLightingConfig?.ledState {
|
||||
hasChanges = $0 != val
|
||||
}
|
||||
.onChange(of: ledState) { _, newLedState in
|
||||
if newLedState != node?.ambientLightingConfig?.ledState { hasChanges = true }
|
||||
}
|
||||
.onChange(of: current) {
|
||||
if let val = node?.ambientLightingConfig?.current {
|
||||
hasChanges = $0 != val
|
||||
}
|
||||
.onChange(of: current) { _, newCurrent in
|
||||
if newCurrent != node?.ambientLightingConfig?.current ?? 10 { hasChanges = true }
|
||||
}
|
||||
.onChange(of: color) { c in
|
||||
if color != c {
|
||||
hasChanges = true
|
||||
}
|
||||
.onChange(of: color) { oldColor, newColor in
|
||||
if oldColor != newColor { hasChanges = true }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,8 +71,7 @@ struct CannedMessagesConfig: View {
|
|||
.foregroundColor(.gray)
|
||||
.autocapitalization(.none)
|
||||
.disableAutocorrection(true)
|
||||
.onChange(of: messages, perform: { _ in
|
||||
|
||||
.onChange(of: messages) {
|
||||
var totalBytes = messages.utf8.count
|
||||
// Only mess with the value if it is too big
|
||||
while totalBytes > 198 {
|
||||
|
|
@ -80,7 +79,7 @@ struct CannedMessagesConfig: View {
|
|||
totalBytes = messages.utf8.count
|
||||
}
|
||||
hasMessagesChanges = true
|
||||
})
|
||||
}
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
.keyboardType(.default)
|
||||
|
|
@ -255,7 +254,7 @@ struct CannedMessagesConfig: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: configPreset) { newPreset in
|
||||
.onChange(of: configPreset) { _, newPreset in
|
||||
|
||||
if newPreset == 1 {
|
||||
|
||||
|
|
@ -284,55 +283,35 @@ struct CannedMessagesConfig: View {
|
|||
|
||||
hasChanges = true
|
||||
}
|
||||
.onChange(of: enabled) {
|
||||
if let val = node?.cannedMessageConfig?.enabled {
|
||||
hasChanges = $0 != val
|
||||
}
|
||||
.onChange(of: enabled) { _, newEnabled in
|
||||
if newEnabled != node?.cannedMessageConfig?.enabled { hasChanges = true }
|
||||
}
|
||||
.onChange(of: sendBell) {
|
||||
if let val = node?.cannedMessageConfig?.sendBell {
|
||||
hasChanges = $0 != val
|
||||
}
|
||||
.onChange(of: sendBell) { _, newSendBell in
|
||||
if newSendBell != node?.cannedMessageConfig?.sendBell { hasChanges = true }
|
||||
}
|
||||
.onChange(of: rotary1Enabled) {
|
||||
if let val = node?.cannedMessageConfig?.rotary1Enabled {
|
||||
hasChanges = $0 != val
|
||||
}
|
||||
.onChange(of: rotary1Enabled) { _, newRotary1Enabled in
|
||||
if newRotary1Enabled != node?.cannedMessageConfig?.rotary1Enabled { hasChanges = true }
|
||||
}
|
||||
.onChange(of: updown1Enabled) {
|
||||
if let val = node?.cannedMessageConfig?.updown1Enabled {
|
||||
hasChanges = $0 != val
|
||||
}
|
||||
.onChange(of: updown1Enabled) { _, newUpdown1Enabled in
|
||||
if newUpdown1Enabled != node?.cannedMessageConfig?.updown1Enabled { hasChanges = true }
|
||||
}
|
||||
.onChange(of: inputbrokerPinA) { newPinA in
|
||||
if node != nil && node!.cannedMessageConfig != nil {
|
||||
if newPinA != node!.cannedMessageConfig!.inputbrokerPinA { hasChanges = true }
|
||||
}
|
||||
.onChange(of: inputbrokerPinA) { _, newPinA in
|
||||
if newPinA != node?.cannedMessageConfig?.inputbrokerPinA ?? -1 { hasChanges = true }
|
||||
}
|
||||
.onChange(of: inputbrokerPinB) { newPinB in
|
||||
if node != nil && node!.cannedMessageConfig != nil {
|
||||
if newPinB != node!.cannedMessageConfig!.inputbrokerPinB { hasChanges = true }
|
||||
}
|
||||
.onChange(of: inputbrokerPinB) { _, newPinB in
|
||||
if newPinB != node?.cannedMessageConfig?.inputbrokerPinB ?? -1 { hasChanges = true }
|
||||
}
|
||||
.onChange(of: inputbrokerPinPress) { newPinPress in
|
||||
if node != nil && node!.cannedMessageConfig != nil {
|
||||
if newPinPress != node!.cannedMessageConfig!.inputbrokerPinPress { hasChanges = true }
|
||||
}
|
||||
.onChange(of: inputbrokerPinPress) { _, newPinPress in
|
||||
if newPinPress != node?.cannedMessageConfig?.inputbrokerPinPress ?? -1 { hasChanges = true }
|
||||
}
|
||||
.onChange(of: inputbrokerEventCw) { newKeyA in
|
||||
if node != nil && node!.cannedMessageConfig != nil {
|
||||
if newKeyA != node!.cannedMessageConfig!.inputbrokerEventCw { hasChanges = true }
|
||||
}
|
||||
.onChange(of: inputbrokerEventCw) { _, newKeyA in
|
||||
if newKeyA != node?.cannedMessageConfig?.inputbrokerEventCw ?? -1 { hasChanges = true }
|
||||
}
|
||||
.onChange(of: inputbrokerEventCcw) { newKeyB in
|
||||
if node != nil && node!.cannedMessageConfig != nil {
|
||||
if newKeyB != node!.cannedMessageConfig!.inputbrokerEventCcw { hasChanges = true }
|
||||
}
|
||||
.onChange(of: inputbrokerEventCcw) { _, newKeyB in
|
||||
if newKeyB != node?.cannedMessageConfig?.inputbrokerEventCcw ?? -1 { hasChanges = true }
|
||||
}
|
||||
.onChange(of: inputbrokerEventPress) { newKeyPress in
|
||||
if node != nil && node!.cannedMessageConfig != nil {
|
||||
if newKeyPress != node!.cannedMessageConfig!.inputbrokerEventPress { hasChanges = true }
|
||||
}
|
||||
.onChange(of: inputbrokerEventPress) { _, newKeyPress in
|
||||
if newKeyPress != node?.cannedMessageConfig?.inputbrokerEventPress ?? -1 { hasChanges = true }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -91,14 +91,14 @@ struct DetectionSensorConfig: View {
|
|||
.foregroundColor(.gray)
|
||||
.autocapitalization(.none)
|
||||
.disableAutocorrection(true)
|
||||
.onChange(of: name, perform: { _ in
|
||||
.onChange(of: name) {
|
||||
var totalBytes = name.utf8.count
|
||||
// Only mess with the value if it is too big
|
||||
while totalBytes > 20 {
|
||||
name = String(name.dropLast())
|
||||
totalBytes = name.utf8.count
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
.listRowSeparator(.hidden)
|
||||
Text("Friendly name used to format message sent to mesh. Example: A name \"Motion\" would result in a message \"Motion detected\"")
|
||||
|
|
@ -210,47 +210,31 @@ struct DetectionSensorConfig: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: enabled) {
|
||||
if let val = node?.detectionSensorConfig?.enabled {
|
||||
hasChanges = $0 != val
|
||||
}
|
||||
.onChange(of: enabled) { _, newEnabled in
|
||||
if newEnabled != node?.detectionSensorConfig?.enabled { hasChanges = true }
|
||||
}
|
||||
.onChange(of: sendBell) {
|
||||
if let val = node?.detectionSensorConfig?.sendBell {
|
||||
hasChanges = $0 != val
|
||||
}
|
||||
.onChange(of: sendBell) { _, newSendBell in
|
||||
if newSendBell != node?.detectionSensorConfig?.sendBell { hasChanges = true }
|
||||
}
|
||||
.onChange(of: detectionTriggeredHigh) { newDetectionTriggeredHigh in
|
||||
if node != nil && node?.detectionSensorConfig != nil {
|
||||
if newDetectionTriggeredHigh != node!.detectionSensorConfig!.detectionTriggeredHigh { hasChanges = true }
|
||||
}
|
||||
.onChange(of: detectionTriggeredHigh) { _, newDetectionTriggeredHigh in
|
||||
if newDetectionTriggeredHigh != node?.detectionSensorConfig?.detectionTriggeredHigh { hasChanges = true }
|
||||
}
|
||||
.onChange(of: usePullup) {
|
||||
if let val = node?.detectionSensorConfig?.usePullup {
|
||||
hasChanges = $0 != val
|
||||
}
|
||||
.onChange(of: usePullup) { _, newUsePullup in
|
||||
if newUsePullup != node?.detectionSensorConfig?.usePullup { hasChanges = true }
|
||||
}
|
||||
.onChange(of: name) { newName in
|
||||
if node != nil && node?.detectionSensorConfig != nil {
|
||||
if newName != node!.detectionSensorConfig!.name { hasChanges = true }
|
||||
}
|
||||
.onChange(of: name) { _, newName in
|
||||
if newName != node?.detectionSensorConfig?.name ?? "" { hasChanges = true }
|
||||
}
|
||||
.onChange(of: monitorPin) { newMonitorPin in
|
||||
if node != nil && node?.detectionSensorConfig != nil {
|
||||
if newMonitorPin != node!.detectionSensorConfig!.monitorPin { hasChanges = true }
|
||||
}
|
||||
.onChange(of: monitorPin) { _, newMonitorPin in
|
||||
if newMonitorPin != node?.detectionSensorConfig?.monitorPin ?? 0 { hasChanges = true }
|
||||
}
|
||||
.onChange(of: minimumBroadcastSecs) { newMinimumBroadcastSecs in
|
||||
if node != nil && node?.detectionSensorConfig != nil {
|
||||
if newMinimumBroadcastSecs != node!.detectionSensorConfig!.minimumBroadcastSecs { hasChanges = true }
|
||||
}
|
||||
.onChange(of: minimumBroadcastSecs) { _, newMinimumBroadcastSecs in
|
||||
if newMinimumBroadcastSecs != node?.detectionSensorConfig?.minimumBroadcastSecs ?? 0 { hasChanges = true }
|
||||
}
|
||||
.onChange(of: stateBroadcastSecs) { newStateBroadcastSecs in
|
||||
if node != nil && node?.detectionSensorConfig != nil {
|
||||
if newStateBroadcastSecs != node!.detectionSensorConfig!.stateBroadcastSecs { hasChanges = true }
|
||||
}
|
||||
.onChange(of: stateBroadcastSecs) { _, newStateBroadcastSecs in
|
||||
if newStateBroadcastSecs != node?.detectionSensorConfig?.stateBroadcastSecs ?? 0 { hasChanges = true }
|
||||
}
|
||||
.onChange(of: detectionNotificationsEnabled) { newDetectionNotificationsEnabled in
|
||||
.onChange(of: detectionNotificationsEnabled) { _, newDetectionNotificationsEnabled in
|
||||
UserDefaults.enableDetectionNotifications = newDetectionNotificationsEnabled
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -220,80 +220,50 @@ struct ExternalNotificationConfig: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: enabled) {
|
||||
if let val = node?.externalNotificationConfig?.enabled {
|
||||
hasChanges = $0 != val
|
||||
}
|
||||
.onChange(of: enabled) { _, newEnabled in
|
||||
if newEnabled != node?.externalNotificationConfig?.enabled { hasChanges = true }
|
||||
}
|
||||
.onChange(of: alertBell) {
|
||||
if let val = node?.externalNotificationConfig?.alertBell {
|
||||
hasChanges = $0 != val
|
||||
}
|
||||
.onChange(of: alertBell) { _, newAlertBell in
|
||||
if newAlertBell != node?.externalNotificationConfig?.alertBell { hasChanges = true }
|
||||
}
|
||||
.onChange(of: alertBellBuzzer) {
|
||||
if let val = node?.externalNotificationConfig?.alertBellBuzzer {
|
||||
hasChanges = $0 != val
|
||||
}
|
||||
.onChange(of: alertBellBuzzer) { _, newAlertBellBuzzer in
|
||||
if newAlertBellBuzzer != node?.externalNotificationConfig?.alertBellBuzzer { hasChanges = true }
|
||||
}
|
||||
.onChange(of: alertBellVibra) {
|
||||
if let val = node?.externalNotificationConfig?.alertBellVibra {
|
||||
hasChanges = $0 != val
|
||||
}
|
||||
.onChange(of: alertBellVibra) { _, newAlertBellVibra in
|
||||
if newAlertBellVibra != node?.externalNotificationConfig?.alertBellVibra { hasChanges = true }
|
||||
}
|
||||
.onChange(of: alertMessage) {
|
||||
if let val = node?.externalNotificationConfig?.alertMessage {
|
||||
hasChanges = $0 != val
|
||||
}
|
||||
.onChange(of: alertMessage) { _, newAlertMessage in
|
||||
if newAlertMessage != node?.externalNotificationConfig?.alertMessage { hasChanges = true }
|
||||
}
|
||||
.onChange(of: alertMessageBuzzer) {
|
||||
if let val = node?.externalNotificationConfig?.alertMessageBuzzer {
|
||||
hasChanges = $0 != val
|
||||
}
|
||||
.onChange(of: alertMessageBuzzer) { _, newAlertMessageBuzzer in
|
||||
if newAlertMessageBuzzer != node?.externalNotificationConfig?.alertMessageBuzzer { hasChanges = true }
|
||||
}
|
||||
.onChange(of: alertMessageVibra) {
|
||||
if let val = node?.externalNotificationConfig?.alertMessageVibra {
|
||||
hasChanges = $0 != val
|
||||
}
|
||||
.onChange(of: alertMessageVibra) { _, newAlertMessageVibra in
|
||||
if newAlertMessageVibra != node?.externalNotificationConfig?.alertMessageVibra { hasChanges = true }
|
||||
}
|
||||
.onChange(of: active) {
|
||||
if let val = node?.externalNotificationConfig?.active {
|
||||
hasChanges = $0 != val
|
||||
}
|
||||
.onChange(of: active) { _, newActive in
|
||||
if newActive != node?.externalNotificationConfig?.active { hasChanges = true }
|
||||
}
|
||||
.onChange(of: output) { newOutput in
|
||||
if node != nil && node!.externalNotificationConfig != nil {
|
||||
if newOutput != node!.externalNotificationConfig!.output { hasChanges = true }
|
||||
}
|
||||
.onChange(of: output) { _, newOutput in
|
||||
if newOutput != node?.externalNotificationConfig?.output ?? -1 { hasChanges = true }
|
||||
}
|
||||
.onChange(of: output) { newOutputBuzzer in
|
||||
if node != nil && node!.externalNotificationConfig != nil {
|
||||
if newOutputBuzzer != node!.externalNotificationConfig!.outputBuzzer { hasChanges = true }
|
||||
}
|
||||
.onChange(of: output) { _, newOutputBuzzer in
|
||||
if newOutputBuzzer != node?.externalNotificationConfig?.outputBuzzer ?? -1 { hasChanges = true }
|
||||
}
|
||||
.onChange(of: output) { newOutputVibra in
|
||||
if node != nil && node!.externalNotificationConfig != nil {
|
||||
if newOutputVibra != node!.externalNotificationConfig!.outputVibra { hasChanges = true }
|
||||
}
|
||||
.onChange(of: output) { _, newOutputVibra in
|
||||
if newOutputVibra != node?.externalNotificationConfig?.outputVibra ?? -1 { hasChanges = true }
|
||||
}
|
||||
.onChange(of: outputMilliseconds) { newOutputMs in
|
||||
if node != nil && node!.externalNotificationConfig != nil {
|
||||
if newOutputMs != node!.externalNotificationConfig!.outputMilliseconds { hasChanges = true }
|
||||
}
|
||||
.onChange(of: outputMilliseconds) { _, newOutputMs in
|
||||
if newOutputMs != node?.externalNotificationConfig?.outputMilliseconds ?? -1 { hasChanges = true }
|
||||
}
|
||||
.onChange(of: usePWM) {
|
||||
if let val = node?.externalNotificationConfig?.usePWM {
|
||||
hasChanges = $0 != val
|
||||
}
|
||||
.onChange(of: usePWM) { _, newPWM in
|
||||
if newPWM != node?.externalNotificationConfig?.usePWM { hasChanges = true }
|
||||
}
|
||||
.onChange(of: nagTimeout) { newNagTimeout in
|
||||
if node != nil && node!.externalNotificationConfig != nil {
|
||||
if newNagTimeout != node!.externalNotificationConfig!.nagTimeout { hasChanges = true }
|
||||
}
|
||||
.onChange(of: nagTimeout) { _, newNagTimeout in
|
||||
if newNagTimeout != node?.externalNotificationConfig?.nagTimeout ?? -1 { hasChanges = true }
|
||||
}
|
||||
.onChange(of: useI2SAsBuzzer) {
|
||||
if let val = node?.externalNotificationConfig?.useI2SAsBuzzer {
|
||||
hasChanges = $0 != val
|
||||
}
|
||||
.onChange(of: useI2SAsBuzzer) { _, newUseI2SAsBuzzer in
|
||||
if newUseI2SAsBuzzer != node?.externalNotificationConfig?.useI2SAsBuzzer { hasChanges = true }
|
||||
}
|
||||
}
|
||||
func setExternalNotificationValues() {
|
||||
|
|
|
|||
|
|
@ -123,14 +123,14 @@ struct MQTTConfig: View {
|
|||
Label("Root Topic", systemImage: "tree")
|
||||
TextField("Root Topic", text: $root)
|
||||
.foregroundColor(.gray)
|
||||
.onChange(of: root, perform: { _ in
|
||||
.onChange(of: root) {
|
||||
var totalBytes = root.utf8.count
|
||||
// Only mess with the value if it is too big
|
||||
while totalBytes > 30 {
|
||||
root = String(root.dropLast())
|
||||
totalBytes = root.utf8.count
|
||||
}
|
||||
})
|
||||
}
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
.keyboardType(.asciiCapable)
|
||||
|
|
@ -162,7 +162,7 @@ struct MQTTConfig: View {
|
|||
.foregroundColor(.gray)
|
||||
.autocapitalization(.none)
|
||||
.disableAutocorrection(true)
|
||||
.onChange(of: address, perform: { _ in
|
||||
.onChange(of: address) {
|
||||
var totalBytes = address.utf8.count
|
||||
// Only mess with the value if it is too big
|
||||
while totalBytes > 62 {
|
||||
|
|
@ -170,7 +170,7 @@ struct MQTTConfig: View {
|
|||
totalBytes = address.utf8.count
|
||||
}
|
||||
hasChanges = true
|
||||
})
|
||||
}
|
||||
.keyboardType(.default)
|
||||
}
|
||||
.autocorrectionDisabled()
|
||||
|
|
@ -181,7 +181,7 @@ struct MQTTConfig: View {
|
|||
.foregroundColor(.gray)
|
||||
.autocapitalization(.none)
|
||||
.disableAutocorrection(true)
|
||||
.onChange(of: username, perform: { _ in
|
||||
.onChange(of: username) {
|
||||
var totalBytes = username.utf8.count
|
||||
// Only mess with the value if it is too big
|
||||
while totalBytes > 62 {
|
||||
|
|
@ -189,7 +189,7 @@ struct MQTTConfig: View {
|
|||
totalBytes = username.utf8.count
|
||||
}
|
||||
hasChanges = true
|
||||
})
|
||||
}
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
.keyboardType(.default)
|
||||
|
|
@ -200,7 +200,7 @@ struct MQTTConfig: View {
|
|||
.foregroundColor(.gray)
|
||||
.autocapitalization(.none)
|
||||
.disableAutocorrection(true)
|
||||
.onChange(of: password, perform: { _ in
|
||||
.onChange(of: password) {
|
||||
var totalBytes = password.utf8.count
|
||||
// Only mess with the value if it is too big
|
||||
while totalBytes > 62 {
|
||||
|
|
@ -208,7 +208,7 @@ struct MQTTConfig: View {
|
|||
totalBytes = password.utf8.count
|
||||
}
|
||||
hasChanges = true
|
||||
})
|
||||
}
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
.keyboardType(.default)
|
||||
|
|
@ -262,55 +262,47 @@ struct MQTTConfig: View {
|
|||
)
|
||||
}
|
||||
)
|
||||
.onChange(of: enabled) {
|
||||
if $0 != node?.mqttConfig?.enabled { hasChanges = true }
|
||||
.onChange(of: enabled) { _, newEnabled in
|
||||
if newEnabled != node?.mqttConfig?.enabled { hasChanges = true }
|
||||
}
|
||||
.onChange(of: proxyToClientEnabled) { newProxyToClientEnabled in
|
||||
.onChange(of: proxyToClientEnabled) { _, newProxyToClientEnabled in
|
||||
if newProxyToClientEnabled {
|
||||
jsonEnabled = false
|
||||
}
|
||||
if newProxyToClientEnabled != node?.mqttConfig?.proxyToClientEnabled { hasChanges = true }
|
||||
}
|
||||
.onChange(of: address) { newAddress in
|
||||
if node != nil && node?.mqttConfig != nil {
|
||||
if newAddress != node!.mqttConfig!.address { hasChanges = true }
|
||||
}
|
||||
.onChange(of: address) { _, newAddress in
|
||||
if newAddress != node?.mqttConfig?.address ?? "" { hasChanges = true }
|
||||
}
|
||||
.onChange(of: username) { newUsername in
|
||||
if node != nil && node?.mqttConfig != nil {
|
||||
if newUsername != node!.mqttConfig!.username { hasChanges = true }
|
||||
}
|
||||
if newUsername != node?.mqttConfig?.username ?? "" { hasChanges = true }
|
||||
}
|
||||
.onChange(of: password) { newPassword in
|
||||
if node != nil && node?.mqttConfig != nil {
|
||||
if newPassword != node!.mqttConfig!.password { hasChanges = true }
|
||||
}
|
||||
if newPassword != node?.mqttConfig?.password ?? "" { hasChanges = true }
|
||||
}
|
||||
.onChange(of: root) { newRoot in
|
||||
if node != nil && node?.mqttConfig != nil {
|
||||
if newRoot != node!.mqttConfig!.root { hasChanges = true }
|
||||
}
|
||||
if newRoot != node?.mqttConfig?.root ?? "" { hasChanges = true }
|
||||
}
|
||||
.onChange(of: selectedTopic) { newSelectedTopic in
|
||||
.onChange(of: selectedTopic) { _, newSelectedTopic in
|
||||
root = newSelectedTopic
|
||||
}
|
||||
.onChange(of: encryptionEnabled) {
|
||||
if $0 != node?.mqttConfig?.encryptionEnabled { hasChanges = true }
|
||||
.onChange(of: encryptionEnabled) { _, newEncryptionEnabled in
|
||||
if newEncryptionEnabled != node?.mqttConfig?.encryptionEnabled { hasChanges = true }
|
||||
}
|
||||
.onChange(of: jsonEnabled) { newJsonEnabled in
|
||||
.onChange(of: jsonEnabled) { _, newJsonEnabled in
|
||||
if newJsonEnabled {
|
||||
proxyToClientEnabled = false
|
||||
}
|
||||
if newJsonEnabled != node?.mqttConfig?.jsonEnabled { hasChanges = true }
|
||||
}
|
||||
.onChange(of: tlsEnabled) { newTlsEnabled in
|
||||
.onChange(of: tlsEnabled) { _, newTlsEnabled in
|
||||
if address.lowercased() == "mqtt.meshtastic.org" {
|
||||
tlsEnabled = false
|
||||
} else {
|
||||
if newTlsEnabled != node?.mqttConfig?.tlsEnabled { hasChanges = true }
|
||||
}
|
||||
}
|
||||
.onChange(of: mqttConnected) { newMqttConnected in
|
||||
.onChange(of: mqttConnected) { _, newMqttConnected in
|
||||
if newMqttConnected == false {
|
||||
if bleManager.mqttProxyConnected {
|
||||
bleManager.mqttManager.disconnect()
|
||||
|
|
@ -321,13 +313,11 @@ struct MQTTConfig: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: mapReportingEnabled) {
|
||||
if $0 != node?.mqttConfig?.mapReportingEnabled { hasChanges = true }
|
||||
.onChange(of: mapReportingEnabled) { _, newMapReportingEnabled in
|
||||
if newMapReportingEnabled != node?.mqttConfig?.mapReportingEnabled { hasChanges = true }
|
||||
}
|
||||
.onChange(of: mapPublishIntervalSecs) { newMapPublishIntervalSecs in
|
||||
if node != nil && node?.mqttConfig != nil {
|
||||
if newMapPublishIntervalSecs != node!.mqttConfig!.mapPublishIntervalSecs { hasChanges = true }
|
||||
}
|
||||
.onChange(of: mapPublishIntervalSecs) { _, newMapPublishIntervalSecs in
|
||||
if newMapPublishIntervalSecs != node?.mqttConfig?.mapPublishIntervalSecs ?? -1 { hasChanges = true }
|
||||
}
|
||||
.onFirstAppear {
|
||||
// Need to request a MqttModuleConfig from the remote node before allowing changes
|
||||
|
|
@ -353,52 +343,50 @@ struct MQTTConfig: View {
|
|||
}
|
||||
func setMqttValues() {
|
||||
|
||||
if #available(iOS 17.0, macOS 14.0, *) {
|
||||
nearbyTopics = []
|
||||
let geocoder = CLGeocoder()
|
||||
if LocationsHandler.shared.locationsArray.count > 0 {
|
||||
let region = RegionCodes(rawValue: Int(node?.loRaConfig?.regionCode ?? 0))?.topic
|
||||
defaultTopic = "msh/" + (region ?? "UNSET")
|
||||
geocoder.reverseGeocodeLocation(LocationsHandler.shared.locationsArray.first!, completionHandler: {(placemarks, error) in
|
||||
if let error {
|
||||
Logger.services.error("Failed to reverse geocode location: \(error.localizedDescription)")
|
||||
return
|
||||
}
|
||||
|
||||
nearbyTopics = []
|
||||
let geocoder = CLGeocoder()
|
||||
if LocationsHandler.shared.locationsArray.count > 0 {
|
||||
let region = RegionCodes(rawValue: Int(node?.loRaConfig?.regionCode ?? 0))?.topic
|
||||
defaultTopic = "msh/" + (region ?? "UNSET")
|
||||
geocoder.reverseGeocodeLocation(LocationsHandler.shared.locationsArray.first!, completionHandler: {(placemarks, error) in
|
||||
if let error {
|
||||
Logger.services.error("Failed to reverse geocode location: \(error.localizedDescription)")
|
||||
return
|
||||
if let placemarks = placemarks, let placemark = placemarks.first {
|
||||
let cc = locale.region?.identifier ?? "UNK"
|
||||
/// Country Topic unless you are US
|
||||
if placemark.isoCountryCode ?? "unknown" != cc {
|
||||
let countryTopic = defaultTopic + "/" + (placemark.isoCountryCode ?? "")
|
||||
if !countryTopic.isEmpty {
|
||||
nearbyTopics.append(countryTopic)
|
||||
}
|
||||
}
|
||||
|
||||
if let placemarks = placemarks, let placemark = placemarks.first {
|
||||
let cc = locale.region?.identifier ?? "UNK"
|
||||
/// Country Topic unless you are US
|
||||
if placemark.isoCountryCode ?? "unknown" != cc {
|
||||
let countryTopic = defaultTopic + "/" + (placemark.isoCountryCode ?? "")
|
||||
if !countryTopic.isEmpty {
|
||||
nearbyTopics.append(countryTopic)
|
||||
}
|
||||
}
|
||||
let stateTopic = defaultTopic + "/" + (placemark.administrativeArea ?? "")
|
||||
if !stateTopic.isEmpty {
|
||||
nearbyTopics.append(stateTopic)
|
||||
}
|
||||
let countyTopic = defaultTopic + "/" + (placemark.administrativeArea ?? "") + "/" + (placemark.subAdministrativeArea?.lowercased().replacingOccurrences(of: " ", with: "") ?? "")
|
||||
if !countyTopic.isEmpty {
|
||||
nearbyTopics.append(countyTopic)
|
||||
}
|
||||
let cityTopic = defaultTopic + "/" + (placemark.administrativeArea ?? "") + "/" + (placemark.locality?.lowercased().replacingOccurrences(of: " ", with: "") ?? "")
|
||||
if !cityTopic.isEmpty {
|
||||
nearbyTopics.append(cityTopic)
|
||||
}
|
||||
let neightborhoodTopic = defaultTopic + "/" + (placemark.administrativeArea ?? "") + "/" + (placemark.subLocality?.lowercased()
|
||||
.replacingOccurrences(of: " ", with: "")
|
||||
.replacingOccurrences(of: "'", with: "") ?? "")
|
||||
if !neightborhoodTopic.isEmpty {
|
||||
nearbyTopics.append(neightborhoodTopic)
|
||||
}
|
||||
} else {
|
||||
Logger.services.debug("No Location")
|
||||
let stateTopic = defaultTopic + "/" + (placemark.administrativeArea ?? "")
|
||||
if !stateTopic.isEmpty {
|
||||
nearbyTopics.append(stateTopic)
|
||||
}
|
||||
})
|
||||
}
|
||||
let countyTopic = defaultTopic + "/" + (placemark.administrativeArea ?? "") + "/" + (placemark.subAdministrativeArea?.lowercased().replacingOccurrences(of: " ", with: "") ?? "")
|
||||
if !countyTopic.isEmpty {
|
||||
nearbyTopics.append(countyTopic)
|
||||
}
|
||||
let cityTopic = defaultTopic + "/" + (placemark.administrativeArea ?? "") + "/" + (placemark.locality?.lowercased().replacingOccurrences(of: " ", with: "") ?? "")
|
||||
if !cityTopic.isEmpty {
|
||||
nearbyTopics.append(cityTopic)
|
||||
}
|
||||
let neightborhoodTopic = defaultTopic + "/" + (placemark.administrativeArea ?? "") + "/" + (placemark.subLocality?.lowercased()
|
||||
.replacingOccurrences(of: " ", with: "")
|
||||
.replacingOccurrences(of: "'", with: "") ?? "")
|
||||
if !neightborhoodTopic.isEmpty {
|
||||
nearbyTopics.append(neightborhoodTopic)
|
||||
}
|
||||
} else {
|
||||
Logger.services.debug("No Location")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
self.enabled = node?.mqttConfig?.enabled ?? false
|
||||
self.proxyToClientEnabled = node?.mqttConfig?.proxyToClientEnabled ?? false
|
||||
self.address = node?.mqttConfig?.address ?? ""
|
||||
|
|
|
|||
|
|
@ -79,11 +79,11 @@ struct PaxCounterConfig: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: enabled) {
|
||||
if $0 != node?.paxCounterConfig?.enabled { hasChanges = true }
|
||||
.onChange(of: enabled) { oldEnabled, newEnabled in
|
||||
if oldEnabled != newEnabled && newEnabled != node?.paxCounterConfig?.enabled { hasChanges = true }
|
||||
}
|
||||
.onChange(of: paxcounterUpdateInterval) {
|
||||
if $0 != node?.paxCounterConfig?.updateInterval ?? -1 { hasChanges = true }
|
||||
.onChange(of: paxcounterUpdateInterval) { oldPaxcounterUpdateInterval, newPaxcounterUpdateInterval in
|
||||
if oldPaxcounterUpdateInterval != newPaxcounterUpdateInterval && newPaxcounterUpdateInterval != node?.paxCounterConfig?.updateInterval ?? -1 { hasChanges = true }
|
||||
}
|
||||
|
||||
SaveConfigButton(node: node, hasChanges: $hasChanges) {
|
||||
|
|
|
|||
|
|
@ -102,14 +102,14 @@ struct RangeTestConfig: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: enabled) {
|
||||
if $0 != node?.rangeTestConfig?.enabled { hasChanges = true }
|
||||
.onChange(of: enabled) { _, newEnabled in
|
||||
if newEnabled != node?.rangeTestConfig?.enabled { hasChanges = true }
|
||||
}
|
||||
.onChange(of: save) {
|
||||
if $0 != node?.rangeTestConfig?.save { hasChanges = true }
|
||||
.onChange(of: save) { _, newSave in
|
||||
if newSave != node?.rangeTestConfig?.save { hasChanges = true }
|
||||
}
|
||||
.onChange(of: sender) {
|
||||
if $0 != node?.rangeTestConfig?.sender ?? -1 { hasChanges = true }
|
||||
.onChange(of: sender) { _, newSender in
|
||||
if newSender != node?.rangeTestConfig?.sender ?? -1 { hasChanges = true }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,14 +31,14 @@ struct RtttlConfig: View {
|
|||
.foregroundColor(.gray)
|
||||
.autocapitalization(.none)
|
||||
.disableAutocorrection(true)
|
||||
.onChange(of: ringtone, perform: { _ in
|
||||
.onChange(of: ringtone) {
|
||||
var totalBytes = ringtone.utf8.count
|
||||
// Only mess with the value if it is too big
|
||||
while totalBytes > 228 {
|
||||
ringtone = String(ringtone.dropLast())
|
||||
totalBytes = ringtone.utf8.count
|
||||
}
|
||||
})
|
||||
}
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
.keyboardType(.default)
|
||||
|
|
@ -93,7 +93,7 @@ struct RtttlConfig: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: ringtone) { newRingtone in
|
||||
.onChange(of: ringtone) { _, newRingtone in
|
||||
if node != nil && node!.rtttlConfig != nil {
|
||||
if newRingtone != node!.rtttlConfig!.ringtone { hasChanges = true }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -157,41 +157,29 @@ struct SerialConfig: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: enabled) {
|
||||
if $0 != node?.serialConfig?.enabled { hasChanges = true }
|
||||
.onChange(of: enabled) { oldEnabled, newEnabled in
|
||||
if oldEnabled != newEnabled && newEnabled != node?.serialConfig?.enabled ?? false { hasChanges = true }
|
||||
}
|
||||
.onChange(of: echo) {
|
||||
if $0 != node?.serialConfig?.echo { hasChanges = true }
|
||||
.onChange(of: echo) { oldEcho, newEcho in
|
||||
if oldEcho != newEcho && newEcho != node?.serialConfig?.echo ?? false { hasChanges = true }
|
||||
}
|
||||
.onChange(of: rxd) { newRxd in
|
||||
if node != nil && node!.serialConfig != nil {
|
||||
if newRxd != node!.serialConfig!.rxd { hasChanges = true }
|
||||
}
|
||||
.onChange(of: rxd) { oldRxd, newRxd in
|
||||
if oldRxd != newRxd && newRxd != node?.serialConfig?.rxd ?? -1 { hasChanges = true }
|
||||
}
|
||||
.onChange(of: txd) { newTxd in
|
||||
if node != nil && node!.serialConfig != nil {
|
||||
if newTxd != node!.serialConfig!.txd { hasChanges = true }
|
||||
}
|
||||
.onChange(of: txd) { oldTxd, newTxd in
|
||||
if oldTxd != newTxd && newTxd != node?.serialConfig?.txd ?? -1 { hasChanges = true }
|
||||
}
|
||||
.onChange(of: baudRate) { newBaud in
|
||||
if node != nil && node!.serialConfig != nil {
|
||||
if newBaud != node!.serialConfig!.baudRate { hasChanges = true }
|
||||
}
|
||||
.onChange(of: baudRate) { oldBaud, newBaud in
|
||||
if oldBaud != newBaud && newBaud != node?.serialConfig?.baudRate ?? -1 { hasChanges = true }
|
||||
}
|
||||
.onChange(of: timeout) { newTimeout in
|
||||
if node != nil && node!.serialConfig != nil {
|
||||
if newTimeout != node!.serialConfig!.timeout { hasChanges = true }
|
||||
}
|
||||
.onChange(of: timeout) { oldTimeout, newTimeout in
|
||||
if oldTimeout != newTimeout && newTimeout != node?.serialConfig?.timeout ?? -1 { hasChanges = true }
|
||||
}
|
||||
.onChange(of: overrideConsoleSerialPort) { newOverrideConsoleSerialPort in
|
||||
if node != nil && node!.serialConfig != nil {
|
||||
if newOverrideConsoleSerialPort != node!.serialConfig!.overrideConsoleSerialPort { hasChanges = true }
|
||||
}
|
||||
.onChange(of: overrideConsoleSerialPort) { oldOverrideConsoleSerialPort, newOverrideConsoleSerialPort in
|
||||
if oldOverrideConsoleSerialPort != newOverrideConsoleSerialPort && newOverrideConsoleSerialPort != node?.serialConfig?.overrideConsoleSerialPort ?? false { hasChanges = true }
|
||||
}
|
||||
.onChange(of: mode) { newMode in
|
||||
if node != nil && node!.serialConfig != nil {
|
||||
if newMode != node!.serialConfig!.mode { hasChanges = true }
|
||||
}
|
||||
.onChange(of: mode) { oldMode, newMode in
|
||||
if oldMode != newMode && newMode != node?.serialConfig?.mode ?? -1 { hasChanges = true }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -167,35 +167,23 @@ struct StoreForwardConfig: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: enabled) { newEnabled in
|
||||
if node != nil && node?.storeForwardConfig != nil {
|
||||
if newEnabled != node!.storeForwardConfig!.enabled { hasChanges = true }
|
||||
}
|
||||
.onChange(of: enabled) { oldEnabled, newEnabled in
|
||||
if oldEnabled != newEnabled && newEnabled != node!.storeForwardConfig!.enabled { hasChanges = true }
|
||||
}
|
||||
.onChange(of: isRouter) { newIsRouter in
|
||||
if node != nil && node?.storeForwardConfig != nil {
|
||||
if newIsRouter != node!.storeForwardConfig!.isRouter { hasChanges = true }
|
||||
}
|
||||
.onChange(of: isRouter) { oldIsRouter, newIsRouter in
|
||||
if oldIsRouter != newIsRouter && newIsRouter != node!.storeForwardConfig!.isRouter { hasChanges = true }
|
||||
}
|
||||
.onChange(of: heartbeat) { newHeartbeat in
|
||||
if node != nil && node?.storeForwardConfig != nil {
|
||||
if newHeartbeat != node!.storeForwardConfig!.heartbeat { hasChanges = true }
|
||||
}
|
||||
.onChange(of: heartbeat) { oldHeartbeat, newHeartbeat in
|
||||
if oldHeartbeat != newHeartbeat && newHeartbeat != node?.storeForwardConfig?.heartbeat ?? true { hasChanges = true }
|
||||
}
|
||||
.onChange(of: records) { newRecords in
|
||||
if node != nil && node?.storeForwardConfig != nil {
|
||||
if newRecords != node!.storeForwardConfig!.records { hasChanges = true }
|
||||
}
|
||||
.onChange(of: records) { oldRecords, newRecords in
|
||||
if oldRecords != newRecords && newRecords != node!.storeForwardConfig?.records ?? -1 { hasChanges = true }
|
||||
}
|
||||
.onChange(of: historyReturnMax) { newHistoryReturnMax in
|
||||
if node != nil && node?.storeForwardConfig != nil {
|
||||
if newHistoryReturnMax != node!.storeForwardConfig!.historyReturnMax { hasChanges = true }
|
||||
}
|
||||
.onChange(of: historyReturnMax) { oldHistoryReturnMax, newHistoryReturnMax in
|
||||
if oldHistoryReturnMax != newHistoryReturnMax && newHistoryReturnMax != node!.storeForwardConfig?.historyReturnMax ?? -1 { hasChanges = true }
|
||||
}
|
||||
.onChange(of: historyReturnWindow) { newHistoryReturnWindow in
|
||||
if node != nil && node?.storeForwardConfig != nil {
|
||||
if newHistoryReturnWindow != node!.storeForwardConfig!.historyReturnWindow { hasChanges = true }
|
||||
}
|
||||
.onChange(of: historyReturnWindow) { oldHistoryReturnWindow, newHistoryReturnWindow in
|
||||
if oldHistoryReturnWindow != newHistoryReturnWindow && newHistoryReturnWindow != node!.storeForwardConfig?.historyReturnWindow ?? -1 { hasChanges = true }
|
||||
}
|
||||
}
|
||||
func setStoreAndForwardValues() {
|
||||
|
|
|
|||
|
|
@ -155,45 +155,29 @@ struct TelemetryConfig: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: deviceUpdateInterval) { newDeviceInterval in
|
||||
if node != nil && node?.telemetryConfig != nil {
|
||||
if newDeviceInterval != node!.telemetryConfig!.deviceUpdateInterval { hasChanges = true }
|
||||
}
|
||||
.onChange(of: deviceUpdateInterval) { _, newDeviceInterval in
|
||||
if newDeviceInterval != node?.telemetryConfig?.deviceUpdateInterval ?? -1 { hasChanges = true }
|
||||
}
|
||||
.onChange(of: environmentUpdateInterval) { newEnvInterval in
|
||||
if node != nil && node?.telemetryConfig != nil {
|
||||
if newEnvInterval != node!.telemetryConfig!.environmentUpdateInterval { hasChanges = true }
|
||||
}
|
||||
.onChange(of: environmentUpdateInterval) { _, newEnvInterval in
|
||||
if newEnvInterval != node?.telemetryConfig?.environmentUpdateInterval ?? -1 { hasChanges = true }
|
||||
}
|
||||
.onChange(of: environmentMeasurementEnabled) { newEnvEnabled in
|
||||
if node != nil && node?.telemetryConfig != nil {
|
||||
if newEnvEnabled != node!.telemetryConfig!.environmentMeasurementEnabled { hasChanges = true }
|
||||
}
|
||||
.onChange(of: environmentMeasurementEnabled) { _, newEnvEnabled in
|
||||
if newEnvEnabled != node?.telemetryConfig?.environmentMeasurementEnabled { hasChanges = true }
|
||||
}
|
||||
.onChange(of: environmentScreenEnabled) { newEnvScreenEnabled in
|
||||
if node!.telemetryConfig != nil {
|
||||
if newEnvScreenEnabled != node!.telemetryConfig!.environmentScreenEnabled { hasChanges = true }
|
||||
}
|
||||
.onChange(of: environmentScreenEnabled) { _, newEnvScreenEnabled in
|
||||
if newEnvScreenEnabled != node?.telemetryConfig?.environmentScreenEnabled { hasChanges = true }
|
||||
}
|
||||
.onChange(of: environmentDisplayFahrenheit) { newEnvDisplayF in
|
||||
if node != nil && node?.telemetryConfig != nil {
|
||||
if newEnvDisplayF != node!.telemetryConfig!.environmentDisplayFahrenheit { hasChanges = true }
|
||||
}
|
||||
.onChange(of: environmentDisplayFahrenheit) { _, newEnvDisplayF in
|
||||
if newEnvDisplayF != node?.telemetryConfig?.environmentDisplayFahrenheit { hasChanges = true }
|
||||
}
|
||||
.onChange(of: powerMeasurementEnabled) { newPowerMeasurementEnabled in
|
||||
if node != nil && node?.telemetryConfig != nil {
|
||||
if newPowerMeasurementEnabled != node!.telemetryConfig!.powerMeasurementEnabled { hasChanges = true }
|
||||
}
|
||||
.onChange(of: powerMeasurementEnabled) { _, newPowerMeasurementEnabled in
|
||||
if newPowerMeasurementEnabled != node?.telemetryConfig?.powerMeasurementEnabled { hasChanges = true }
|
||||
}
|
||||
.onChange(of: powerUpdateInterval) { newPowerUpdateInterval in
|
||||
if node != nil && node?.telemetryConfig != nil {
|
||||
if newPowerUpdateInterval != node!.telemetryConfig!.powerUpdateInterval { hasChanges = true }
|
||||
}
|
||||
.onChange(of: powerUpdateInterval) { _, newPowerUpdateInterval in
|
||||
if newPowerUpdateInterval != node?.telemetryConfig?.powerUpdateInterval ?? -1 { hasChanges = true }
|
||||
}
|
||||
.onChange(of: powerScreenEnabled) { newPowerScreenEnabled in
|
||||
if node != nil && node?.telemetryConfig != nil {
|
||||
if newPowerScreenEnabled != node!.telemetryConfig!.powerScreenEnabled { hasChanges = true }
|
||||
}
|
||||
.onChange(of: powerScreenEnabled) { _, newPowerScreenEnabled in
|
||||
if newPowerScreenEnabled != node?.telemetryConfig?.powerScreenEnabled { hasChanges = true }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ struct NetworkConfig: View {
|
|||
.foregroundColor(.gray)
|
||||
.autocapitalization(.none)
|
||||
.disableAutocorrection(true)
|
||||
.onChange(of: wifiSsid, perform: { _ in
|
||||
.onChange(of: wifiSsid) {
|
||||
var totalBytes = wifiSsid.utf8.count
|
||||
// Only mess with the value if it is too big
|
||||
while totalBytes > 32 {
|
||||
|
|
@ -53,7 +53,7 @@ struct NetworkConfig: View {
|
|||
totalBytes = wifiSsid.utf8.count
|
||||
}
|
||||
hasChanges = true
|
||||
})
|
||||
}
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
.keyboardType(.default)
|
||||
|
|
@ -63,7 +63,7 @@ struct NetworkConfig: View {
|
|||
.foregroundColor(.gray)
|
||||
.autocapitalization(.none)
|
||||
.disableAutocorrection(true)
|
||||
.onChange(of: wifiPsk, perform: { _ in
|
||||
.onChange(of: wifiPsk) {
|
||||
var totalBytes = wifiPsk.utf8.count
|
||||
// Only mess with the value if it is too big
|
||||
while totalBytes > 63 {
|
||||
|
|
@ -71,7 +71,7 @@ struct NetworkConfig: View {
|
|||
totalBytes = wifiPsk.utf8.count
|
||||
}
|
||||
hasChanges = true
|
||||
})
|
||||
}
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
.keyboardType(.default)
|
||||
|
|
@ -151,20 +151,20 @@ struct NetworkConfig: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: wifiEnabled) {
|
||||
if $0 != node?.networkConfig?.wifiEnabled { hasChanges = true }
|
||||
.onChange(of: wifiEnabled) { _, newEnabled in
|
||||
if newEnabled != node?.networkConfig?.wifiEnabled { hasChanges = true }
|
||||
}
|
||||
.onChange(of: wifiSsid) { newSSID in
|
||||
.onChange(of: wifiSsid) { _, newSSID in
|
||||
if newSSID != node?.networkConfig?.wifiSsid { hasChanges = true }
|
||||
}
|
||||
.onChange(of: wifiPsk) { newPsk in
|
||||
.onChange(of: wifiPsk) { _, newPsk in
|
||||
if newPsk != node?.networkConfig?.wifiPsk { hasChanges = true }
|
||||
}
|
||||
.onChange(of: wifiMode) {
|
||||
if $0 != node?.networkConfig?.wifiMode ?? -1 { hasChanges = true }
|
||||
.onChange(of: wifiMode) { _, newMode in
|
||||
if newMode != node?.networkConfig?.wifiMode ?? -1 { hasChanges = true }
|
||||
}
|
||||
.onChange(of: ethEnabled) {
|
||||
if $0 != node?.networkConfig?.ethEnabled { hasChanges = true }
|
||||
.onChange(of: ethEnabled) { _, newEthEnabled in
|
||||
if newEthEnabled != node?.networkConfig?.ethEnabled { hasChanges = true }
|
||||
}
|
||||
}
|
||||
func setNetworkValues() {
|
||||
|
|
|
|||
|
|
@ -398,7 +398,7 @@ struct PositionConfig: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: fixedPosition) { newFixed in
|
||||
.onChange(of: fixedPosition) { _, newFixed in
|
||||
if supportedVersion {
|
||||
if let positionConfig = node?.positionConfig {
|
||||
/// Fixed Position is off to start
|
||||
|
|
@ -411,37 +411,37 @@ struct PositionConfig: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: gpsMode) { newGpsMode in
|
||||
.onChange(of: gpsMode) { _, newGpsMode in
|
||||
if newGpsMode != node?.positionConfig?.gpsMode ?? 0 { hasChanges = true }
|
||||
}
|
||||
.onChange(of: rxGpio) { newRxGpio in
|
||||
.onChange(of: rxGpio) { _, newRxGpio in
|
||||
if newRxGpio != node?.positionConfig?.rxGpio ?? 0 { hasChanges = true }
|
||||
}
|
||||
.onChange(of: txGpio) { newTxGpio in
|
||||
.onChange(of: txGpio) { _, newTxGpio in
|
||||
if newTxGpio != node?.positionConfig?.txGpio ?? 0 { hasChanges = true }
|
||||
}
|
||||
.onChange(of: gpsEnGpio) { newGpsEnGpio in
|
||||
.onChange(of: gpsEnGpio) { _, newGpsEnGpio in
|
||||
if newGpsEnGpio != node?.positionConfig?.gpsEnGpio ?? 0 { hasChanges = true }
|
||||
}
|
||||
.onChange(of: smartPositionEnabled) { newSmartPositionEnabled in
|
||||
.onChange(of: smartPositionEnabled) { _, newSmartPositionEnabled in
|
||||
if newSmartPositionEnabled != node?.positionConfig?.smartPositionEnabled { hasChanges = true }
|
||||
}
|
||||
.onChange(of: positionBroadcastSeconds) { newPositionBroadcastSeconds in
|
||||
.onChange(of: positionBroadcastSeconds) { _, newPositionBroadcastSeconds in
|
||||
if newPositionBroadcastSeconds != node?.positionConfig?.positionBroadcastSeconds ?? 0 { hasChanges = true }
|
||||
}
|
||||
.onChange(of: broadcastSmartMinimumIntervalSecs) { newBroadcastSmartMinimumIntervalSecs in
|
||||
.onChange(of: broadcastSmartMinimumIntervalSecs) { _, newBroadcastSmartMinimumIntervalSecs in
|
||||
if newBroadcastSmartMinimumIntervalSecs != node?.positionConfig?.broadcastSmartMinimumIntervalSecs ?? 0 { hasChanges = true }
|
||||
}
|
||||
.onChange(of: broadcastSmartMinimumDistance) { newBroadcastSmartMinimumDistance in
|
||||
.onChange(of: broadcastSmartMinimumDistance) { _, newBroadcastSmartMinimumDistance in
|
||||
if newBroadcastSmartMinimumDistance != node?.positionConfig?.broadcastSmartMinimumDistance ?? 0 { hasChanges = true }
|
||||
}
|
||||
.onChange(of: gpsUpdateInterval) { newGpsUpdateInterval in
|
||||
.onChange(of: gpsUpdateInterval) { _, newGpsUpdateInterval in
|
||||
if newGpsUpdateInterval != node?.positionConfig?.gpsUpdateInterval ?? 0 { hasChanges = true }
|
||||
}
|
||||
}
|
||||
|
||||
func handlePositionFlagtChanges() {
|
||||
guard let positionConfig = node?.positionConfig else { return }
|
||||
guard (node?.positionConfig) != nil else { return }
|
||||
let pf = PositionFlags(rawValue: self.positionFlags)
|
||||
hasChanges =
|
||||
pf.contains(.Altitude) ||
|
||||
|
|
|
|||
|
|
@ -148,31 +148,31 @@ struct PowerConfig: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: isPowerSaving) {
|
||||
if $0 != node?.powerConfig?.isPowerSaving { hasChanges = true }
|
||||
.onChange(of: isPowerSaving) { oldIsPowerSaving, newIsPowerSaving in
|
||||
if oldIsPowerSaving != newIsPowerSaving && newIsPowerSaving != node?.powerConfig?.isPowerSaving { hasChanges = true }
|
||||
}
|
||||
.onChange(of: shutdownOnPowerLoss) { newShutdownOnPowerLoss in
|
||||
.onChange(of: shutdownOnPowerLoss) { _, newShutdownOnPowerLoss in
|
||||
if newShutdownOnPowerLoss {
|
||||
hasChanges = true
|
||||
}
|
||||
}
|
||||
.onChange(of: shutdownAfterSecs) {
|
||||
if $0 != node?.powerConfig?.minWakeSecs ?? -1 { hasChanges = true }
|
||||
.onChange(of: shutdownAfterSecs) { oldShutdownAfterSecs, newShutdownAfterSecs in
|
||||
if oldShutdownAfterSecs != newShutdownAfterSecs && newShutdownAfterSecs != node?.powerConfig?.minWakeSecs ?? -1 { hasChanges = true }
|
||||
}
|
||||
.onChange(of: adcOverride) { _ in
|
||||
.onChange(of: adcOverride) {
|
||||
hasChanges = true
|
||||
}
|
||||
.onChange(of: adcMultiplier) { newAdcMultiplier in
|
||||
if newAdcMultiplier != node?.powerConfig?.adcMultiplierOverride ?? -1 { hasChanges = true }
|
||||
.onChange(of: adcMultiplier) { _, newAdcMultiplier in
|
||||
if newAdcMultiplier != node?.powerConfig?.adcMultiplierOverride ?? -1 { hasChanges = true }
|
||||
}
|
||||
.onChange(of: waitBluetoothSecs) {
|
||||
if $0 != node?.powerConfig?.waitBluetoothSecs ?? -1 { hasChanges = true }
|
||||
.onChange(of: waitBluetoothSecs) { oldWaitBluetoothSecs, newWaitBluetoothSecs in
|
||||
if oldWaitBluetoothSecs != newWaitBluetoothSecs && newWaitBluetoothSecs != node?.powerConfig?.waitBluetoothSecs ?? -1 { hasChanges = true }
|
||||
}
|
||||
.onChange(of: lsSecs) {
|
||||
if $0 != node?.powerConfig?.lsSecs ?? -1 { hasChanges = true }
|
||||
.onChange(of: lsSecs) { _, newLsSecs in
|
||||
if newLsSecs != node?.powerConfig?.lsSecs ?? -1 { hasChanges = true }
|
||||
}
|
||||
.onChange(of: minWakeSecs) {
|
||||
if $0 != node?.powerConfig?.minWakeSecs ?? -1 { hasChanges = true }
|
||||
.onChange(of: minWakeSecs) { _, newMinWakeSecs in
|
||||
if newMinWakeSecs != node?.powerConfig?.minWakeSecs ?? -1 { hasChanges = true }
|
||||
}
|
||||
|
||||
SaveConfigButton(node: node, hasChanges: $hasChanges) {
|
||||
|
|
@ -232,13 +232,13 @@ private struct FloatField: View {
|
|||
TextField(title.localized, value: $typingNumber, format: .number)
|
||||
.foregroundColor(.gray)
|
||||
.multilineTextAlignment(.trailing)
|
||||
.onChange(of: typingNumber, perform: { _ in
|
||||
.onChange(of: typingNumber) {
|
||||
if isValid(typingNumber) {
|
||||
number = typingNumber
|
||||
} else {
|
||||
typingNumber = number
|
||||
}
|
||||
})
|
||||
}
|
||||
.keyboardType(.decimalPad)
|
||||
.onAppear {
|
||||
typingNumber = number
|
||||
|
|
|
|||
|
|
@ -106,19 +106,19 @@ struct SecurityConfig: View {
|
|||
name: "\(bleManager.connectedPeripheral?.shortName ?? "?")"
|
||||
)
|
||||
})
|
||||
.onChange(of: isManaged) {
|
||||
if $0 != node?.securityConfig?.isManaged { hasChanges = true }
|
||||
.onChange(of: isManaged) { _, newIsManaged in
|
||||
if newIsManaged != node?.securityConfig?.isManaged { hasChanges = true }
|
||||
}
|
||||
.onChange(of: serialEnabled) {
|
||||
if $0 != node?.securityConfig?.serialEnabled { hasChanges = true }
|
||||
.onChange(of: serialEnabled) { _, newSerialEnabled in
|
||||
if newSerialEnabled != node?.securityConfig?.serialEnabled { hasChanges = true }
|
||||
}
|
||||
.onChange(of: debugLogApiEnabled) {
|
||||
if $0 != node?.securityConfig?.debugLogApiEnabled { hasChanges = true }
|
||||
.onChange(of: debugLogApiEnabled) { _, newDebugLogApiEnabled in
|
||||
if newDebugLogApiEnabled != node?.securityConfig?.debugLogApiEnabled { hasChanges = true }
|
||||
}
|
||||
.onChange(of: adminChannelEnabled) {
|
||||
if $0 != node?.securityConfig?.adminChannelEnabled { hasChanges = true }
|
||||
.onChange(of: adminChannelEnabled) { _, newAdminChannelEnabled in
|
||||
if newAdminChannelEnabled != node?.securityConfig?.adminChannelEnabled { hasChanges = true }
|
||||
}
|
||||
.onChange(of: publicKey) { _ in
|
||||
.onChange(of: publicKey) {
|
||||
let tempKey = Data(base64Encoded: publicKey) ?? Data()
|
||||
if tempKey.count == 32 {
|
||||
hasValidPublicKey = true
|
||||
|
|
@ -127,7 +127,7 @@ struct SecurityConfig: View {
|
|||
}
|
||||
hasChanges = true
|
||||
}
|
||||
.onChange(of: privateKey) { _ in
|
||||
.onChange(of: privateKey) {
|
||||
let tempKey = Data(base64Encoded: privateKey) ?? Data()
|
||||
if tempKey.count == 32 {
|
||||
hasValidPrivateKey = true
|
||||
|
|
@ -136,7 +136,7 @@ struct SecurityConfig: View {
|
|||
}
|
||||
hasChanges = true
|
||||
}
|
||||
.onChange(of: adminKey) { key in
|
||||
.onChange(of: adminKey) { _, key in
|
||||
let tempKey = Data(base64Encoded: key) ?? Data()
|
||||
if key.isEmpty {
|
||||
hasValidAdminKey = true
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ struct Firmware: View {
|
|||
@Environment(\.managedObjectContext) var context
|
||||
@EnvironmentObject var bleManager: BLEManager
|
||||
var node: NodeInfoEntity?
|
||||
@State var minimumVersion = "2.4.2"
|
||||
@State var minimumVersion = "2.5.4"
|
||||
@State var version = ""
|
||||
@State private var currentDevice: DeviceHardware?
|
||||
@State private var latestStable: FirmwareRelease?
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@
|
|||
import SwiftUI
|
||||
import CoreLocation
|
||||
|
||||
@available(iOS 17.0, macOS 14.0, *)
|
||||
struct GPSStatus: View {
|
||||
|
||||
var largeFont: Font = .footnote
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import CoreLocation
|
|||
import CoreMotion
|
||||
import OSLog
|
||||
|
||||
@available(iOS 17.0, macOS 14.0, *)
|
||||
struct RouteRecorder: View {
|
||||
|
||||
@ObservedObject var locationsHandler: LocationsHandler = LocationsHandler.shared
|
||||
|
|
@ -284,7 +283,7 @@ struct RouteRecorder: View {
|
|||
.onDisappear(perform: {
|
||||
UIApplication.shared.isIdleTimerDisabled = false
|
||||
})
|
||||
.onChange(of: locationsHandler.locationsArray.last) { newLoc in
|
||||
.onChange(of: locationsHandler.locationsArray.last) { _, newLoc in
|
||||
if locationsHandler.isRecording {
|
||||
if let loc = newLoc {
|
||||
if recording != nil {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import CoreData
|
|||
import MapKit
|
||||
import OSLog
|
||||
|
||||
@available(iOS 17.0, macOS 14.0, *)
|
||||
struct Routes: View {
|
||||
|
||||
@State private var columnVisibility = NavigationSplitViewVisibility.doubleColumn
|
||||
|
|
@ -58,7 +57,6 @@ struct Routes: View {
|
|||
}
|
||||
|
||||
do {
|
||||
|
||||
guard let fileContent = String(data: try Data(contentsOf: selectedFile), encoding: .utf8) else { return }
|
||||
let routeName = selectedFile.lastPathComponent.dropLast(4)
|
||||
let lines = fileContent.components(separatedBy: "\n")
|
||||
|
|
@ -176,14 +174,14 @@ struct Routes: View {
|
|||
axis: .vertical
|
||||
)
|
||||
.foregroundColor(Color.gray)
|
||||
.onChange(of: name, perform: { _ in
|
||||
.onChange(of: name) {
|
||||
var totalBytes = name.utf8.count
|
||||
// Only mess with the value if it is too big
|
||||
while totalBytes > 100 {
|
||||
name = String(name.dropLast())
|
||||
totalBytes = name.utf8.count
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Toggle(isOn: $enabled) {
|
||||
Label("enabled", systemImage: "point.topleft.filled.down.to.point.bottomright.curvepath")
|
||||
|
|
@ -237,16 +235,16 @@ struct Routes: View {
|
|||
.controlSize(.large)
|
||||
.disabled(!hasChanges)
|
||||
}
|
||||
.onChange(of: name) { _ in
|
||||
.onChange(of: name) {
|
||||
hasChanges = true
|
||||
}
|
||||
.onChange(of: notes) { _ in
|
||||
.onChange(of: notes) {
|
||||
hasChanges = true
|
||||
}
|
||||
.onChange(of: enabled) { _ in
|
||||
.onChange(of: enabled) {
|
||||
hasChanges = true
|
||||
}
|
||||
.onChange(of: color) { _ in
|
||||
.onChange(of: color) {
|
||||
hasChanges = true
|
||||
}
|
||||
Map {
|
||||
|
|
|
|||
|
|
@ -7,9 +7,7 @@
|
|||
|
||||
import SwiftUI
|
||||
import OSLog
|
||||
#if canImport(TipKit)
|
||||
import TipKit
|
||||
#endif
|
||||
|
||||
struct Settings: View {
|
||||
@Environment(\.managedObjectContext) var context
|
||||
|
|
@ -156,13 +154,11 @@ struct Settings: View {
|
|||
|
||||
var moduleConfigurationSection: some View {
|
||||
Section("module.configuration") {
|
||||
if #available(iOS 17.0, macOS 14.0, *) {
|
||||
NavigationLink(value: SettingsNavigationState.ambientLighting) {
|
||||
Label {
|
||||
Text("ambient.lighting")
|
||||
} icon: {
|
||||
Image(systemName: "light.max")
|
||||
}
|
||||
NavigationLink(value: SettingsNavigationState.ambientLighting) {
|
||||
Label {
|
||||
Text("ambient.lighting")
|
||||
} icon: {
|
||||
Image(systemName: "light.max")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -323,22 +319,20 @@ struct Settings: View {
|
|||
Image(systemName: "gearshape")
|
||||
}
|
||||
}
|
||||
if #available(iOS 17.0, macOS 14.0, *) {
|
||||
NavigationLink(value: SettingsNavigationState.routes) {
|
||||
Label {
|
||||
Text("routes")
|
||||
} icon: {
|
||||
Image(systemName: "road.lanes.curved.right")
|
||||
}
|
||||
NavigationLink(value: SettingsNavigationState.routes) {
|
||||
Label {
|
||||
Text("routes")
|
||||
} icon: {
|
||||
Image(systemName: "road.lanes.curved.right")
|
||||
}
|
||||
}
|
||||
|
||||
NavigationLink(value: SettingsNavigationState.routeRecorder) {
|
||||
Label {
|
||||
Text("route.recorder")
|
||||
} icon: {
|
||||
Image(systemName: "record.circle")
|
||||
.foregroundColor(.red)
|
||||
}
|
||||
NavigationLink(value: SettingsNavigationState.routeRecorder) {
|
||||
Label {
|
||||
Text("route.recorder")
|
||||
} icon: {
|
||||
Image(systemName: "record.circle")
|
||||
.foregroundColor(.red)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -392,7 +386,7 @@ struct Settings: View {
|
|||
}
|
||||
}
|
||||
.pickerStyle(.navigationLink)
|
||||
.onChange(of: selectedNode) { newValue in
|
||||
.onChange(of: selectedNode) { _, newValue in
|
||||
if selectedNode > 0 {
|
||||
let node = nodes.first(where: { $0.num == newValue })
|
||||
let connectedNode = nodes.first(where: { $0.num == preferredNodeNum })
|
||||
|
|
@ -405,9 +399,7 @@ struct Settings: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
if #available(iOS 17.0, macOS 14.0, *) {
|
||||
TipView(AdminChannelTip(), arrowEdge: .top)
|
||||
}
|
||||
TipView(AdminChannelTip(), arrowEdge: .top)
|
||||
} else {
|
||||
if bleManager.connectedPeripheral != nil {
|
||||
Text("Connected Node \(node?.user?.longName ?? "unknown".localized)")
|
||||
|
|
@ -418,9 +410,7 @@ struct Settings: View {
|
|||
radioConfigurationSection
|
||||
deviceConfigurationSection
|
||||
moduleConfigurationSection
|
||||
if #available (iOS 17.0, *) {
|
||||
loggingSection
|
||||
}
|
||||
loggingSection
|
||||
#if DEBUG
|
||||
developersSection
|
||||
#endif
|
||||
|
|
@ -435,13 +425,9 @@ struct Settings: View {
|
|||
case .appSettings:
|
||||
AppSettings()
|
||||
case .routes:
|
||||
if #available(iOS 17.0, *) {
|
||||
Routes()
|
||||
}
|
||||
Routes()
|
||||
case .routeRecorder:
|
||||
if #available(iOS 17.0, *) {
|
||||
RouteRecorder()
|
||||
}
|
||||
RouteRecorder()
|
||||
case .lora:
|
||||
LoRaConfig(node: nodes.first(where: { $0.num == selectedNode }))
|
||||
case .channels:
|
||||
|
|
@ -463,9 +449,7 @@ struct Settings: View {
|
|||
case .power:
|
||||
PowerConfig(node: nodes.first(where: { $0.num == selectedNode }))
|
||||
case .ambientLighting:
|
||||
if #available(iOS 17.0, macOS 14.0, *) {
|
||||
AmbientLightingConfig(node: node)
|
||||
}
|
||||
AmbientLightingConfig(node: node)
|
||||
case .cannedMessages:
|
||||
CannedMessagesConfig(node: nodes.first(where: { $0.num == selectedNode }))
|
||||
case .detectionSensor:
|
||||
|
|
@ -491,16 +475,14 @@ struct Settings: View {
|
|||
case .meshLog:
|
||||
MeshLog()
|
||||
case .debugLogs:
|
||||
if #available(iOS 17.0, macOS 14.0, *) {
|
||||
AppLog()
|
||||
}
|
||||
AppLog()
|
||||
case .appFiles:
|
||||
AppData()
|
||||
case .firmwareUpdates:
|
||||
Firmware(node: node)
|
||||
}
|
||||
}
|
||||
.onChange(of: UserDefaults.preferredPeripheralNum ) { newConnectedNode in
|
||||
.onChange(of: UserDefaults.preferredPeripheralNum ) { _, newConnectedNode in
|
||||
preferredNodeNum = newConnectedNode
|
||||
if nodes.count > 1 {
|
||||
if selectedNode == 0 {
|
||||
|
|
|
|||
|
|
@ -8,10 +8,7 @@ import SwiftUI
|
|||
import CoreData
|
||||
import CoreImage.CIFilterBuiltins
|
||||
import MeshtasticProtobufs
|
||||
|
||||
#if canImport(TipKit)
|
||||
import TipKit
|
||||
#endif
|
||||
|
||||
struct QrCodeImage {
|
||||
let context = CIContext()
|
||||
|
|
@ -55,10 +52,8 @@ struct ShareChannels: View {
|
|||
|
||||
var body: some View {
|
||||
|
||||
if #available(iOS 17.0, macOS 14.0, *) {
|
||||
VStack {
|
||||
TipView(ShareChannelsTip(), arrowEdge: .bottom)
|
||||
}
|
||||
VStack {
|
||||
TipView(ShareChannelsTip(), arrowEdge: .bottom)
|
||||
}
|
||||
GeometryReader { bounds in
|
||||
let smallest = min(bounds.size.width, bounds.size.height)
|
||||
|
|
@ -240,15 +235,15 @@ struct ShareChannels: View {
|
|||
.onAppear {
|
||||
generateChannelSet()
|
||||
}
|
||||
.onChange(of: includeChannel0) { _ in generateChannelSet() }
|
||||
.onChange(of: includeChannel1) { _ in generateChannelSet() }
|
||||
.onChange(of: includeChannel2) { _ in generateChannelSet() }
|
||||
.onChange(of: includeChannel3) { _ in generateChannelSet() }
|
||||
.onChange(of: includeChannel4) { _ in generateChannelSet() }
|
||||
.onChange(of: includeChannel5) { _ in generateChannelSet() }
|
||||
.onChange(of: includeChannel6) { _ in generateChannelSet() }
|
||||
.onChange(of: includeChannel7) { _ in generateChannelSet() }
|
||||
.onChange(of: replaceChannels) { _ in generateChannelSet() }
|
||||
.onChange(of: includeChannel0) { generateChannelSet() }
|
||||
.onChange(of: includeChannel1) { generateChannelSet() }
|
||||
.onChange(of: includeChannel2) { generateChannelSet() }
|
||||
.onChange(of: includeChannel3) { generateChannelSet() }
|
||||
.onChange(of: includeChannel4) { generateChannelSet() }
|
||||
.onChange(of: includeChannel5) { generateChannelSet() }
|
||||
.onChange(of: includeChannel6) { generateChannelSet() }
|
||||
.onChange(of: includeChannel7) { generateChannelSet() }
|
||||
.onChange(of: replaceChannels) { generateChannelSet() }
|
||||
}
|
||||
}
|
||||
func generateChannelSet() {
|
||||
|
|
|
|||
|
|
@ -49,14 +49,14 @@ struct UserConfig: View {
|
|||
Label(isLicensed ? "Call Sign" : "Long Name", systemImage: "person.crop.rectangle.fill")
|
||||
|
||||
TextField("Long Name", text: $longName)
|
||||
.onChange(of: longName, perform: { _ in
|
||||
.onChange(of: longName) {
|
||||
var totalBytes = longName.utf8.count
|
||||
// Only mess with the value if it is too big
|
||||
while totalBytes > (isLicensed ? 6 : 36) {
|
||||
longName = String(longName.dropLast())
|
||||
totalBytes = longName.utf8.count
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
.keyboardType(.default)
|
||||
.disableAutocorrection(true)
|
||||
|
|
@ -74,14 +74,14 @@ struct UserConfig: View {
|
|||
Label("Short Name", systemImage: "circlebadge.fill")
|
||||
TextField("Short Name", text: $shortName)
|
||||
.foregroundColor(.gray)
|
||||
.onChange(of: shortName, perform: { _ in
|
||||
.onChange(of: shortName) {
|
||||
var totalBytes = shortName.utf8.count
|
||||
// Only mess with the value if it is too big
|
||||
if totalBytes > 4 {
|
||||
shortName = String(shortName.dropLast())
|
||||
totalBytes = shortName.utf8.count
|
||||
}
|
||||
})
|
||||
}
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
.keyboardType(.default)
|
||||
|
|
@ -197,17 +197,17 @@ struct UserConfig: View {
|
|||
self.overrideFrequency = node?.loRaConfig?.overrideFrequency ?? 0.00
|
||||
self.hasChanges = false
|
||||
}
|
||||
.onChange(of: shortName) { newShort in
|
||||
.onChange(of: shortName) { _, newShort in
|
||||
if node != nil && node!.user != nil {
|
||||
if newShort != node?.user!.shortName { hasChanges = true }
|
||||
}
|
||||
}
|
||||
.onChange(of: longName) { newLong in
|
||||
.onChange(of: longName) { _, newLong in
|
||||
if node != nil && node!.user != nil {
|
||||
if newLong != node?.user!.longName { hasChanges = true }
|
||||
}
|
||||
}
|
||||
.onChange(of: isLicensed) { newIsLicensed in
|
||||
.onChange(of: isLicensed) { _, newIsLicensed in
|
||||
if node != nil && node!.user != nil {
|
||||
if newIsLicensed != node?.user!.isLicensed {
|
||||
hasChanges = true
|
||||
|
|
@ -219,10 +219,10 @@ struct UserConfig: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: overrideFrequency) { _ in
|
||||
.onChange(of: overrideFrequency) {
|
||||
hasChanges = true
|
||||
}
|
||||
.onChange(of: txPower) { _ in
|
||||
.onChange(of: txPower) {
|
||||
hasChanges = true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -120,7 +120,6 @@ struct WidgetsLiveActivity: Widget {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
struct WidgetsLiveActivity_Previews: PreviewProvider {
|
||||
static let attributes = MeshActivityAttributes(nodeNum: 123456789, name: "RAK Compact Rotary Handset Gray 8E6G")
|
||||
static let state = MeshActivityAttributes.ContentState(uptimeSeconds: 600, channelUtilization: 1.2, airtime: 3.5, sentPackets: 12587, receivedPackets: 12555, badReceivedPackets: 800, nodesOnline: 99, totalNodes: 100, timerRange: Date.now...Date(timeIntervalSinceNow: 300))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue