Remove ios 16 conditions

This commit is contained in:
Garth Vander Houwen 2024-10-05 10:44:46 -07:00
parent 386042b8df
commit d477de80c2
23 changed files with 386 additions and 598 deletions

View file

@ -11481,9 +11481,6 @@
},
"Map Tile Data" : {
},
"Map Type" : {
},
"map.centering" : {
"extractionState" : "manual",

View file

@ -1202,65 +1202,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
}

View file

@ -5,7 +5,6 @@ import CoreData
import OSLog
import TipKit
@available(iOS 17.0, *)
@main
struct MeshtasticAppleApp: App {
@ -109,23 +108,21 @@ 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

View file

@ -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 {
@ -66,7 +62,6 @@ class MeshtasticAppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificat
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 {
@ -82,7 +77,6 @@ class MeshtasticAppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificat
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,
@ -99,11 +93,10 @@ class MeshtasticAppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificat
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 +105,6 @@ class MeshtasticAppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificat
} else {
Logger.services.error("Failed to handle notification response: \(userInfo)")
}
completionHandler()
}
}

View file

@ -47,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) {
@ -76,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)
}

View file

@ -4,7 +4,6 @@
import SwiftUI
@available(iOS 17.0, *)
struct ContentView: View {
@ObservedObject
var appState: AppState
@ -39,20 +38,12 @@ 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
)

View file

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

View file

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

View file

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

View file

@ -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)
}
@ -269,11 +214,7 @@ struct DeviceMetricsLog: View {
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")

View file

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

View file

@ -8,7 +8,6 @@
import SwiftUI
import MapKit
@available(iOS 17.0, macOS 14.0, *)
struct MeshMapContent: MapContent {
/// Parameters

View file

@ -214,20 +214,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!)
}

View file

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

View file

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

View file

@ -274,18 +274,10 @@ 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

View file

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

View file

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

View file

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

View file

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

View file

@ -353,52 +353,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 ?? ""

View file

@ -154,16 +154,14 @@ 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")
}
}
NavigationLink(value: SettingsNavigationState.cannedMessages) {
Label {
Text("canned.messages")
@ -321,25 +319,24 @@ 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.routeRecorder) {
Label {
Text("route.recorder")
} icon: {
Image(systemName: "record.circle")
.foregroundColor(.red)
}
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)
}
}
if !(node?.deviceConfig?.isManaged ?? false) {
if bleManager.connectedPeripheral != nil {
Section("Configure") {
@ -403,9 +400,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)")
@ -416,9 +411,7 @@ struct Settings: View {
radioConfigurationSection
deviceConfigurationSection
moduleConfigurationSection
if #available (iOS 17.0, *) {
loggingSection
}
loggingSection
#if DEBUG
developersSection
#endif
@ -433,13 +426,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:
@ -461,9 +450,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:
@ -489,9 +476,7 @@ struct Settings: View {
case .meshLog:
MeshLog()
case .debugLogs:
if #available(iOS 17.0, macOS 14.0, *) {
AppLog()
}
AppLog()
case .appFiles:
AppData()
case .firmwareUpdates:

View file

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