mirror of
https://github.com/meshtastic/Meshtastic-Apple.git
synced 2026-04-20 22:13:56 +00:00
Merge pull request #654 from 72A12F4E/main
Address several SwiftLint issues
This commit is contained in:
commit
79e23d561e
108 changed files with 1230 additions and 1232 deletions
|
|
@ -1,3 +1,7 @@
|
|||
# Exclude automatically generated Swift files
|
||||
excluded:
|
||||
- Meshtastic/Protobufs
|
||||
|
||||
line_length: 400
|
||||
|
||||
type_name:
|
||||
|
|
@ -46,3 +50,4 @@ disabled_rules: # rule identifiers to exclude from running
|
|||
nesting:
|
||||
type_level:
|
||||
warning: 3
|
||||
|
||||
|
|
|
|||
|
|
@ -1032,10 +1032,10 @@
|
|||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = DDC2E17E26CE248F0042C5E4 /* Build configuration list for PBXNativeTarget "Meshtastic" */;
|
||||
buildPhases = (
|
||||
BB450974275599CE00509624 /* ShellScript */,
|
||||
DDC2E15026CE248E0042C5E4 /* Sources */,
|
||||
DDC2E15126CE248E0042C5E4 /* Frameworks */,
|
||||
DDC2E15226CE248E0042C5E4 /* Resources */,
|
||||
BB450974275599CE00509624 /* ShellScript */,
|
||||
DDDE5A0829AF163F00490C6C /* Embed Foundation Extensions */,
|
||||
);
|
||||
buildRules = (
|
||||
|
|
@ -1177,6 +1177,7 @@
|
|||
/* Begin PBXShellScriptBuildPhase section */
|
||||
BB450974275599CE00509624 /* ShellScript */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ enum DeviceRoles: Int, CaseIterable, Identifiable {
|
|||
case repeater = 4
|
||||
case router = 2
|
||||
case routerClient = 3
|
||||
|
||||
|
||||
var id: Int { self.rawValue }
|
||||
var name: String {
|
||||
switch self {
|
||||
|
|
@ -48,7 +48,7 @@ enum DeviceRoles: Int, CaseIterable, Identifiable {
|
|||
case .lostAndFound:
|
||||
return "Lost and Found"
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
var description: String {
|
||||
switch self {
|
||||
|
|
@ -76,7 +76,7 @@ enum DeviceRoles: Int, CaseIterable, Identifiable {
|
|||
return "device.role.lostandfound".localized
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var systemName: String {
|
||||
switch self {
|
||||
case .client:
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ enum RegionCodes: Int, CaseIterable, Identifiable {
|
|||
case my_919 = 17
|
||||
case sg_923 = 18
|
||||
case lora24 = 13
|
||||
var topic: String {
|
||||
var topic: String {
|
||||
switch self {
|
||||
case .unset:
|
||||
"UNSET"
|
||||
|
|
|
|||
|
|
@ -103,9 +103,9 @@ enum GpsMode: Int, CaseIterable, Equatable {
|
|||
case enabled = 1
|
||||
case disabled = 0
|
||||
case notPresent = 2
|
||||
|
||||
|
||||
var id: Int { self.rawValue }
|
||||
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .disabled:
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ enum ActivityType: Int, CaseIterable, Identifiable {
|
|||
case driving = 3
|
||||
case overlanding = 4
|
||||
case skiing = 5
|
||||
|
||||
|
||||
var id: Int { self.rawValue }
|
||||
var description: String {
|
||||
switch self {
|
||||
|
|
@ -33,7 +33,7 @@ enum ActivityType: Int, CaseIterable, Identifiable {
|
|||
return "routes.activitytype.skiing".localized
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var fileNameString: String {
|
||||
switch self {
|
||||
case .walking:
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ enum Aqi: Int, CaseIterable, Identifiable {
|
|||
case unhealthy = 3
|
||||
case veryUnhealthy = 4
|
||||
case hazardous = 5
|
||||
|
||||
|
||||
var id: Int { self.rawValue }
|
||||
var description: String {
|
||||
switch self {
|
||||
|
|
@ -65,7 +65,7 @@ enum Aqi: Int, CaseIterable, Identifiable {
|
|||
return Range(301...500)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static func getAqi(for value: Int) -> Aqi {
|
||||
let aqi: Aqi
|
||||
switch value {
|
||||
|
|
@ -96,7 +96,7 @@ enum Iaq: Int, CaseIterable, Identifiable {
|
|||
case heavilyPolluted = 4
|
||||
case severelyPolluted = 5
|
||||
case extremelyPolluted = 6
|
||||
|
||||
|
||||
var id: Int { self.rawValue }
|
||||
var description: String {
|
||||
switch self {
|
||||
|
|
@ -134,7 +134,7 @@ enum Iaq: Int, CaseIterable, Identifiable {
|
|||
return .brown
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var range: Range<Int> {
|
||||
switch self {
|
||||
case .excellent:
|
||||
|
|
|
|||
|
|
@ -118,7 +118,6 @@ func positionToCsvFile(positions: [PositionEntity]) -> String {
|
|||
return csvString
|
||||
}
|
||||
|
||||
|
||||
func routeToCsvFile(locations: [LocationEntity]) -> String {
|
||||
var csvString: String = ""
|
||||
let localeDateFormat = DateFormatter.dateFormat(fromTemplate: "yyMMddjmma", options: 0, locale: Locale.current)
|
||||
|
|
|
|||
|
|
@ -8,15 +8,15 @@
|
|||
import Foundation
|
||||
|
||||
extension Bundle {
|
||||
public var appName: String { getInfo("CFBundleName") }
|
||||
public var displayName: String { getInfo("CFBundleDisplayName") }
|
||||
public var language: String { getInfo("CFBundleDevelopmentRegion") }
|
||||
public var identifier: String { getInfo("CFBundleIdentifier") }
|
||||
public var copyright: String { getInfo("NSHumanReadableCopyright").replacingOccurrences(of: "\\\\n", with: "\n") }
|
||||
|
||||
public var appBuild: String { getInfo("CFBundleVersion") }
|
||||
public var appVersionLong: String { getInfo("CFBundleShortVersionString") }
|
||||
//public var appVersionShort: String { getInfo("CFBundleShortVersion") }
|
||||
|
||||
public var appName: String { getInfo("CFBundleName") }
|
||||
public var displayName: String { getInfo("CFBundleDisplayName") }
|
||||
public var language: String { getInfo("CFBundleDevelopmentRegion") }
|
||||
public var identifier: String { getInfo("CFBundleIdentifier") }
|
||||
public var copyright: String { getInfo("NSHumanReadableCopyright").replacingOccurrences(of: "\\\\n", with: "\n") }
|
||||
|
||||
public var appBuild: String { getInfo("CFBundleVersion") }
|
||||
public var appVersionLong: String { getInfo("CFBundleShortVersionString") }
|
||||
// public var appVersionShort: String { getInfo("CFBundleShortVersion") }
|
||||
|
||||
fileprivate func getInfo(_ str: String) -> String { infoDictionary?[str] as? String ?? "⚠️" }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,19 +28,19 @@ extension [CLLocationCoordinate2D] {
|
|||
/// 2D cross product of OA and OB vectors, i.e. z-component of their 3D cross product.
|
||||
/// Returns a positive value, if OAB makes a counter-clockwise turn,
|
||||
/// negative for clockwise turn, and zero if the points are collinear.
|
||||
func cross(P: CLLocationCoordinate2D, A: CLLocationCoordinate2D, B: CLLocationCoordinate2D) -> Double {
|
||||
let part1 = (A.longitude - P.longitude) * (B.latitude - P.latitude)
|
||||
let part2 = (A.latitude - P.latitude) * (B.longitude - P.longitude)
|
||||
return part1 - part2;
|
||||
func cross(p: CLLocationCoordinate2D, a: CLLocationCoordinate2D, b: CLLocationCoordinate2D) -> Double {
|
||||
let part1 = (a.longitude - p.longitude) * (b.latitude - p.latitude)
|
||||
let part2 = (a.latitude - p.latitude) * (b.longitude - p.longitude)
|
||||
return part1 - part2
|
||||
}
|
||||
// Sort points lexicographically
|
||||
let points = self.sorted() {
|
||||
let points = self.sorted {
|
||||
$0.longitude == $1.longitude ? $0.latitude < $1.latitude : $0.longitude < $1.longitude
|
||||
}
|
||||
// Build the lower hull
|
||||
var lower: [CLLocationCoordinate2D] = []
|
||||
for p in points {
|
||||
while lower.count >= 2 && cross(P: lower[lower.count - 2], A: lower[lower.count - 1], B: p) <= 0 {
|
||||
while lower.count >= 2 && cross(p: lower[lower.count - 2], a: lower[lower.count - 1], b: p) <= 0 {
|
||||
lower.removeLast()
|
||||
}
|
||||
lower.append(p)
|
||||
|
|
@ -48,7 +48,7 @@ extension [CLLocationCoordinate2D] {
|
|||
// Build upper hull
|
||||
var upper: [CLLocationCoordinate2D] = []
|
||||
for p in points.reversed() {
|
||||
while upper.count >= 2 && cross(P: upper[upper.count-2], A: upper[upper.count-1], B: p) <= 0 {
|
||||
while upper.count >= 2 && cross(p: upper[upper.count-2], a: upper[upper.count-1], b: p) <= 0 {
|
||||
upper.removeLast()
|
||||
}
|
||||
upper.append(p)
|
||||
|
|
|
|||
|
|
@ -12,13 +12,13 @@ extension ChannelEntity {
|
|||
|
||||
self.value(forKey: "allPrivateMessages") as? [MessageEntity] ?? [MessageEntity]()
|
||||
}
|
||||
|
||||
|
||||
var unreadMessages: Int {
|
||||
|
||||
let unreadMessages = allPrivateMessages.filter{ ($0 as AnyObject).read == false }
|
||||
|
||||
let unreadMessages = allPrivateMessages.filter { ($0 as AnyObject).read == false }
|
||||
return unreadMessages.count
|
||||
}
|
||||
|
||||
|
||||
var protoBuf: Channel {
|
||||
var channel = Channel()
|
||||
channel.index = self.index
|
||||
|
|
|
|||
|
|
@ -39,4 +39,3 @@ extension LocationEntity {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,17 +8,17 @@
|
|||
import Foundation
|
||||
|
||||
extension MyInfoEntity {
|
||||
|
||||
|
||||
var messageList: [MessageEntity] {
|
||||
self.value(forKey: "allMessages") as? [MessageEntity] ?? [MessageEntity]()
|
||||
}
|
||||
|
||||
|
||||
var unreadMessages: Int {
|
||||
let unreadMessages = messageList.filter{ ($0 as AnyObject).read == false && ($0 as AnyObject).isEmoji == false }
|
||||
let unreadMessages = messageList.filter { ($0 as AnyObject).read == false && ($0 as AnyObject).isEmoji == false }
|
||||
return unreadMessages.count
|
||||
}
|
||||
var hasAdmin: Bool {
|
||||
let adminChannel = channels?.filter{ ($0 as AnyObject).name?.lowercased() == "admin" }
|
||||
let adminChannel = channels?.filter { ($0 as AnyObject).name?.lowercased() == "admin" }
|
||||
return adminChannel?.count ?? 0 > 0
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,37 +9,36 @@ import Foundation
|
|||
import CoreData
|
||||
|
||||
extension NodeInfoEntity {
|
||||
|
||||
|
||||
var hasPositions: Bool {
|
||||
return positions?.count ?? 0 > 0
|
||||
}
|
||||
|
||||
|
||||
var hasDeviceMetrics: Bool {
|
||||
let deviceMetrics = telemetries?.filter{ ($0 as AnyObject).metricsType == 0 }
|
||||
let deviceMetrics = telemetries?.filter { ($0 as AnyObject).metricsType == 0 }
|
||||
return deviceMetrics?.count ?? 0 > 0
|
||||
}
|
||||
|
||||
|
||||
var hasEnvironmentMetrics: Bool {
|
||||
let environmentMetrics = telemetries?.filter{ ($0 as AnyObject).metricsType == 1 }
|
||||
let environmentMetrics = telemetries?.filter { ($0 as AnyObject).metricsType == 1 }
|
||||
return environmentMetrics?.count ?? 0 > 0
|
||||
}
|
||||
var hasDetectionSensorMetrics: Bool {
|
||||
return user?.sensorMessageList.count ?? 0 > 0
|
||||
}
|
||||
|
||||
|
||||
var hasTraceRoutes: Bool {
|
||||
return traceRoutes?.count ?? 0 > 0
|
||||
}
|
||||
|
||||
|
||||
var hasPax: Bool {
|
||||
return pax?.count ?? 0 > 0
|
||||
}
|
||||
|
||||
|
||||
|
||||
var isStoreForwardRouter: Bool {
|
||||
return storeForwardConfig?.isRouter ?? false
|
||||
}
|
||||
|
||||
|
||||
var isOnline: Bool {
|
||||
let fifteenMinutesAgo = Calendar.current.date(byAdding: .minute, value: -15, to: Date())
|
||||
if lastHeard?.compare(fifteenMinutesAgo!) == .orderedDescending {
|
||||
|
|
@ -50,13 +49,13 @@ extension NodeInfoEntity {
|
|||
}
|
||||
|
||||
public func createNodeInfo(num: Int64, context: NSManagedObjectContext) -> NodeInfoEntity {
|
||||
|
||||
|
||||
let newNode = NodeInfoEntity(context: context)
|
||||
newNode.id = Int64(num)
|
||||
newNode.num = Int64(num)
|
||||
let newUser = UserEntity(context: context)
|
||||
newUser.num = Int64(num)
|
||||
let userId = String(format:"%2X", num)
|
||||
let userId = String(format: "%2X", num)
|
||||
newUser.userId = "!\(userId)"
|
||||
let last4 = String(userId.suffix(4))
|
||||
newUser.longName = "Meshtastic \(last4)"
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import MapKit
|
|||
import SwiftUI
|
||||
|
||||
extension PositionEntity {
|
||||
|
||||
|
||||
static func allPositionsFetchRequest() -> NSFetchRequest<PositionEntity> {
|
||||
let request: NSFetchRequest<PositionEntity> = PositionEntity.fetchRequest()
|
||||
request.fetchLimit = 1000
|
||||
|
|
@ -20,20 +20,20 @@ extension PositionEntity {
|
|||
request.returnsDistinctResults = true
|
||||
request.sortDescriptors = [NSSortDescriptor(key: "time", ascending: false)]
|
||||
let positionPredicate = NSPredicate(format: "nodePosition != nil && (nodePosition.user.shortName != nil || nodePosition.user.shortName != '') && latest == true")
|
||||
|
||||
|
||||
let pointOfInterest = LocationHelper.currentLocation
|
||||
|
||||
|
||||
if pointOfInterest.latitude != LocationHelper.DefaultLocation.latitude && pointOfInterest.longitude != LocationHelper.DefaultLocation.longitude {
|
||||
let D: Double = UserDefaults.meshMapDistance * 1.1
|
||||
let R: Double = 6371009
|
||||
let d: Double = UserDefaults.meshMapDistance * 1.1
|
||||
let r: Double = 6371009
|
||||
let meanLatitidue = pointOfInterest.latitude * .pi / 180
|
||||
let deltaLatitude = D / R * 180 / .pi
|
||||
let deltaLongitude = D / (R * cos(meanLatitidue)) * 180 / .pi
|
||||
let deltaLatitude = d / r * 180 / .pi
|
||||
let deltaLongitude = d / (r * cos(meanLatitidue)) * 180 / .pi
|
||||
let minLatitude: Double = pointOfInterest.latitude - deltaLatitude
|
||||
let maxLatitude: Double = pointOfInterest.latitude + deltaLatitude
|
||||
let minLongitude: Double = pointOfInterest.longitude - deltaLongitude
|
||||
let maxLongitude: Double = pointOfInterest.longitude + deltaLongitude
|
||||
let distancePredicate = NSPredicate(format: "(%lf <= (longitudeI / 1e7)) AND ((longitudeI / 1e7) <= %lf) AND (%lf <= (latitudeI / 1e7)) AND ((latitudeI / 1e7) <= %lf)", minLongitude, maxLongitude,minLatitude, maxLatitude)
|
||||
let distancePredicate = NSPredicate(format: "(%lf <= (longitudeI / 1e7)) AND ((longitudeI / 1e7) <= %lf) AND (%lf <= (latitudeI / 1e7)) AND ((latitudeI / 1e7) <= %lf)", minLongitude, maxLongitude, minLatitude, maxLatitude)
|
||||
request.predicate = NSCompoundPredicate(type: .and, subpredicates: [positionPredicate, distancePredicate])
|
||||
} else {
|
||||
request.predicate = positionPredicate
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import Foundation
|
|||
import CoreData
|
||||
|
||||
extension UserEntity {
|
||||
|
||||
|
||||
var messageList: [MessageEntity] {
|
||||
self.value(forKey: "allMessages") as? [MessageEntity] ?? [MessageEntity]()
|
||||
|
|
@ -18,22 +17,21 @@ extension UserEntity {
|
|||
var adminMessageList: [MessageEntity] {
|
||||
self.value(forKey: "adminMessages") as? [MessageEntity] ?? [MessageEntity]()
|
||||
}
|
||||
|
||||
|
||||
var sensorMessageList: [MessageEntity] {
|
||||
self.value(forKey: "detectionSensorMessages") as? [MessageEntity] ?? [MessageEntity]()
|
||||
}
|
||||
|
||||
|
||||
var unreadMessages: Int {
|
||||
let unreadMessages = messageList.filter{ ($0 as AnyObject).read == false }
|
||||
let unreadMessages = messageList.filter { ($0 as AnyObject).read == false }
|
||||
return unreadMessages.count
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public func createUser(num: Int64, context: NSManagedObjectContext) -> UserEntity {
|
||||
let newUser = UserEntity(context: context)
|
||||
newUser.num = Int64(num)
|
||||
let userId = String(format:"%2X", num)
|
||||
let userId = String(format: "%2X", num)
|
||||
newUser.userId = "!\(userId)"
|
||||
let last4 = String(userId.suffix(4))
|
||||
newUser.longName = "Meshtastic \(last4)"
|
||||
|
|
|
|||
|
|
@ -10,13 +10,13 @@ import MapKit
|
|||
import SwiftUI
|
||||
|
||||
extension WaypointEntity {
|
||||
|
||||
|
||||
static func allWaypointssFetchRequest() -> NSFetchRequest<WaypointEntity> {
|
||||
let request: NSFetchRequest<WaypointEntity> = WaypointEntity.fetchRequest()
|
||||
request.fetchLimit = 50
|
||||
//request.fetchBatchSize = 1
|
||||
//request.returnsObjectsAsFaults = false
|
||||
//request.includesSubentities = true
|
||||
// request.fetchBatchSize = 1
|
||||
// request.returnsObjectsAsFaults = false
|
||||
// request.includesSubentities = true
|
||||
request.returnsDistinctResults = true
|
||||
request.sortDescriptors = [NSSortDescriptor(key: "name", ascending: false)]
|
||||
request.predicate = NSPredicate(format: "expire == nil || expire >= %@", Date() as NSDate)
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
import Foundation
|
||||
|
||||
extension Date {
|
||||
|
||||
|
||||
func formattedDate(format: String) -> String {
|
||||
let dateformat = DateFormatter()
|
||||
dateformat.dateFormat = format
|
||||
|
|
@ -20,12 +20,12 @@ extension Date {
|
|||
}
|
||||
func relativeTimeOfDay() -> String {
|
||||
let hour = Calendar.current.component(.hour, from: self)
|
||||
|
||||
|
||||
switch hour {
|
||||
case 6..<12 : return "relativetimeofday.morning".localized
|
||||
case 12 : return "relativetimeofday.midday".localized
|
||||
case 13..<17 : return "relativetimeofday.afternoon".localized
|
||||
case 17..<22 : return "relativetimeofday.evening".localized
|
||||
case 6..<12: return "relativetimeofday.morning".localized
|
||||
case 12: return "relativetimeofday.midday".localized
|
||||
case 13..<17: return "relativetimeofday.afternoon".localized
|
||||
case 17..<22: return "relativetimeofday.evening".localized
|
||||
default: return "relativetimeofday.nighttime".localized
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ extension PlottableMeasurement: Plottable where UnitType == UnitLength {
|
|||
var primitivePlottable: Double {
|
||||
self.measurement.converted(to: .meters).value
|
||||
}
|
||||
|
||||
|
||||
init?(primitivePlottable: Double) {
|
||||
self.init(
|
||||
measurement: Measurement(
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ extension UIColor {
|
|||
var green: CGFloat = 0
|
||||
var alpha: CGFloat = 0
|
||||
|
||||
getRed(&red, green: &green, blue: &blue, alpha: &alpha)
|
||||
getRed(&red, green: &green, blue: &blue, alpha: &alpha)
|
||||
|
||||
return UIColor(
|
||||
red: add(componentDelta, toComponent: red),
|
||||
|
|
@ -37,7 +37,7 @@ extension UIColor {
|
|||
private func add(_ value: CGFloat, toComponent: CGFloat) -> CGFloat {
|
||||
return max(0, min(1, toComponent + value))
|
||||
}
|
||||
|
||||
|
||||
static var random: UIColor {
|
||||
return UIColor(
|
||||
red: .random(in: 0...1),
|
||||
|
|
|
|||
|
|
@ -8,10 +8,10 @@
|
|||
import Foundation
|
||||
|
||||
extension URL {
|
||||
|
||||
|
||||
func regularFileAllocatedSize() throws -> UInt64 {
|
||||
let resourceValues = try self.resourceValues(forKeys: allocatedSizeResourceKeys)
|
||||
|
||||
|
||||
guard resourceValues.isRegularFile ?? false else {
|
||||
return 0
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ struct UserDefault<T: Decodable> {
|
|||
|
||||
return value
|
||||
}
|
||||
|
||||
|
||||
return UserDefaults.standard.object(forKey: key.rawValue) as? T ?? defaultValue
|
||||
}
|
||||
set {
|
||||
|
|
@ -95,10 +95,10 @@ extension UserDefaults {
|
|||
|
||||
@UserDefault(.meshMapDistance, defaultValue: 800000)
|
||||
static var meshMapDistance: Double
|
||||
|
||||
|
||||
@UserDefault(.enableMapWaypoints, defaultValue: false)
|
||||
static var enableMapWaypoints: Bool
|
||||
|
||||
|
||||
@UserDefault(.enableMapRecentering, defaultValue: false)
|
||||
static var enableMapRecentering: Bool
|
||||
|
||||
|
|
@ -146,13 +146,13 @@ extension UserDefaults {
|
|||
|
||||
@UserDefault(.enableSmartPosition, defaultValue: false)
|
||||
static var enableSmartPosition: Bool
|
||||
|
||||
|
||||
@UserDefault(.channelMessageNotifications, defaultValue: true)
|
||||
static var channelMessageNotifications: Bool
|
||||
|
||||
|
||||
@UserDefault(.newNodeNotifications, defaultValue: true)
|
||||
static var newNodeNotifications: Bool
|
||||
|
||||
|
||||
@UserDefault(.lowBatteryNotifications, defaultValue: true)
|
||||
static var lowBatteryNotifications: Bool
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -8,10 +8,6 @@ import SwiftUI
|
|||
|
||||
class SwiftUIEmojiTextField: UITextField {
|
||||
|
||||
override func awakeFromNib() {
|
||||
super.awakeFromNib()
|
||||
}
|
||||
|
||||
func setEmoji() {
|
||||
_ = self.textInputMode
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ class LocalNotificationManager {
|
|||
content.body = notification.content
|
||||
content.sound = .default
|
||||
content.interruptionLevel = .timeSensitive
|
||||
|
||||
|
||||
if notification.target != nil {
|
||||
content.userInfo["target"] = notification.target
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ import MapKit
|
|||
class LocationHelper: NSObject, ObservableObject, CLLocationManagerDelegate {
|
||||
static let shared = LocationHelper()
|
||||
var locationManager = CLLocationManager()
|
||||
|
||||
//@Published var region = MKCoordinateRegion()
|
||||
|
||||
// @Published var region = MKCoordinateRegion()
|
||||
@Published var authorizationStatus: CLAuthorizationStatus?
|
||||
override init() {
|
||||
super.init()
|
||||
|
|
@ -47,7 +47,7 @@ class LocationHelper: NSObject, ObservableObject, CLLocationManagerDelegate {
|
|||
}
|
||||
return sats
|
||||
}
|
||||
|
||||
|
||||
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
|
||||
switch manager.authorizationStatus {
|
||||
case .authorizedAlways:
|
||||
|
|
@ -67,7 +67,7 @@ class LocationHelper: NSObject, ObservableObject, CLLocationManagerDelegate {
|
|||
}
|
||||
}
|
||||
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
|
||||
|
||||
|
||||
}
|
||||
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
|
||||
print("Location manager error: \(error.localizedDescription)")
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import CoreLocation
|
|||
private let manager: CLLocationManager
|
||||
private var background: CLBackgroundActivitySession?
|
||||
var enableSmartPosition: Bool = UserDefaults.enableSmartPosition
|
||||
|
||||
|
||||
@Published var locationsArray: [CLLocation]
|
||||
@Published var isStationary = false
|
||||
@Published var count = 0
|
||||
|
|
@ -25,12 +25,12 @@ import CoreLocation
|
|||
@Published var recordingStarted: Date?
|
||||
@Published var distanceTraveled = 0.0
|
||||
@Published var elevationGain = 0.0
|
||||
|
||||
|
||||
@Published
|
||||
var updatesStarted: Bool = UserDefaults.standard.bool(forKey: "liveUpdatesStarted") {
|
||||
didSet { UserDefaults.standard.set(updatesStarted, forKey: "liveUpdatesStarted") }
|
||||
}
|
||||
|
||||
|
||||
@Published
|
||||
var backgroundActivity: Bool = UserDefaults.standard.bool(forKey: "BGActivitySessionStarted") {
|
||||
didSet {
|
||||
|
|
@ -38,27 +38,27 @@ import CoreLocation
|
|||
UserDefaults.standard.set(backgroundActivity, forKey: "BGActivitySessionStarted")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private init() {
|
||||
self.manager = CLLocationManager() // Creating a location manager instance is safe to call here in `MainActor`.
|
||||
self.manager.allowsBackgroundLocationUpdates = true
|
||||
locationsArray = [CLLocation]()
|
||||
}
|
||||
|
||||
|
||||
func startLocationUpdates() {
|
||||
if self.manager.authorizationStatus == .notDetermined {
|
||||
self.manager.requestWhenInUseAuthorization()
|
||||
}
|
||||
print("Starting location updates")
|
||||
Task() {
|
||||
Task {
|
||||
do {
|
||||
self.updatesStarted = true
|
||||
let updates = CLLocationUpdate.liveUpdates()
|
||||
for try await update in updates {
|
||||
if !self.updatesStarted { break }
|
||||
if !self.updatesStarted { break }
|
||||
if let loc = update.location {
|
||||
self.isStationary = update.isStationary
|
||||
|
||||
|
||||
var locationAdded: Bool
|
||||
locationAdded = addLocation(loc, smartPostion: enableSmartPosition)
|
||||
if !isRecording && locationAdded {
|
||||
|
|
@ -74,12 +74,12 @@ import CoreLocation
|
|||
return
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func stopLocationUpdates() {
|
||||
print("Stopping location updates")
|
||||
self.updatesStarted = false
|
||||
}
|
||||
|
||||
|
||||
func addLocation(_ location: CLLocation, smartPostion: Bool) -> Bool {
|
||||
if smartPostion {
|
||||
let age = -location.timestamp.timeIntervalSinceNow
|
||||
|
|
@ -111,9 +111,9 @@ import CoreLocation
|
|||
}
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
static let DefaultLocation = CLLocationCoordinate2D(latitude: 37.3346, longitude: -122.0090)
|
||||
|
||||
|
||||
static var satsInView: Int {
|
||||
var sats = 0
|
||||
if let newLocation = shared.locationsArray.last {
|
||||
|
|
|
|||
|
|
@ -226,7 +226,7 @@ func deviceMetadataPacket (metadata: DeviceMetadata, fromNum: Int64, context: NS
|
|||
if fetchedNode.count > 0 {
|
||||
fetchedNode[0].metadata = newMetadata
|
||||
} else {
|
||||
|
||||
|
||||
if fromNum > 0 {
|
||||
let newNode = createNodeInfo(num: Int64(fromNum), context: context)
|
||||
newNode.metadata = newMetadata
|
||||
|
|
@ -284,7 +284,7 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje
|
|||
newNode.lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(nodeInfo.lastHeard)))
|
||||
newNode.snr = nodeInfo.snr
|
||||
if nodeInfo.hasUser {
|
||||
|
||||
|
||||
let newUser = UserEntity(context: context)
|
||||
newUser.userId = nodeInfo.user.id
|
||||
newUser.num = Int64(nodeInfo.num)
|
||||
|
|
@ -307,7 +307,7 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje
|
|||
position.longitudeI = nodeInfo.position.longitudeI
|
||||
position.altitude = nodeInfo.position.altitude
|
||||
position.satsInView = Int32(nodeInfo.position.satsInView)
|
||||
position.speed = Int32(nodeInfo.position.groundSpeed)
|
||||
position.speed = Int32(nodeInfo.position.groundSpeed)
|
||||
position.heading = Int32(nodeInfo.position.groundTrack)
|
||||
position.time = Date(timeIntervalSince1970: TimeInterval(Int64(nodeInfo.position.time)))
|
||||
var newPostions = [PositionEntity]()
|
||||
|
|
@ -349,7 +349,7 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje
|
|||
fetchedNode[0].hopsAway = Int32(nodeInfo.hopsAway)
|
||||
|
||||
if nodeInfo.hasUser {
|
||||
if (fetchedNode[0].user == nil) {
|
||||
if fetchedNode[0].user == nil {
|
||||
fetchedNode[0].user = UserEntity(context: context)
|
||||
}
|
||||
fetchedNode[0].user!.userId = nodeInfo.user.id
|
||||
|
|
@ -360,9 +360,9 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje
|
|||
fetchedNode[0].user!.isLicensed = nodeInfo.user.isLicensed
|
||||
fetchedNode[0].user!.role = Int32(nodeInfo.user.role.rawValue)
|
||||
fetchedNode[0].user!.hwModel = String(describing: nodeInfo.user.hwModel).uppercased()
|
||||
} else {
|
||||
if (fetchedNode[0].user == nil && nodeInfo.num > Int16.max) {
|
||||
|
||||
} else {
|
||||
if fetchedNode[0].user == nil && nodeInfo.num > Int16.max {
|
||||
|
||||
let newUser = createUser(num: Int64(nodeInfo.num), context: context)
|
||||
fetchedNode[0].user = newUser
|
||||
}
|
||||
|
|
@ -550,25 +550,25 @@ func adminResponseAck (packet: MeshPacket, context: NSManagedObjectContext) {
|
|||
}
|
||||
}
|
||||
func paxCounterPacket (packet: MeshPacket, context: NSManagedObjectContext) {
|
||||
|
||||
|
||||
let logString = String.localizedStringWithFormat("mesh.log.paxcounter %@".localized, String(packet.from))
|
||||
MeshLogger.log("🧑🤝🧑 \(logString)")
|
||||
|
||||
let fetchNodeInfoRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "NodeInfoEntity")
|
||||
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(packet.from))
|
||||
|
||||
|
||||
do {
|
||||
let fetchedNode = try context.fetch(fetchNodeInfoRequest) as? [NodeInfoEntity]
|
||||
|
||||
|
||||
if let paxMessage = try? Paxcount(serializedData: packet.decoded.payload) {
|
||||
|
||||
|
||||
let newPax = PaxCounterEntity(context: context)
|
||||
newPax.ble = Int32(truncatingIfNeeded: paxMessage.ble)
|
||||
newPax.wifi = Int32(truncatingIfNeeded: paxMessage.wifi)
|
||||
newPax.uptime = Int32(truncatingIfNeeded: paxMessage.uptime)
|
||||
newPax.time = Date()
|
||||
|
||||
if (fetchedNode?.count ?? 0 > 0) {
|
||||
|
||||
if fetchedNode?.count ?? 0 > 0 {
|
||||
guard let mutablePax = fetchedNode?[0].pax!.mutableCopy() as? NSMutableOrderedSet else {
|
||||
return
|
||||
}
|
||||
|
|
@ -584,7 +584,7 @@ func paxCounterPacket (packet: MeshPacket, context: NSManagedObjectContext) {
|
|||
}
|
||||
}
|
||||
} catch {
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -619,7 +619,7 @@ func routingPacket (packet: MeshPacket, connectedNodeNum: Int64, context: NSMana
|
|||
}
|
||||
fetchedMessage![0].ackSNR = packet.rxSnr
|
||||
fetchedMessage![0].ackTimestamp = Int32(truncatingIfNeeded: packet.rxTime)
|
||||
|
||||
|
||||
if fetchedMessage![0].toUser != nil {
|
||||
fetchedMessage![0].toUser!.objectWillChange.send()
|
||||
} else {
|
||||
|
|
@ -772,20 +772,20 @@ func textMessageAppPacket(packet: MeshPacket, wantRangeTestPackets: Bool, connec
|
|||
}
|
||||
}
|
||||
let rangeTest = messageText?.contains(rangeTestRegex) ?? false && messageText?.starts(with: "seq ") ?? false
|
||||
|
||||
|
||||
if !wantRangeTestPackets && rangeTest {
|
||||
return
|
||||
}
|
||||
var storeForwardBroadcast = false
|
||||
if storeForward {
|
||||
if let storeAndForwardMessage = try? StoreAndForward(serializedData: packet.decoded.payload) {
|
||||
messageText = String(bytes: storeAndForwardMessage.text, encoding: .utf8)
|
||||
messageText = String(bytes: storeAndForwardMessage.text, encoding: .utf8)
|
||||
if storeAndForwardMessage.rr == .routerTextBroadcast {
|
||||
storeForwardBroadcast = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if messageText?.count ?? 0 > 0 {
|
||||
|
||||
MeshLogger.log("💬 \("mesh.log.textmessage.received".localized)")
|
||||
|
|
@ -837,7 +837,7 @@ func textMessageAppPacket(packet: MeshPacket, wantRangeTestPackets: Bool, connec
|
|||
messageSaved = true
|
||||
|
||||
if messageSaved {
|
||||
|
||||
|
||||
if packet.decoded.portnum == PortNum.detectionSensorApp && !UserDefaults.enableDetectionNotifications {
|
||||
return
|
||||
}
|
||||
|
|
@ -876,7 +876,7 @@ func textMessageAppPacket(packet: MeshPacket, wantRangeTestPackets: Bool, connec
|
|||
if !fetchedMyInfo.isEmpty {
|
||||
appState.unreadChannelMessages = fetchedMyInfo[0].unreadMessages
|
||||
UIApplication.shared.applicationIconBadgeNumber = appState.unreadChannelMessages + appState.unreadDirectMessages
|
||||
|
||||
|
||||
for channel in (fetchedMyInfo[0].channels?.array ?? []) as? [ChannelEntity] ?? [] {
|
||||
if channel.index == newMessage.channel {
|
||||
context.refresh(channel, mergeChanges: true)
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ class MqttClientProxyManager {
|
|||
let minimumVersion = "2.3.2"
|
||||
let currentVersion = UserDefaults.firmwareVersion
|
||||
let supportedVersion = minimumVersion.compare(currentVersion, options: .numeric) == .orderedAscending || minimumVersion.compare(currentVersion, options: .numeric) == .orderedSame
|
||||
|
||||
|
||||
if let host = host {
|
||||
let port = defaultServerPort
|
||||
let username = node.mqttConfig?.username
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import TipKit
|
|||
@available(iOS 17.0, *)
|
||||
@main
|
||||
struct MeshtasticAppleApp: App {
|
||||
|
||||
|
||||
@UIApplicationDelegateAdaptor(MeshtasticAppDelegate.self) var appDelegate
|
||||
let persistenceController = PersistenceController.shared
|
||||
@ObservedObject private var bleManager: BLEManager = BLEManager.shared
|
||||
|
|
@ -28,7 +28,7 @@ struct MeshtasticAppleApp: App {
|
|||
.environment(\.managedObjectContext, persistenceController.container.viewContext)
|
||||
.environmentObject(bleManager)
|
||||
.sheet(isPresented: $saveChannels) {
|
||||
SaveChannelQRCode(channelSetLink: channelSettings ?? "Empty Channel URL", addChannels: addChannels, bleManager: bleManager)
|
||||
SaveChannelQRCode(channelSetLink: channelSettings ?? "Empty Channel URL", addChannels: addChannels, bleManager: bleManager)
|
||||
.presentationDetents([.large])
|
||||
.presentationDragIndicator(.visible)
|
||||
}
|
||||
|
|
@ -71,13 +71,12 @@ struct MeshtasticAppleApp: App {
|
|||
} else if path.starts(with: "meshtastic://nodes") {
|
||||
AppState.shared.tabSelection = Tab.nodes
|
||||
}
|
||||
|
||||
|
||||
|
||||
} else {
|
||||
saveChannels = false
|
||||
print("User wants to import a MBTILES offline map file: \(self.incomingUrl?.absoluteString ?? "No Tiles link")")
|
||||
}
|
||||
|
||||
|
||||
/// Only do the map tiles stuff if it is enabled
|
||||
if UserDefaults.enableOfflineMapsMBTiles {
|
||||
/// we are expecting a .mbtiles map file that contains raster data
|
||||
|
|
@ -87,30 +86,30 @@ struct MeshtasticAppleApp: App {
|
|||
let destination = documentsDirectory.appendingPathComponent("offline_map.mbtiles", isDirectory: false)
|
||||
|
||||
if !self.saveChannels {
|
||||
|
||||
|
||||
// tell the system we want the file please
|
||||
guard url.startAccessingSecurityScopedResource() else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// do we need to delete an old one?
|
||||
if fileManager.fileExists(atPath: destination.path) {
|
||||
print("ℹ️ Found an old map file. Deleting it")
|
||||
try? fileManager.removeItem(atPath: destination.path)
|
||||
}
|
||||
|
||||
|
||||
do {
|
||||
try fileManager.copyItem(at: url, to: destination)
|
||||
} catch {
|
||||
print("Copy MB Tile file failed. Error: \(error)")
|
||||
}
|
||||
|
||||
|
||||
if fileManager.fileExists(atPath: destination.path) {
|
||||
print("ℹ️ Saved the map file")
|
||||
|
||||
|
||||
// need to tell the map view that it needs to update and try loading the new overlay
|
||||
UserDefaults.standard.set(Date().timeIntervalSince1970, forKey: "lastUpdatedLocalMapFile")
|
||||
|
||||
|
||||
} else {
|
||||
print("💥 Didn't save the map file")
|
||||
}
|
||||
|
|
@ -168,6 +167,6 @@ class AppState: ObservableObject {
|
|||
@Published var unreadDirectMessages: Int = 0
|
||||
@Published var unreadChannelMessages: Int = 0
|
||||
@Published var firmwareVersion: String = "0.0.0"
|
||||
//@Published var connectedNode: NodeInfoEntity?
|
||||
// @Published var connectedNode: NodeInfoEntity?
|
||||
@Published var navigationPath: String?
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,12 +8,12 @@
|
|||
import SwiftUI
|
||||
|
||||
class MeshtasticAppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate, ObservableObject {
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
|
||||
print("🚀 Meshtstic Apple App launched!")
|
||||
// Default User Default Values
|
||||
UserDefaults.standard.register(defaults: ["meshMapRecentering" : true])
|
||||
UserDefaults.standard.register(defaults: ["meshMapShowNodeHistory" : true])
|
||||
UserDefaults.standard.register(defaults: ["meshMapShowRouteLines" : true])
|
||||
UserDefaults.standard.register(defaults: ["meshMapRecentering": true])
|
||||
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
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ class PersistenceController {
|
|||
do {
|
||||
try persistentStoreCoordinator.destroyPersistentStore(at: url, ofType: NSSQLiteStoreType, options: nil)
|
||||
print("💥 CoreData database truncated. All app data has been erased.")
|
||||
|
||||
|
||||
do {
|
||||
try persistentStoreCoordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: url, options: nil)
|
||||
} catch let error {
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ public func getNodeInfo(id: Int64, context: NSManagedObjectContext) -> NodeInfoE
|
|||
}
|
||||
|
||||
public func getStoreAndForwardMessageIds(seconds: Int, context: NSManagedObjectContext) -> [UInt32] {
|
||||
|
||||
|
||||
let time = seconds * -1
|
||||
let fetchMessagesRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "MessageEntity")
|
||||
let timeRange = Calendar.current.date(byAdding: .minute, value: time, to: Date())
|
||||
|
|
|
|||
|
|
@ -110,12 +110,12 @@ public func clearCoreDataDatabase(context: NSManagedObjectContext, includeRoutes
|
|||
|
||||
let persistenceController = PersistenceController.shared.container
|
||||
for i in 0...persistenceController.managedObjectModel.entities.count-1 {
|
||||
|
||||
|
||||
let entity = persistenceController.managedObjectModel.entities[i]
|
||||
let query = NSFetchRequest<NSFetchRequestResult>(entityName: entity.name!)
|
||||
var deleteRequest = NSBatchDeleteRequest(fetchRequest: query)
|
||||
let entityName = entity.name ?? "UNK"
|
||||
|
||||
|
||||
if includeRoutes {
|
||||
deleteRequest = NSBatchDeleteRequest(fetchRequest: query)
|
||||
} else if !includeRoutes {
|
||||
|
|
@ -153,7 +153,7 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext)
|
|||
newNode.snr = packet.rxSnr
|
||||
newNode.rssi = packet.rxRssi
|
||||
newNode.viaMqtt = packet.viaMqtt
|
||||
|
||||
|
||||
if packet.to == 4294967295 || packet.to == UserDefaults.preferredPeripheralNum {
|
||||
newNode.channel = Int32(packet.channel)
|
||||
}
|
||||
|
|
@ -161,16 +161,16 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext)
|
|||
newNode.hopsAway = Int32(nodeInfoMessage.hopsAway)
|
||||
newNode.favorite = nodeInfoMessage.isFavorite
|
||||
}
|
||||
|
||||
|
||||
if let newUserMessage = try? User(serializedData: packet.decoded.payload) {
|
||||
|
||||
if newUserMessage.id.isEmpty {
|
||||
|
||||
if newUserMessage.id.isEmpty {
|
||||
if packet.from > Int16.max {
|
||||
let newUser = createUser(num: Int64(packet.from), context: context)
|
||||
newNode.user = newUser
|
||||
}
|
||||
} else {
|
||||
|
||||
|
||||
let newUser = UserEntity(context: context)
|
||||
newUser.userId = newUserMessage.id
|
||||
newUser.num = Int64(packet.from)
|
||||
|
|
@ -179,9 +179,8 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext)
|
|||
newUser.role = Int32(newUserMessage.role.rawValue)
|
||||
newUser.hwModel = String(describing: newUserMessage.hwModel).uppercased()
|
||||
newNode.user = newUser
|
||||
|
||||
|
||||
if (UserDefaults.newNodeNotifications){
|
||||
|
||||
if UserDefaults.newNodeNotifications {
|
||||
let manager = LocalNotificationManager()
|
||||
manager.notifications = [
|
||||
Notification(
|
||||
|
|
@ -202,7 +201,7 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext)
|
|||
fetchedNode[0].user = newUser
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if newNode.user == nil && packet.from > Int16.max {
|
||||
newNode.user = createUser(num: Int64(packet.from), context: context)
|
||||
}
|
||||
|
|
@ -219,7 +218,7 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext)
|
|||
print("💥 Error Inserting New Core Data MyInfoEntity: \(nsError)")
|
||||
}
|
||||
newNode.myInfo = myInfoEntity
|
||||
|
||||
|
||||
} else {
|
||||
// Update an existing node
|
||||
fetchedNode[0].id = Int64(packet.from)
|
||||
|
|
@ -260,7 +259,7 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext)
|
|||
} else if packet.hopStart != 0 && packet.hopLimit <= packet.hopStart {
|
||||
fetchedNode[0].hopsAway = Int32(packet.hopStart - packet.hopLimit)
|
||||
}
|
||||
if (fetchedNode[0].user == nil) {
|
||||
if fetchedNode[0].user == nil {
|
||||
let newUser = createUser(num: Int64(packet.from), context: context)
|
||||
fetchedNode[0].user! = newUser
|
||||
}
|
||||
|
|
@ -335,8 +334,7 @@ func upsertPositionPacket (packet: MeshPacket, context: NSManagedObjectContext)
|
|||
}
|
||||
/// Don't save nearly the same position over and over. If the next position is less than 10 meters from the new position, delete the previous position and save the new one.
|
||||
if mutablePositions.count > 0 && (position.precisionBits == 32 || position.precisionBits == 0) {
|
||||
let mostRecent = mutablePositions.lastObject as! PositionEntity
|
||||
if mostRecent.coordinate.distance(from: position.coordinate) < 15.0 {
|
||||
if let mostRecent = mutablePositions.lastObject as? PositionEntity, mostRecent.coordinate.distance(from: position.coordinate) < 15.0 {
|
||||
mutablePositions.remove(mostRecent)
|
||||
}
|
||||
} else if mutablePositions.count > 0 {
|
||||
|
|
@ -798,7 +796,7 @@ func upsertAmbientLightingModuleConfigPacket(config: Meshtastic.ModuleConfig.Amb
|
|||
fetchedNode[0].ambientLightingConfig = newAmbientLightingConfig
|
||||
|
||||
} else {
|
||||
|
||||
|
||||
if fetchedNode[0].ambientLightingConfig == nil {
|
||||
fetchedNode[0].ambientLightingConfig = AmbientLightingConfigEntity(context: context)
|
||||
}
|
||||
|
|
@ -1041,7 +1039,7 @@ func upsertPaxCounterModuleConfigPacket(config: Meshtastic.ModuleConfig.Paxcount
|
|||
let newPaxCounterConfig = PaxCounterConfigEntity(context: context)
|
||||
newPaxCounterConfig.enabled = config.enabled
|
||||
newPaxCounterConfig.paxcounterUpdateInterval = Int32(config.paxcounterUpdateInterval)
|
||||
|
||||
|
||||
fetchedNode[0].paxCounterConfig = newPaxCounterConfig
|
||||
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -33,11 +33,11 @@ struct ContactsTip: Tip {
|
|||
return "tip.messages.contacts"
|
||||
}
|
||||
var title: Text {
|
||||
//Text("tip.messages.contacts.title")
|
||||
// Text("tip.messages.contacts.title")
|
||||
Text("Contacts")
|
||||
}
|
||||
var message: Text? {
|
||||
//Text("tip.messages.contacts.message")
|
||||
// Text("tip.messages.contacts.message")
|
||||
Text("Each node is an available contact. Contacts with recent messages or marked as favorites show up at the top of the list. Select a contact to send or view messages. Long press to favorite or mute the contact or delete the conversation.")
|
||||
}
|
||||
var image: Image? {
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ struct Connect: View {
|
|||
Text("subscribed").font(.callout)
|
||||
.foregroundColor(.green)
|
||||
} else {
|
||||
|
||||
|
||||
HStack {
|
||||
if #available(iOS 17.0, macOS 14.0, *) {
|
||||
Image(systemName: "square.stack.3d.down.forward")
|
||||
|
|
@ -125,7 +125,7 @@ struct Connect: View {
|
|||
if !bleManager.sendShutdown(fromUser: node!.user!, toUser: node!.user!, adminIndex: node!.myInfo!.adminIndex) {
|
||||
print("Shutdown Failed")
|
||||
}
|
||||
|
||||
|
||||
} label: {
|
||||
Label("Power Off", systemImage: "power")
|
||||
}
|
||||
|
|
@ -233,7 +233,7 @@ struct Connect: View {
|
|||
bleManager.disconnectPeripheral()
|
||||
}
|
||||
clearCoreDataDatabase(context: context, includeRoutes: false)
|
||||
|
||||
|
||||
let radio = bleManager.peripherals.first(where: { $0.peripheral.identifier.uuidString == selectedPeripherialId })
|
||||
if radio != nil {
|
||||
bleManager.connectTo(peripheral: radio!.peripheral)
|
||||
|
|
@ -242,7 +242,7 @@ struct Connect: View {
|
|||
}
|
||||
.textCase(nil)
|
||||
}
|
||||
|
||||
|
||||
} else {
|
||||
Text("bluetooth.off")
|
||||
.foregroundColor(.red)
|
||||
|
|
@ -269,7 +269,7 @@ struct Connect: View {
|
|||
if bleManager.isConnecting {
|
||||
Button(role: .destructive, action: {
|
||||
bleManager.cancelPeripheralConnection()
|
||||
|
||||
|
||||
}) {
|
||||
Label("disconnect", systemImage: "antenna.radiowaves.left.and.right.slash")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,19 +56,19 @@ struct ContentView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
//#Preview {
|
||||
// #Preview {
|
||||
// if #available(iOS 17.0, *) {
|
||||
// // ContentView(deepLinkManager: .init())
|
||||
// } else {
|
||||
// // Fallback on earlier versions
|
||||
// }
|
||||
//}
|
||||
// }
|
||||
|
||||
//struct ContentView_Previews: PreviewProvider {
|
||||
// struct ContentView_Previews: PreviewProvider {
|
||||
// static var previews: some View {
|
||||
// ContentView()
|
||||
// }
|
||||
//}
|
||||
// }
|
||||
|
||||
enum Tab: Hashable {
|
||||
case contacts
|
||||
|
|
|
|||
|
|
@ -9,17 +9,17 @@ import SwiftUI
|
|||
import Charts
|
||||
|
||||
struct BatteryGauge: View {
|
||||
|
||||
|
||||
@ObservedObject var node: NodeInfoEntity
|
||||
private let minValue = 0.0
|
||||
private let maxValue = 100.00
|
||||
|
||||
var body: some View {
|
||||
|
||||
|
||||
let deviceMetrics = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0"))
|
||||
let mostRecent = deviceMetrics?.lastObject as? TelemetryEntity
|
||||
let batteryLevel = Double(mostRecent?.batteryLevel ?? 0)
|
||||
|
||||
|
||||
VStack {
|
||||
if batteryLevel > 100.0 {
|
||||
// Plugged in
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
import SwiftUI
|
||||
|
||||
struct BatteryLevelCompact: View {
|
||||
|
||||
|
||||
@ObservedObject var node: NodeInfoEntity
|
||||
|
||||
var font: Font
|
||||
|
|
@ -26,25 +26,25 @@ struct BatteryLevelCompact: View {
|
|||
.foregroundColor(color)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
} else if batteryLevel < 100 && batteryLevel > 74 {
|
||||
|
||||
|
||||
Image(systemName: "battery.75")
|
||||
.font(iconFont)
|
||||
.foregroundColor(color)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
} else if batteryLevel < 75 && batteryLevel > 49 {
|
||||
|
||||
|
||||
Image(systemName: "battery.50")
|
||||
.font(iconFont)
|
||||
.foregroundColor(color)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
} else if batteryLevel < 50 && batteryLevel > 14 {
|
||||
|
||||
|
||||
Image(systemName: "battery.25")
|
||||
.font(iconFont)
|
||||
.foregroundColor(color)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
} else if batteryLevel < 15 && batteryLevel > 0 {
|
||||
|
||||
|
||||
Image(systemName: "battery.0")
|
||||
.font(iconFont)
|
||||
.foregroundColor(color)
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ struct CircleText: View {
|
|||
var text: String
|
||||
var color: Color
|
||||
var circleSize: CGFloat = 45
|
||||
|
||||
|
||||
var body: some View {
|
||||
|
||||
ZStack {
|
||||
|
|
@ -49,8 +49,7 @@ struct CircleText_Previews: PreviewProvider {
|
|||
.previewLayout(.fixed(width: 300, height: 100))
|
||||
}
|
||||
HStack {
|
||||
|
||||
|
||||
|
||||
CircleText(text: "CW-A", color: Color.secondary)
|
||||
.previewLayout(.fixed(width: 300, height: 100))
|
||||
CircleText(text: "CW-A", color: Color.secondary, circleSize: 80)
|
||||
|
|
@ -61,7 +60,7 @@ struct CircleText_Previews: PreviewProvider {
|
|||
.previewLayout(.fixed(width: 300, height: 100))
|
||||
}
|
||||
HStack {
|
||||
|
||||
|
||||
CircleText(text: "🚗", color: Color.orange)
|
||||
.previewLayout(.fixed(width: 300, height: 100))
|
||||
CircleText(text: "🔋", color: Color.indigo, circleSize: 80)
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ A view draws the indicator used in the upper right corner for views using BLE
|
|||
|
||||
import SwiftUI
|
||||
|
||||
|
||||
struct ConnectedDevice: View {
|
||||
var bluetoothOn: Bool
|
||||
var deviceConnected: Bool
|
||||
|
|
@ -22,7 +21,7 @@ struct ConnectedDevice: View {
|
|||
if (phoneOnly && UIDevice.current.userInterfaceIdiom == .phone) || !phoneOnly {
|
||||
if bluetoothOn {
|
||||
if deviceConnected {
|
||||
if (mqttUplinkEnabled || mqttDownlinkEnabled) {
|
||||
if mqttUplinkEnabled || mqttDownlinkEnabled {
|
||||
MQTTIcon(connected: mqttProxyConnected, uplink: mqttUplinkEnabled, downlink: mqttDownlinkEnabled, topic: mqttTopic)
|
||||
}
|
||||
Image(systemName: "antenna.radiowaves.left.and.right.circle.fill")
|
||||
|
|
@ -44,12 +43,9 @@ struct ConnectedDevice: View {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
struct ConnectedDevice_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
VStack (alignment: .trailing) {
|
||||
VStack(alignment: .trailing) {
|
||||
ConnectedDevice(bluetoothOn: true, deviceConnected: true, name: "MEMO", mqttProxyConnected: true)
|
||||
ConnectedDevice(bluetoothOn: true, deviceConnected: true, name: "MEMO", mqttProxyConnected: false, mqttUplinkEnabled: true, mqttDownlinkEnabled: true)
|
||||
ConnectedDevice(bluetoothOn: true, deviceConnected: true, name: "MEMO", mqttProxyConnected: true, mqttUplinkEnabled: true, mqttDownlinkEnabled: true, mqttTopic: "msh/US/2/e/#")
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ struct DateTimeText: View {
|
|||
|
||||
let sixMonthsAgo = Calendar.current.date(byAdding: .month, value: -6, to: Date())
|
||||
let localeDateFormat = DateFormatter.dateFormat(fromTemplate: "yyMMddjmmssa", options: 0, locale: Locale.current)
|
||||
|
||||
|
||||
var body: some View {
|
||||
let dateFormatString = (localeDateFormat ?? "MM/dd/YY j:mm:ss a")
|
||||
|
||||
|
|
|
|||
|
|
@ -8,13 +8,13 @@ import SwiftUI
|
|||
struct LastHeardText: View {
|
||||
var lastHeard: Date?
|
||||
let sixMonthsAgo = Calendar.current.date(byAdding: .month, value: -6, to: Date())
|
||||
|
||||
|
||||
static let formatter: RelativeDateTimeFormatter = {
|
||||
let formatter = RelativeDateTimeFormatter()
|
||||
formatter.unitsStyle = .full
|
||||
return formatter
|
||||
}()
|
||||
|
||||
|
||||
var body: some View {
|
||||
if lastHeard != nil && lastHeard! >= sixMonthsAgo! {
|
||||
Text(lastHeard?.formatted() ?? "unknown.age".localized)
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@ struct LoRaSignalStrengthMeter: View {
|
|||
var preset: ModemPresets
|
||||
var compact: Bool
|
||||
var body: some View {
|
||||
|
||||
if (snr != 0.0 && rssi != 0) {
|
||||
|
||||
if snr != 0.0 && rssi != 0 {
|
||||
let signalStrength = getLoRaSignalStrength(snr: snr, rssi: rssi, preset: preset)
|
||||
let gradient = Gradient(colors: [.red, .orange, .yellow, .green])
|
||||
if !compact {
|
||||
|
|
|
|||
|
|
@ -18,8 +18,8 @@ struct MQTTIcon: View {
|
|||
|
||||
var body: some View {
|
||||
Button( action: {
|
||||
if(topic.length > 0) {self.isPopoverOpen.toggle()}
|
||||
} ) {
|
||||
if topic.length > 0 {self.isPopoverOpen.toggle()}
|
||||
}) {
|
||||
// the last one defaults to just showing up/down if it isn't specified b/c on the mqtt config screen, there's no information about uplink/downlink and no good alternative icon
|
||||
Image(systemName: uplink && downlink ? "arrow.up.arrow.down.circle.fill" : uplink ? "arrow.up.circle.fill" : downlink ? "arrow.down.circle.fill" : "arrow.up.arrow.down.circle.fill")
|
||||
.imageScale(.large)
|
||||
|
|
|
|||
|
|
@ -21,13 +21,13 @@ struct AirQualityIndex: View {
|
|||
var aqi: Int
|
||||
var displayMode: IaqDisplayMode = .pill
|
||||
let gradient = Gradient(colors: [.green, .yellow, .orange, .red, .purple, .magenta])
|
||||
|
||||
|
||||
var body: some View {
|
||||
|
||||
|
||||
let aqiEnum = Aqi.getAqi(for: aqi)
|
||||
switch displayMode {
|
||||
case .pill:
|
||||
ZStack (alignment: .leading) {
|
||||
ZStack(alignment: .leading) {
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.fill(aqiEnum.color)
|
||||
.frame(width: 125, height: 30)
|
||||
|
|
@ -48,7 +48,7 @@ struct AirQualityIndex: View {
|
|||
.font(.caption)
|
||||
case .gauge:
|
||||
Gauge(value: Double(aqi), in: 0...500) {
|
||||
|
||||
|
||||
Text("IAQ")
|
||||
.foregroundColor(aqiEnum.color)
|
||||
} currentValueLabel: {
|
||||
|
|
@ -115,19 +115,19 @@ struct AirQualityIndex_Previews: PreviewProvider {
|
|||
}
|
||||
Text(".gauge")
|
||||
.font(.title2)
|
||||
HStack (alignment: .top) {
|
||||
HStack(alignment: .top) {
|
||||
AirQualityIndex(aqi: 6, displayMode: .gauge)
|
||||
AirQualityIndex(aqi: 51, displayMode: .gauge)
|
||||
AirQualityIndex(aqi: 101, displayMode: .gauge)
|
||||
AirQualityIndex(aqi: 151, displayMode: .gauge)
|
||||
}
|
||||
HStack (alignment: .top) {
|
||||
HStack(alignment: .top) {
|
||||
AirQualityIndex(aqi: 201, displayMode: .gauge)
|
||||
AirQualityIndex(aqi: 251, displayMode: .gauge)
|
||||
AirQualityIndex(aqi: 301, displayMode: .gauge)
|
||||
AirQualityIndex(aqi: 351, displayMode: .gauge)
|
||||
}
|
||||
HStack (alignment: .top) {
|
||||
HStack(alignment: .top) {
|
||||
AirQualityIndex(aqi: 401, displayMode: .gauge)
|
||||
AirQualityIndex(aqi: 500, displayMode: .gauge)
|
||||
}
|
||||
|
|
@ -140,7 +140,7 @@ struct AirQualityIndex_Previews: PreviewProvider {
|
|||
AirQualityIndex(aqi: 351, displayMode: .gradient)
|
||||
AirQualityIndex(aqi: 401, displayMode: .gradient)
|
||||
AirQualityIndex(aqi: 500, displayMode: .gradient)
|
||||
|
||||
|
||||
}.previewLayout(.fixed(width: 300, height: 800))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import SwiftUI
|
|||
struct IAQScale: View {
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment:.leading) {
|
||||
VStack(alignment: .leading) {
|
||||
ForEach(Iaq.allCases) { iaq in
|
||||
HStack {
|
||||
RoundedRectangle(cornerRadius: 5)
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ struct IndoorAirQuality: View {
|
|||
let iaqEnum = Iaq.getIaq(for: iaq)
|
||||
switch displayMode {
|
||||
case .pill:
|
||||
ZStack (alignment: .leading) {
|
||||
ZStack(alignment: .leading) {
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.fill(iaqEnum.color)
|
||||
.frame(width: 125, height: 30)
|
||||
|
|
@ -49,7 +49,7 @@ struct IndoorAirQuality: View {
|
|||
.font(.caption)
|
||||
case .gauge:
|
||||
Gauge(value: Double(iaq), in: 0...500) {
|
||||
|
||||
|
||||
Text("IAQ")
|
||||
.foregroundColor(iaqEnum.color)
|
||||
} currentValueLabel: {
|
||||
|
|
@ -117,19 +117,19 @@ struct IndoorAirQuality_Previews: PreviewProvider {
|
|||
}
|
||||
Text(".gauge")
|
||||
.font(.title2)
|
||||
HStack (alignment: .top) {
|
||||
HStack(alignment: .top) {
|
||||
IndoorAirQuality(iaq: 6, displayMode: .gauge)
|
||||
IndoorAirQuality(iaq: 51, displayMode: .gauge)
|
||||
IndoorAirQuality(iaq: 101, displayMode: .gauge)
|
||||
IndoorAirQuality(iaq: 151, displayMode: .gauge)
|
||||
}
|
||||
HStack (alignment: .top) {
|
||||
HStack(alignment: .top) {
|
||||
IndoorAirQuality(iaq: 201, displayMode: .gauge)
|
||||
IndoorAirQuality(iaq: 251, displayMode: .gauge)
|
||||
IndoorAirQuality(iaq: 301, displayMode: .gauge)
|
||||
IndoorAirQuality(iaq: 351, displayMode: .gauge)
|
||||
}
|
||||
HStack (alignment: .top) {
|
||||
HStack(alignment: .top) {
|
||||
IndoorAirQuality(iaq: 401, displayMode: .gauge)
|
||||
IndoorAirQuality(iaq: 500, displayMode: .gauge)
|
||||
}
|
||||
|
|
@ -142,7 +142,7 @@ struct IndoorAirQuality_Previews: PreviewProvider {
|
|||
IndoorAirQuality(iaq: 351, displayMode: .gradient)
|
||||
IndoorAirQuality(iaq: 401, displayMode: .gradient)
|
||||
IndoorAirQuality(iaq: 500, displayMode: .gradient)
|
||||
|
||||
|
||||
}.previewLayout(.fixed(width: 300, height: 800))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ struct MapButtons: View {
|
|||
}
|
||||
|
||||
// MARK: Previews
|
||||
//struct MapControl_Previews: PreviewProvider {
|
||||
// struct MapControl_Previews: PreviewProvider {
|
||||
// @State static var tracking: UserTrackingModes = .none
|
||||
// @State static var isPresentingInfoSheet = false
|
||||
// static var previews: some View {
|
||||
|
|
@ -61,4 +61,4 @@ struct MapButtons: View {
|
|||
// }
|
||||
// .previewLayout(.fixed(width: 60, height: 100))
|
||||
// }
|
||||
//}
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ struct MapViewSwiftUI: UIViewRepresentable {
|
|||
mapView.showsBuildings = true
|
||||
mapView.showsScale = true
|
||||
mapView.showsTraffic = true
|
||||
|
||||
|
||||
mapView.showsCompass = false
|
||||
let compass = MKCompassButton(mapView: mapView)
|
||||
compass.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import MapKit
|
|||
import WeatherKit
|
||||
|
||||
struct NodeMapMapkit: View {
|
||||
|
||||
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@EnvironmentObject var bleManager: BLEManager
|
||||
/// Weather
|
||||
|
|
@ -21,7 +21,7 @@ struct NodeMapMapkit: View {
|
|||
@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
|
||||
|
|
@ -40,7 +40,7 @@ struct NodeMapMapkit: View {
|
|||
), animation: .none)
|
||||
private var waypoints: FetchedResults<WaypointEntity>
|
||||
@ObservedObject var node: NodeInfoEntity
|
||||
|
||||
|
||||
var body: some View {
|
||||
|
||||
let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral?.num ?? -1, context: context)
|
||||
|
|
@ -90,7 +90,7 @@ struct NodeMapMapkit: View {
|
|||
VStack {
|
||||
Label(temperature?.formatted(.measurement(width: .narrow)) ?? "??", systemImage: symbolName)
|
||||
.font(.caption)
|
||||
|
||||
|
||||
Label("\(humidity ?? 0)%", systemImage: "humidity")
|
||||
.font(.caption2)
|
||||
|
||||
|
|
@ -103,12 +103,12 @@ struct NodeMapMapkit: View {
|
|||
.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)
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import SwiftUI
|
|||
import CoreData
|
||||
|
||||
struct ChannelList: View {
|
||||
|
||||
|
||||
@StateObject var appState = AppState.shared
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@EnvironmentObject var bleManager: BLEManager
|
||||
|
|
@ -20,133 +20,139 @@ struct ChannelList: View {
|
|||
@State private var isPresentingDeleteChannelMessagesConfirm: Bool = false
|
||||
|
||||
@State private var isPresentingTraceRouteSentAlert = false
|
||||
|
||||
|
||||
var restrictedChannels = ["gpio", "mqtt", "serial"]
|
||||
|
||||
var body: some View {
|
||||
|
||||
@ViewBuilder
|
||||
private func makeNavigationLink(
|
||||
myInfo: MyInfoEntity,
|
||||
channel: ChannelEntity
|
||||
) -> some View {
|
||||
let localeDateFormat = DateFormatter.dateFormat(fromTemplate: "yyMMdd", options: 0, locale: Locale.current)
|
||||
let dateFormatString = (localeDateFormat ?? "MM/dd/YY")
|
||||
|
||||
|
||||
NavigationLink(destination: ChannelMessageList(myInfo: myInfo, channel: channel)) {
|
||||
let mostRecent = channel.allPrivateMessages.last(where: { $0.channel == channel.index })
|
||||
let lastMessageTime = Date(timeIntervalSince1970: TimeInterval(Int64((mostRecent?.messageTimestamp ?? 0 ))))
|
||||
let lastMessageDay = Calendar.current.dateComponents([.day], from: lastMessageTime).day ?? 0
|
||||
let currentDay = Calendar.current.dateComponents([.day], from: Date()).day ?? 0
|
||||
|
||||
ZStack {
|
||||
Image(systemName: "circle.fill")
|
||||
.opacity(channel.unreadMessages > 0 ? 1 : 0)
|
||||
.font(.system(size: 10))
|
||||
.foregroundColor(.accentColor)
|
||||
.brightness(0.2)
|
||||
}
|
||||
CircleText(text: String(channel.index), color: .accentColor)
|
||||
.brightness(0.2)
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
HStack {
|
||||
if channel.name?.isEmpty ?? false {
|
||||
if channel.role == 1 {
|
||||
Text(String("PrimaryChannel").camelCaseToWords())
|
||||
.font(.headline)
|
||||
} else {
|
||||
Text(String("Channel \(channel.index)").camelCaseToWords())
|
||||
.font(.headline)
|
||||
}
|
||||
} else {
|
||||
Text(String(channel.name ?? "Channel \(channel.index)").camelCaseToWords())
|
||||
.font(.headline)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
if channel.allPrivateMessages.count > 0 {
|
||||
|
||||
if lastMessageDay == currentDay {
|
||||
Text(lastMessageTime, style: .time )
|
||||
.font(.footnote)
|
||||
.foregroundColor(.secondary)
|
||||
} else if lastMessageDay == (currentDay - 1) {
|
||||
Text("Yesterday")
|
||||
.font(.footnote)
|
||||
.foregroundColor(.secondary)
|
||||
} else if lastMessageDay < (currentDay - 1) && lastMessageDay > (currentDay - 5) {
|
||||
Text(lastMessageTime.formattedDate(format: dateFormatString))
|
||||
.font(.footnote)
|
||||
.foregroundColor(.secondary)
|
||||
} else if lastMessageDay < (currentDay - 1800) {
|
||||
Text(lastMessageTime.formattedDate(format: dateFormatString))
|
||||
.font(.footnote)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if channel.allPrivateMessages.count > 0 {
|
||||
HStack(alignment: .top) {
|
||||
Text("\(mostRecent != nil ? mostRecent!.messagePayload! : " ")")
|
||||
// .font(.system(size: 16))
|
||||
.font(.footnote)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
// Display Contacts for the rest of the non admin channels
|
||||
if node != nil && node!.myInfo != nil && node!.myInfo!.channels != nil {
|
||||
List(node!.myInfo!.channels!.array as! [ChannelEntity], id: \.self, selection: $channelSelection) { (channel: ChannelEntity) in
|
||||
if let node, let myInfo = node.myInfo, let channels = myInfo.channels?.array as? [ChannelEntity] {
|
||||
List(channels, id: \.self, selection: $channelSelection) { (channel: ChannelEntity) in
|
||||
if !restrictedChannels.contains(channel.name?.lowercased() ?? "") {
|
||||
|
||||
NavigationLink(destination: ChannelMessageList(myInfo: node!.myInfo!, channel: channel)) {
|
||||
|
||||
let mostRecent = channel.allPrivateMessages.last(where: { $0.channel == channel.index })
|
||||
let lastMessageTime = Date(timeIntervalSince1970: TimeInterval(Int64((mostRecent?.messageTimestamp ?? 0 ))))
|
||||
let lastMessageDay = Calendar.current.dateComponents([.day], from: lastMessageTime).day ?? 0
|
||||
let currentDay = Calendar.current.dateComponents([.day], from: Date()).day ?? 0
|
||||
|
||||
ZStack {
|
||||
Image(systemName: "circle.fill")
|
||||
.opacity(channel.unreadMessages > 0 ? 1 : 0)
|
||||
.font(.system(size: 10))
|
||||
.foregroundColor(.accentColor)
|
||||
.brightness(0.2)
|
||||
}
|
||||
CircleText(text: String(channel.index), color: .accentColor)
|
||||
.brightness(0.2)
|
||||
|
||||
VStack(alignment: .leading){
|
||||
HStack{
|
||||
if channel.name?.isEmpty ?? false {
|
||||
if channel.role == 1 {
|
||||
Text(String("PrimaryChannel").camelCaseToWords())
|
||||
.font(.headline)
|
||||
} else {
|
||||
Text(String("Channel \(channel.index)").camelCaseToWords())
|
||||
.font(.headline)
|
||||
}
|
||||
} else {
|
||||
Text(String(channel.name ?? "Channel \(channel.index)").camelCaseToWords())
|
||||
.font(.headline)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
if channel.allPrivateMessages.count > 0 {
|
||||
|
||||
if lastMessageDay == currentDay {
|
||||
Text(lastMessageTime, style: .time )
|
||||
.font(.footnote)
|
||||
.foregroundColor(.secondary)
|
||||
} else if lastMessageDay == (currentDay - 1) {
|
||||
Text("Yesterday")
|
||||
.font(.footnote)
|
||||
.foregroundColor(.secondary)
|
||||
} else if lastMessageDay < (currentDay - 1) && lastMessageDay > (currentDay - 5) {
|
||||
Text(lastMessageTime.formattedDate(format: dateFormatString))
|
||||
.font(.footnote)
|
||||
.foregroundColor(.secondary)
|
||||
} else if lastMessageDay < (currentDay - 1800) {
|
||||
Text(lastMessageTime.formattedDate(format: dateFormatString))
|
||||
.font(.footnote)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
makeNavigationLink(myInfo: myInfo, channel: channel)
|
||||
.frame(height: 62)
|
||||
.contextMenu {
|
||||
if channel.allPrivateMessages.count > 0 {
|
||||
HStack(alignment: .top) {
|
||||
Text("\(mostRecent != nil ? mostRecent!.messagePayload! : " ")")
|
||||
//.font(.system(size: 16))
|
||||
.font(.footnote)
|
||||
.foregroundColor(.secondary)
|
||||
Button(role: .destructive) {
|
||||
isPresentingDeleteChannelMessagesConfirm = true
|
||||
channelSelection = channel
|
||||
} label: {
|
||||
Label("Delete Messages", systemImage: "trash")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(height: 62)
|
||||
.contextMenu {
|
||||
if channel.allPrivateMessages.count > 0 {
|
||||
Button(role: .destructive) {
|
||||
isPresentingDeleteChannelMessagesConfirm = true
|
||||
channelSelection = channel
|
||||
Button {
|
||||
channel.mute = !channel.mute
|
||||
|
||||
do {
|
||||
let adminMessageId = bleManager.saveChannel(channel: channel.protoBuf, fromUser: node.user!, toUser: node.user!)
|
||||
if adminMessageId > 0 {
|
||||
context.refresh(channel, mergeChanges: true)
|
||||
}
|
||||
|
||||
try context.save()
|
||||
|
||||
} catch {
|
||||
context.rollback()
|
||||
print("💥 Save Channel Mute Error")
|
||||
}
|
||||
} label: {
|
||||
Label("Delete Messages", systemImage: "trash")
|
||||
Label(channel.mute ? "Show Alerts" : "Hide Alerts", systemImage: channel.mute ? "bell" : "bell.slash")
|
||||
}
|
||||
}
|
||||
Button {
|
||||
channel.mute = !channel.mute
|
||||
|
||||
do {
|
||||
let adminMessageId = bleManager.saveChannel(channel: channel.protoBuf, fromUser: node!.user!, toUser: node!.user!)
|
||||
if adminMessageId > 0 {
|
||||
context.refresh(channel, mergeChanges: true)
|
||||
}
|
||||
|
||||
try context.save()
|
||||
|
||||
} catch {
|
||||
context.rollback()
|
||||
print("💥 Save Channel Mute Error")
|
||||
.confirmationDialog(
|
||||
"This conversation will be deleted.",
|
||||
isPresented: $isPresentingDeleteChannelMessagesConfirm,
|
||||
titleVisibility: .visible
|
||||
) {
|
||||
Button(role: .destructive) {
|
||||
deleteChannelMessages(channel: channelSelection!, context: context)
|
||||
context.refresh(myInfo, mergeChanges: true)
|
||||
UIApplication.shared.applicationIconBadgeNumber = appState.unreadChannelMessages + appState.unreadDirectMessages
|
||||
channelSelection = nil
|
||||
} label: {
|
||||
Text("delete")
|
||||
}
|
||||
} label: {
|
||||
Label(channel.mute ? "Show Alerts" : "Hide Alerts", systemImage: channel.mute ? "bell" : "bell.slash")
|
||||
}
|
||||
}
|
||||
.confirmationDialog(
|
||||
"This conversation will be deleted.",
|
||||
isPresented: $isPresentingDeleteChannelMessagesConfirm,
|
||||
titleVisibility: .visible
|
||||
) {
|
||||
Button(role: .destructive) {
|
||||
deleteChannelMessages(channel: channelSelection!, context: context)
|
||||
context.refresh(node!.myInfo!, mergeChanges: true)
|
||||
UIApplication.shared.applicationIconBadgeNumber = appState.unreadChannelMessages + appState.unreadDirectMessages
|
||||
channelSelection = nil
|
||||
} label: {
|
||||
Text("delete")
|
||||
.onAppear {
|
||||
if self.bleManager.context == nil {
|
||||
self.bleManager.context = context
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
if self.bleManager.context == nil {
|
||||
self.bleManager.context = context
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding([.top, .bottom])
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ struct ChannelMessageList: View {
|
|||
.foregroundColor(.gray)
|
||||
.offset(y: 8)
|
||||
}
|
||||
|
||||
|
||||
HStack {
|
||||
MessageText(
|
||||
message: message,
|
||||
|
|
@ -75,13 +75,13 @@ struct ChannelMessageList: View {
|
|||
RetryButton(message: message, destination: .channel(channel))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
TapbackResponses(message: message) {
|
||||
appState.unreadChannelMessages = myInfo.unreadMessages
|
||||
UIApplication.shared.applicationIconBadgeNumber = appState.unreadChannelMessages + appState.unreadDirectMessages
|
||||
context.refresh(myInfo, mergeChanges: true)
|
||||
}
|
||||
|
||||
|
||||
HStack {
|
||||
if currentUser && message.receivedACK {
|
||||
// Ack Received
|
||||
|
|
@ -142,7 +142,7 @@ struct ChannelMessageList: View {
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
TextMessageField(
|
||||
destination: .channel(channel),
|
||||
replyMessageId: $replyMessageId,
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import CoreData
|
|||
struct MessageContextMenuItems: View {
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@EnvironmentObject var bleManager: BLEManager
|
||||
|
||||
|
||||
let message: MessageEntity
|
||||
let tapBackDestination: MessageDestination
|
||||
let isCurrentUser: Bool
|
||||
|
|
@ -47,7 +47,7 @@ struct MessageContextMenuItems: View {
|
|||
Text("copy")
|
||||
Image(systemName: "doc.on.doc")
|
||||
}
|
||||
|
||||
|
||||
Menu("message.details") {
|
||||
VStack {
|
||||
let messageDate = Date(timeIntervalSince1970: TimeInterval(message.messageTimestamp))
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ struct MessageText: View {
|
|||
static let dateFormatString = (localeDateFormat ?? "MM/dd/YY j:mm:ss:a")
|
||||
|
||||
@Environment(\.managedObjectContext) var context
|
||||
|
||||
|
||||
let message: MessageEntity
|
||||
let tapBackDestination: MessageDestination
|
||||
let isCurrentUser: Bool
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import TipKit
|
|||
#endif
|
||||
|
||||
struct Messages: View {
|
||||
|
||||
|
||||
@StateObject var appState = AppState.shared
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@EnvironmentObject var bleManager: BLEManager
|
||||
|
|
@ -20,9 +20,9 @@ struct Messages: View {
|
|||
@State var node: NodeInfoEntity?
|
||||
@State private var userSelection: UserEntity? // Nothing selected by default.
|
||||
@State private var channelSelection: ChannelEntity? // Nothing selected by default.
|
||||
|
||||
|
||||
@State private var columnVisibility = NavigationSplitViewVisibility.all
|
||||
|
||||
|
||||
enum MessagesSidebar {
|
||||
case groupMessages
|
||||
case directMessages
|
||||
|
|
@ -67,9 +67,9 @@ struct Messages: View {
|
|||
.navigationBarTitleDisplayMode(.large)
|
||||
.navigationBarItems(leading: MeshtasticLogo())
|
||||
.onChange(of: (appState.navigationPath)) { newPath in
|
||||
|
||||
if ((newPath?.hasPrefix("meshtastic://messages")) != nil) {
|
||||
|
||||
|
||||
if (newPath?.hasPrefix("meshtastic://messages")) != nil {
|
||||
|
||||
if let urlComponent = URLComponents(string: newPath ?? "") {
|
||||
let queryItems = urlComponent.queryItems
|
||||
let messageId = queryItems?.first(where: { $0.name == "messageId" })?.value
|
||||
|
|
@ -77,8 +77,7 @@ struct Messages: View {
|
|||
|
||||
if channel == nil {
|
||||
print("Channel not found")
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
print("Channel \(channel)")
|
||||
// selectedNode = nodes.first(where: { $0.num == Int64(nodeNum ?? "-1") })
|
||||
// AppState.shared.navigationPath = nil
|
||||
|
|
@ -106,7 +105,7 @@ struct Messages: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
} content: {
|
||||
|
||||
} detail: {
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@ import SwiftUI
|
|||
|
||||
struct TapbackResponses: View {
|
||||
@Environment(\.managedObjectContext) var context
|
||||
|
||||
|
||||
let message: MessageEntity
|
||||
let onRead: () -> Void
|
||||
let onRead: () -> Void
|
||||
|
||||
@ViewBuilder
|
||||
var body: some View {
|
||||
|
|
|
|||
|
|
@ -3,12 +3,12 @@ import SwiftUI
|
|||
struct TextMessageField: View {
|
||||
static let maxbytes = 228
|
||||
@EnvironmentObject var bleManager: BLEManager
|
||||
|
||||
|
||||
let destination: MessageDestination
|
||||
@Binding var replyMessageId: Int64
|
||||
@FocusState.Binding var isFocused: Bool
|
||||
let onSubmit: () -> Void
|
||||
|
||||
|
||||
@State private var typingMessage: String = ""
|
||||
@State private var totalBytes = 0
|
||||
@State private var sendPositionWithMessage = false
|
||||
|
|
@ -25,7 +25,7 @@ struct TextMessageField: View {
|
|||
TextMessageSize(maxbytes: Self.maxbytes, totalBytes: totalBytes).padding(.trailing)
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
HStack(alignment: .top) {
|
||||
ZStack {
|
||||
TextField("message", text: $typingMessage, axis: .vertical)
|
||||
|
|
@ -80,13 +80,13 @@ struct TextMessageField: View {
|
|||
}
|
||||
.padding(.all, 15)
|
||||
}
|
||||
|
||||
|
||||
private func requestPosition() {
|
||||
let userLongName = bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.longName : "Unknown"
|
||||
sendPositionWithMessage = true
|
||||
typingMessage = "📍 " + userLongName + " \(destination.positionShareMessage)."
|
||||
}
|
||||
|
||||
|
||||
private func sendMessage() {
|
||||
let messageSent = bleManager.sendMessage(
|
||||
message: typingMessage,
|
||||
|
|
@ -121,7 +121,7 @@ private extension MessageDestination {
|
|||
case .channel: return "has shared their position with you"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var positionDestNum: Int64 {
|
||||
switch self {
|
||||
case let .user(user): return user.num
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import SwiftUI
|
|||
struct TextMessageSize: View {
|
||||
let maxbytes: Int
|
||||
let totalBytes: Int
|
||||
|
||||
|
||||
var body: some View {
|
||||
ProgressView("\("bytes".localized): \(totalBytes) / \(maxbytes)", value: Double(totalBytes), total: Double(maxbytes))
|
||||
.frame(width: 130)
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import TipKit
|
|||
#endif
|
||||
|
||||
struct UserList: View {
|
||||
|
||||
|
||||
@StateObject var appState = AppState.shared
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@EnvironmentObject var bleManager: BLEManager
|
||||
|
|
@ -26,7 +26,7 @@ struct UserList: View {
|
|||
@State private var hopsAway: Int = -1
|
||||
@State private var deviceRole: Int = -1
|
||||
@State var isEditingFilters = false
|
||||
|
||||
|
||||
@FetchRequest(
|
||||
sortDescriptors: [NSSortDescriptor(key: "lastMessage", ascending: false),
|
||||
NSSortDescriptor(key: "userNode.favorite", ascending: false),
|
||||
|
|
@ -38,7 +38,7 @@ struct UserList: View {
|
|||
@State var selectedUserNum: Int64?
|
||||
@State private var userSelection: UserEntity? // Nothing selected by default.
|
||||
@State private var isPresentingDeleteUserMessagesConfirm: Bool = false
|
||||
|
||||
|
||||
var body: some View {
|
||||
let localeDateFormat = DateFormatter.dateFormat(fromTemplate: "yyMMdd", options: 0, locale: Locale.current)
|
||||
let dateFormatString = (localeDateFormat ?? "MM/dd/YY")
|
||||
|
|
@ -61,15 +61,15 @@ struct UserList: View {
|
|||
.foregroundColor(.accentColor)
|
||||
.brightness(0.2)
|
||||
}
|
||||
|
||||
|
||||
CircleText(text: user.shortName ?? "?", color: Color(UIColor(hex: UInt32(user.num))))
|
||||
|
||||
VStack(alignment: .leading){
|
||||
HStack{
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
HStack {
|
||||
Text(user.longName ?? "unknown".localized)
|
||||
.font(.headline)
|
||||
Spacer()
|
||||
if (user.userNode?.favorite ?? false) {
|
||||
if user.userNode?.favorite ?? false {
|
||||
Image(systemName: "star.fill")
|
||||
.foregroundColor(.yellow)
|
||||
}
|
||||
|
|
@ -93,7 +93,7 @@ struct UserList: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if user.messageList.count > 0 {
|
||||
HStack(alignment: .top) {
|
||||
Text("\(mostRecent != nil ? mostRecent!.messagePayload! : " ")")
|
||||
|
|
@ -106,7 +106,7 @@ struct UserList: View {
|
|||
.frame(height: 62)
|
||||
.contextMenu {
|
||||
Button {
|
||||
|
||||
|
||||
if node != nil && !(user.userNode?.favorite ?? false) {
|
||||
let success = bleManager.setFavoriteNode(node: user.userNode!, connectedNodeNum: Int64(node!.num))
|
||||
if success {
|
||||
|
|
@ -238,9 +238,9 @@ struct UserList: View {
|
|||
.scrollDismissesKeyboard(.immediately)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func searchUserList() {
|
||||
|
||||
|
||||
/// Case Insensitive Search Text Predicates
|
||||
let searchPredicates = ["userId", "numString", "hwModel", "longName", "shortName"].map { property in
|
||||
return NSPredicate(format: "%K CONTAINS[c] %@", property, searchText)
|
||||
|
|
@ -269,7 +269,7 @@ struct UserList: View {
|
|||
let hopsAwayPredicate = NSPredicate(format: "userNode.hopsAway == %i", Int32(hopsAway))
|
||||
predicates.append(hopsAwayPredicate)
|
||||
}
|
||||
|
||||
|
||||
/// Online
|
||||
if isOnline {
|
||||
let isOnlinePredicate = NSPredicate(format: "userNode.lastHeard >= %@", Calendar.current.date(byAdding: .minute, value: -15, to: Date())! as NSDate)
|
||||
|
|
@ -283,22 +283,22 @@ struct UserList: View {
|
|||
/// Distance
|
||||
if distanceFilter {
|
||||
let pointOfInterest = LocationHelper.currentLocation
|
||||
|
||||
|
||||
if pointOfInterest.latitude != LocationHelper.DefaultLocation.latitude && pointOfInterest.longitude != LocationHelper.DefaultLocation.longitude {
|
||||
let D: Double = maxDistance * 1.1
|
||||
let R: Double = 6371009
|
||||
let d: Double = maxDistance * 1.1
|
||||
let r: Double = 6371009
|
||||
let meanLatitidue = pointOfInterest.latitude * .pi / 180
|
||||
let deltaLatitude = D / R * 180 / .pi
|
||||
let deltaLongitude = D / (R * cos(meanLatitidue)) * 180 / .pi
|
||||
let deltaLatitude = d / r * 180 / .pi
|
||||
let deltaLongitude = d / (r * cos(meanLatitidue)) * 180 / .pi
|
||||
let minLatitude: Double = pointOfInterest.latitude - deltaLatitude
|
||||
let maxLatitude: Double = pointOfInterest.latitude + deltaLatitude
|
||||
let minLongitude: Double = pointOfInterest.longitude - deltaLongitude
|
||||
let maxLongitude: Double = pointOfInterest.longitude + deltaLongitude
|
||||
let distancePredicate = NSPredicate(format: "(SUBQUERY(userNode.positions, $position, $position.latest == TRUE && (%lf <= ($position.longitudeI / 1e7)) AND (($position.longitudeI / 1e7) <= %lf) AND (%lf <= ($position.latitudeI / 1e7)) AND (($position.latitudeI / 1e7) <= %lf))).@count > 0", minLongitude, maxLongitude,minLatitude, maxLatitude)
|
||||
let distancePredicate = NSPredicate(format: "(SUBQUERY(userNode.positions, $position, $position.latest == TRUE && (%lf <= ($position.longitudeI / 1e7)) AND (($position.longitudeI / 1e7) <= %lf) AND (%lf <= ($position.latitudeI / 1e7)) AND (($position.latitudeI / 1e7) <= %lf))).@count > 0", minLongitude, maxLongitude, minLatitude, maxLatitude)
|
||||
predicates.append(distancePredicate)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if predicates.count > 0 || !searchText.isEmpty {
|
||||
if !searchText.isEmpty {
|
||||
let filterPredicates = NSCompoundPredicate(type: .and, subpredicates: predicates)
|
||||
|
|
|
|||
|
|
@ -113,7 +113,7 @@ struct EnvironmentMetricsLog: View {
|
|||
GridItem(spacing: 0)
|
||||
]
|
||||
LazyVGrid(columns: columns, alignment: .leading, spacing: 1, pinnedViews: [.sectionHeaders]) {
|
||||
|
||||
|
||||
GridRow {
|
||||
Text("Temp")
|
||||
.font(.caption)
|
||||
|
|
@ -132,9 +132,9 @@ struct EnvironmentMetricsLog: View {
|
|||
.fontWeight(.bold)
|
||||
}
|
||||
ForEach(environmentMetrics, id: \.self) { em in
|
||||
|
||||
|
||||
GridRow {
|
||||
|
||||
|
||||
Text(em.temperature.formattedTemperature())
|
||||
.font(.caption)
|
||||
Text("\(String(format: "%.0f", em.relativeHumidity))%")
|
||||
|
|
@ -154,7 +154,7 @@ struct EnvironmentMetricsLog: View {
|
|||
}
|
||||
}
|
||||
HStack {
|
||||
|
||||
|
||||
Button(role: .destructive) {
|
||||
isPresentingClearLogConfirm = true
|
||||
} label: {
|
||||
|
|
@ -188,7 +188,7 @@ struct EnvironmentMetricsLog: View {
|
|||
.padding(.bottom)
|
||||
.padding(.trailing)
|
||||
}
|
||||
|
||||
|
||||
} else {
|
||||
if #available (iOS 17, *) {
|
||||
ContentUnavailableView("No Environment Metrics", systemImage: "slash.circle")
|
||||
|
|
@ -197,7 +197,7 @@ struct EnvironmentMetricsLog: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.navigationTitle("Environment Metrics Log")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationBarItems(trailing:
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import MapKit
|
|||
|
||||
@available(iOS 17.0, macOS 14.0, *)
|
||||
struct MeshMapContent: MapContent {
|
||||
|
||||
|
||||
@StateObject var appState = AppState.shared
|
||||
/// Parameters
|
||||
@Binding var showUserLocation: Bool
|
||||
|
|
@ -24,13 +24,13 @@ struct MeshMapContent: MapContent {
|
|||
@Binding var selectedPosition: PositionEntity?
|
||||
@AppStorage("enableMapWaypoints") private var showWaypoints = false
|
||||
@Binding var selectedWaypoint: WaypointEntity?
|
||||
|
||||
|
||||
@FetchRequest(fetchRequest: PositionEntity.allPositionsFetchRequest(), animation: .easeIn)
|
||||
var positions: FetchedResults<PositionEntity>
|
||||
|
||||
|
||||
@FetchRequest(fetchRequest: WaypointEntity.allWaypointssFetchRequest(), animation: .none)
|
||||
var waypoints: FetchedResults<WaypointEntity>
|
||||
|
||||
|
||||
@FetchRequest(sortDescriptors: [NSSortDescriptor(key: "name", ascending: true)],
|
||||
predicate: NSPredicate(format: "enabled == true", ""), animation: .none)
|
||||
private var routes: FetchedResults<RouteEntity>
|
||||
|
|
@ -39,26 +39,13 @@ struct MeshMapContent: MapContent {
|
|||
@State private var scale: CGFloat = 0.5
|
||||
|
||||
@MapContentBuilder
|
||||
var meshMap: some MapContent {
|
||||
let loraNodes = positions.filter { $0.nodePosition?.viaMqtt ?? true == false }
|
||||
let loraCoords = Array(loraNodes).compactMap({(position) -> CLLocationCoordinate2D in
|
||||
return position.nodeCoordinate ?? LocationsHandler.DefaultLocation
|
||||
})
|
||||
/// Convex Hull
|
||||
if showConvexHull {
|
||||
if loraCoords.count > 0 {
|
||||
let hull = loraCoords.getConvexHull()
|
||||
MapPolygon(coordinates: hull)
|
||||
.stroke(.blue, lineWidth: 3)
|
||||
.foregroundStyle(.indigo.opacity(0.4))
|
||||
}
|
||||
}
|
||||
/// Position Annotations
|
||||
ForEach(Array(positions), id: \.id) { position in
|
||||
var positionAnnotations: some MapContent {
|
||||
ForEach(positions, id: \.id) { position in
|
||||
/// Node color from node.num
|
||||
let nodeColor = UIColor(hex: UInt32(position.nodePosition?.num ?? 0))
|
||||
let positionName = position.nodePosition?.user?.longName ?? "?"
|
||||
/// Latest Position Anotations
|
||||
Annotation(position.nodePosition?.user?.longName ?? "?", coordinate: position.coordinate) {
|
||||
Annotation(positionName, coordinate: position.coordinate) {
|
||||
LazyVStack {
|
||||
ZStack {
|
||||
let nodeColor = UIColor(hex: UInt32(position.nodePosition?.num ?? 0))
|
||||
|
|
@ -89,16 +76,14 @@ struct MeshMapContent: MapContent {
|
|||
}
|
||||
}
|
||||
}
|
||||
.onTapGesture { location in
|
||||
selectedPosition = (selectedPosition == position ? nil : position)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// Node History and Route Lines for favorites
|
||||
if position.nodePosition?.favorite ?? false {
|
||||
if let nodePosition = position.nodePosition,
|
||||
nodePosition.favorite,
|
||||
let positions = nodePosition.positions,
|
||||
let nodePositions = Array(positions) as? [PositionEntity] {
|
||||
if showRouteLines {
|
||||
let nodePositions = Array(position.nodePosition!.positions!) as! [PositionEntity]
|
||||
let routeCoords = nodePositions.compactMap({(pos) -> CLLocationCoordinate2D in
|
||||
return pos.nodeCoordinate ?? LocationHelper.DefaultLocation
|
||||
})
|
||||
|
|
@ -114,7 +99,7 @@ struct MeshMapContent: MapContent {
|
|||
.stroke(gradient, style: dashed)
|
||||
}
|
||||
if showNodeHistory {
|
||||
ForEach(Array(position.nodePosition!.positions!) as! [PositionEntity], id: \.self) { (mappin: PositionEntity) in
|
||||
ForEach(nodePositions, id: \.self) { (mappin: PositionEntity) in
|
||||
if mappin.latest == false && mappin.nodePosition?.favorite ?? false {
|
||||
let pf = PositionFlags(rawValue: Int(mappin.nodePosition?.metadata?.positionFlags ?? 771))
|
||||
let headingDegrees = Angle.degrees(Double(mappin.heading))
|
||||
|
|
@ -129,11 +114,11 @@ struct MeshMapContent: MapContent {
|
|||
.clipShape(Circle())
|
||||
.rotationEffect(headingDegrees)
|
||||
.frame(width: 16, height: 16)
|
||||
|
||||
|
||||
} else {
|
||||
Circle()
|
||||
.fill(Color(UIColor(hex: UInt32(mappin.nodePosition?.num ?? 0))))
|
||||
.strokeBorder(Color(UIColor(hex: UInt32(mappin.nodePosition?.num ?? 0))).isLight() ? .black : .white ,lineWidth: 2)
|
||||
.strokeBorder(Color(UIColor(hex: UInt32(mappin.nodePosition?.num ?? 0))).isLight() ? .black : .white, lineWidth: 2)
|
||||
.frame(width: 12, height: 12)
|
||||
}
|
||||
}
|
||||
|
|
@ -147,19 +132,23 @@ struct MeshMapContent: MapContent {
|
|||
/// Reduced Precision Map Circles
|
||||
if 10...19 ~= position.precisionBits {
|
||||
let pp = PositionPrecision(rawValue: Int(position.precisionBits))
|
||||
let radius : CLLocationDistance = pp?.precisionMeters ?? 0
|
||||
let radius: CLLocationDistance = pp?.precisionMeters ?? 0
|
||||
if radius > 0.0 {
|
||||
MapCircle(center: position.coordinate, radius: radius)
|
||||
.foregroundStyle(Color(nodeColor).opacity(0.25))
|
||||
.stroke(.white, lineWidth: 2)
|
||||
}
|
||||
}
|
||||
/// Routes
|
||||
ForEach(Array(routes)) { route in
|
||||
let routeLocations = Array(route.locations!) as! [LocationEntity]
|
||||
let routeCoords = routeLocations.compactMap({(loc) -> CLLocationCoordinate2D in
|
||||
}
|
||||
}
|
||||
|
||||
@MapContentBuilder
|
||||
var routeAnnotations: some MapContent {
|
||||
ForEach(routes) { route in
|
||||
if let routeLocations = route.locations, let locations = Array(routeLocations) as? [LocationEntity] {
|
||||
let routeCoords = locations.compactMap {(loc) -> CLLocationCoordinate2D in
|
||||
return loc.locationCoordinate ?? LocationHelper.DefaultLocation
|
||||
})
|
||||
}
|
||||
Annotation("Start", coordinate: routeCoords.first ?? LocationHelper.DefaultLocation) {
|
||||
ZStack {
|
||||
Circle()
|
||||
|
|
@ -184,18 +173,19 @@ struct MeshMapContent: MapContent {
|
|||
)
|
||||
MapPolyline(coordinates: routeCoords)
|
||||
.stroke(Color(UIColor(hex: UInt32(route.color))), style: solid)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/// Waypoint Annotations
|
||||
if waypoints.count > 0 && showWaypoints {
|
||||
ForEach(Array(waypoints) as! [WaypointEntity], id: \.self) { waypoint in
|
||||
}
|
||||
|
||||
@MapContentBuilder
|
||||
var waypointAnnotations: some MapContent {
|
||||
if waypoints.count > 0, showWaypoints, let waypoints = Array(waypoints) as? [WaypointEntity] {
|
||||
ForEach(waypoints, id: \.self) { waypoint in
|
||||
Annotation(waypoint.name ?? "?", coordinate: waypoint.coordinate) {
|
||||
LazyVStack {
|
||||
ZStack {
|
||||
CircleText(text: String(UnicodeScalar(Int(waypoint.icon)) ?? "📍"), color: Color.orange, circleSize: 40)
|
||||
.onTapGesture(perform: { location in
|
||||
.onTapGesture(perform: { _ in
|
||||
selectedWaypoint = (selectedWaypoint == waypoint ? nil : waypoint)
|
||||
})
|
||||
}
|
||||
|
|
@ -204,7 +194,27 @@ struct MeshMapContent: MapContent {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@MapContentBuilder
|
||||
var meshMap: some MapContent {
|
||||
let loraNodes = positions.filter { $0.nodePosition?.viaMqtt ?? true == false }
|
||||
let loraCoords = Array(loraNodes).compactMap({(position) -> CLLocationCoordinate2D in
|
||||
return position.nodeCoordinate ?? LocationsHandler.DefaultLocation
|
||||
})
|
||||
/// Convex Hull
|
||||
if showConvexHull {
|
||||
if loraCoords.count > 0 {
|
||||
let hull = loraCoords.getConvexHull()
|
||||
MapPolygon(coordinates: hull)
|
||||
.stroke(.blue, lineWidth: 3)
|
||||
.foregroundStyle(.indigo.opacity(0.4))
|
||||
}
|
||||
}
|
||||
positionAnnotations
|
||||
routeAnnotations
|
||||
waypointAnnotations
|
||||
}
|
||||
|
||||
@MapContentBuilder
|
||||
var body: some MapContent {
|
||||
meshMap
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import CoreData
|
|||
|
||||
@available(iOS 17.0, macOS 14.0, *)
|
||||
struct NodeMapContent: MapContent {
|
||||
|
||||
|
||||
@ObservedObject var node: NodeInfoEntity
|
||||
@State var showUserLocation: Bool = false
|
||||
@State var positions: [PositionEntity] = []
|
||||
|
|
@ -22,7 +22,7 @@ struct NodeMapContent: MapContent {
|
|||
@AppStorage("enableMapTraffic") private var showTraffic: Bool = false
|
||||
@AppStorage("enableMapPointsOfInterest") private var showPointsOfInterest: Bool = false
|
||||
@AppStorage("mapLayer") private var selectedMapLayer: MapLayer = .hybrid
|
||||
|
||||
|
||||
// Map Configuration
|
||||
@Namespace var mapScope
|
||||
@State var mapStyle: MapStyle = MapStyle.hybrid(elevation: .realistic, pointsOfInterest: .all, showsTraffic: true)
|
||||
|
|
@ -33,26 +33,26 @@ struct NodeMapContent: MapContent {
|
|||
@State var isEditingSettings = false
|
||||
@State var selectedPosition: PositionEntity?
|
||||
@State var isMeshMap = false
|
||||
|
||||
|
||||
@MapContentBuilder
|
||||
var nodeMap: some MapContent {
|
||||
let positionArray = node.positions?.array as? [PositionEntity] ?? []
|
||||
let lineCoords = positionArray.compactMap({(position) -> CLLocationCoordinate2D in
|
||||
return position.nodeCoordinate ?? LocationsHandler.DefaultLocation
|
||||
})
|
||||
|
||||
|
||||
/// Node Color from node.num
|
||||
let nodeColor = UIColor(hex: UInt32(node.num))
|
||||
|
||||
|
||||
/// Node Annotations
|
||||
ForEach(node.positions?.array as? [PositionEntity] ?? [], id: \.id) { position in
|
||||
|
||||
|
||||
let pf = PositionFlags(rawValue: Int(position.nodePosition?.metadata?.positionFlags ?? 771))
|
||||
let headingDegrees = Angle.degrees(Double(position.heading))
|
||||
/// Reduced Precision Map Circle
|
||||
if position.latest && 10...19 ~= position.precisionBits {
|
||||
let pp = PositionPrecision(rawValue: Int(position.precisionBits))
|
||||
let radius : CLLocationDistance = pp?.precisionMeters ?? 0
|
||||
let radius: CLLocationDistance = pp?.precisionMeters ?? 0
|
||||
if radius > 0.0 {
|
||||
MapCircle(center: position.coordinate, radius: radius)
|
||||
.foregroundStyle(Color(nodeColor).opacity(0.25))
|
||||
|
|
@ -73,7 +73,7 @@ struct NodeMapContent: MapContent {
|
|||
}
|
||||
}
|
||||
/// Route Lines
|
||||
if showRouteLines {
|
||||
if showRouteLines {
|
||||
let gradient = LinearGradient(
|
||||
colors: [Color(nodeColor.lighter().lighter().lighter()), Color(nodeColor.lighter()), Color(nodeColor)],
|
||||
startPoint: .leading, endPoint: .trailing
|
||||
|
|
@ -153,11 +153,11 @@ struct NodeMapContent: MapContent {
|
|||
.clipShape(Circle())
|
||||
.rotationEffect(headingDegrees)
|
||||
.frame(width: 16, height: 16)
|
||||
|
||||
|
||||
} else {
|
||||
Circle()
|
||||
.fill(Color(UIColor(hex: UInt32(position.nodePosition?.num ?? 0))))
|
||||
.strokeBorder(Color(UIColor(hex: UInt32(position.nodePosition?.num ?? 0))).isLight() ? .black : .white ,lineWidth: 2)
|
||||
.strokeBorder(Color(UIColor(hex: UInt32(position.nodePosition?.num ?? 0))).isLight() ? .black : .white, lineWidth: 2)
|
||||
.frame(width: 12, height: 12)
|
||||
}
|
||||
}
|
||||
|
|
@ -168,7 +168,7 @@ struct NodeMapContent: MapContent {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@MapContentBuilder
|
||||
var body: some MapContent {
|
||||
if node.positions?.count ?? 0 > 0 {
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ struct MapSettingsForm: View {
|
|||
@Binding var meshMap: Bool
|
||||
|
||||
var body: some View {
|
||||
|
||||
|
||||
NavigationStack {
|
||||
Form {
|
||||
Section(header: Text("Map Options")) {
|
||||
|
|
@ -63,7 +63,7 @@ struct MapSettingsForm: View {
|
|||
UserDefaults.enableMapWaypoints = !waypoints
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Toggle(isOn: $nodeHistory) {
|
||||
Label("Node History", systemImage: "building.columns.fill")
|
||||
}
|
||||
|
|
@ -75,7 +75,7 @@ struct MapSettingsForm: View {
|
|||
Toggle(isOn: $routeLines) {
|
||||
Label("Route Lines", systemImage: "road.lanes")
|
||||
}
|
||||
|
||||
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
.onTapGesture {
|
||||
self.routeLines.toggle()
|
||||
|
|
@ -123,6 +123,6 @@ Spacer()
|
|||
}
|
||||
.presentationDetents([.fraction(meshMap ? 0.55 : 0.45), .fraction(0.65)])
|
||||
.presentationDragIndicator(.visible)
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,21 +32,21 @@ struct NodeMapSwiftUI: View {
|
|||
@State var isShowingAltitude = false
|
||||
@State var isEditingSettings = false
|
||||
@State var isMeshMap = false
|
||||
|
||||
|
||||
@State private var mapRegion = MKCoordinateRegion.init()
|
||||
|
||||
|
||||
@FetchRequest(sortDescriptors: [NSSortDescriptor(key: "name", ascending: false)],
|
||||
predicate: NSPredicate(
|
||||
format: "expire == nil || expire >= %@", Date() as NSDate
|
||||
), animation: .none)
|
||||
private var waypoints: FetchedResults<WaypointEntity>
|
||||
|
||||
|
||||
var body: some View {
|
||||
var mostRecent = node.positions?.lastObject as? PositionEntity
|
||||
|
||||
|
||||
if node.hasPositions {
|
||||
ZStack {
|
||||
MapReader { reader in
|
||||
MapReader { _ in
|
||||
Map(position: $position, bounds: MapCameraBounds(minimumDistance: 1, maximumDistance: .infinity), scope: mapScope) {
|
||||
NodeMapContent(node: node)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,14 +21,26 @@ struct PositionAltitudeChart: View {
|
|||
@Environment(\.dismiss) private var dismiss
|
||||
@ObservedObject var node: NodeInfoEntity
|
||||
@State private var lineWidth = 2.0
|
||||
|
||||
var body: some View {
|
||||
|
||||
var data: [PositionAltitude] {
|
||||
let fiveYearsAgo = Calendar.current.date(byAdding: .year, value: -5, to: Date())
|
||||
let nodePositions = Array(node.positions!) as! [PositionEntity]
|
||||
let filteredPositions = nodePositions.filter({$0.time != nil && ($0.time ?? fiveYearsAgo!) > fiveYearsAgo!})
|
||||
let data = filteredPositions.map { PositionAltitude(time: $0.time ?? Date(), altitude: Measurement(value: Double($0.altitude), unit: .meters) ) }
|
||||
guard let nodePositions = node.positions,
|
||||
let positions = Array(nodePositions) as? [PositionEntity]
|
||||
else {
|
||||
return []
|
||||
}
|
||||
|
||||
let filteredPositions = positions.filter({$0.time != nil && ($0.time ?? fiveYearsAgo!) > fiveYearsAgo!})
|
||||
return filteredPositions.map {
|
||||
PositionAltitude(
|
||||
time: $0.time ?? Date(),
|
||||
altitude: Measurement(value: Double($0.altitude), unit: .meters)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
GroupBox(label: Label("Altitude", systemImage: "mountain.2")) {
|
||||
|
||||
Chart(data, id: \.time) {
|
||||
LineMark(
|
||||
x: .value("Time", $0.time),
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ struct PositionPopover: View {
|
|||
VStack {
|
||||
HStack {
|
||||
ZStack {
|
||||
|
||||
|
||||
if position.nodePosition?.isOnline ?? false {
|
||||
Circle()
|
||||
.fill(Color(nodeColor.lighter()).opacity(0.4).shadow(.drop(color: Color(nodeColor).isLight() ? .black : .white, radius: 5)))
|
||||
|
|
@ -42,13 +42,13 @@ struct PositionPopover: View {
|
|||
}
|
||||
CircleText(text: position.nodePosition?.user?.shortName ?? "?", color: Color(nodeColor), circleSize: 65)
|
||||
}
|
||||
|
||||
|
||||
Text(position.nodePosition?.user?.longName ?? "Unknown")
|
||||
.font(.largeTitle)
|
||||
}
|
||||
Divider()
|
||||
HStack (alignment: .center) {
|
||||
VStack (alignment: .leading) {
|
||||
HStack(alignment: .center) {
|
||||
VStack(alignment: .leading) {
|
||||
/// Time
|
||||
Label {
|
||||
Text("heard".localized + ":")
|
||||
|
|
@ -131,7 +131,7 @@ struct PositionPopover: View {
|
|||
}
|
||||
.padding(.bottom, 5)
|
||||
if position.nodePosition?.viaMqtt ?? false {
|
||||
|
||||
|
||||
Label {
|
||||
let heading = Measurement(value: degrees.degrees, unit: UnitAngle.degrees)
|
||||
Text("MQTT")
|
||||
|
|
@ -146,7 +146,7 @@ struct PositionPopover: View {
|
|||
if let lastLocation = locationsHandler.locationsArray.last {
|
||||
/// Distance
|
||||
if lastLocation.distance(from: CLLocation(latitude: LocationsHandler.DefaultLocation.latitude, longitude: LocationsHandler.DefaultLocation.longitude)) > 0.0 {
|
||||
let metersAway = position.coordinate.distance(from:CLLocationCoordinate2D(latitude: lastLocation.coordinate.latitude, longitude: lastLocation.coordinate.longitude))
|
||||
let metersAway = position.coordinate.distance(from: CLLocationCoordinate2D(latitude: lastLocation.coordinate.latitude, longitude: lastLocation.coordinate.longitude))
|
||||
Label {
|
||||
Text("distance".localized + ": \(distanceFormatter.string(fromDistance: Double(metersAway)))")
|
||||
.foregroundColor(.primary)
|
||||
|
|
@ -160,7 +160,7 @@ struct PositionPopover: View {
|
|||
Spacer()
|
||||
}
|
||||
Spacer()
|
||||
VStack (alignment: .center) {
|
||||
VStack(alignment: .center) {
|
||||
if position.nodePosition != nil {
|
||||
if position.nodePosition?.favorite ?? false {
|
||||
Image(systemName: "star.fill")
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
//
|
||||
// WaypointForm.swift
|
||||
// Meshtastic
|
||||
|
|
@ -11,7 +10,7 @@ import MapKit
|
|||
import CoreLocation
|
||||
|
||||
struct WaypointForm: View {
|
||||
|
||||
|
||||
@EnvironmentObject var bleManager: BLEManager
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@State var waypoint: WaypointEntity
|
||||
|
|
@ -27,7 +26,7 @@ struct WaypointForm: View {
|
|||
@State private var expire: Date = Date.now.addingTimeInterval(60 * 480) // 1 minute * 480 = 8 Hours
|
||||
@State private var locked: Bool = false
|
||||
@State private var lockedTo: Int64 = 0
|
||||
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
if editMode {
|
||||
|
|
@ -35,7 +34,7 @@ struct WaypointForm: View {
|
|||
.font(.largeTitle)
|
||||
Divider()
|
||||
Form {
|
||||
let distance = CLLocation(latitude: LocationHelper.currentLocation.latitude, longitude: LocationHelper.currentLocation.longitude).distance(from: CLLocation(latitude: waypoint.coordinate.latitude , longitude: waypoint.coordinate.longitude ))
|
||||
let distance = CLLocation(latitude: LocationHelper.currentLocation.latitude, longitude: LocationHelper.currentLocation.longitude).distance(from: CLLocation(latitude: waypoint.coordinate.latitude, longitude: waypoint.coordinate.longitude ))
|
||||
Section(header: Text("Coordinate") ) {
|
||||
HStack {
|
||||
Text("Location: \(String(format: "%.5f", waypoint.coordinate.latitude) + "," + String(format: "%.5f", waypoint.coordinate.longitude))")
|
||||
|
|
@ -91,14 +90,14 @@ struct WaypointForm: View {
|
|||
.font(.title)
|
||||
.focused($iconIsFocused)
|
||||
.onChange(of: icon) { value in
|
||||
|
||||
|
||||
// If you have anything other than emojis in your string make it empty
|
||||
if !value.onlyEmojis() {
|
||||
icon = ""
|
||||
}
|
||||
// If a second emoji is entered delete the first one
|
||||
if value.count >= 1 {
|
||||
|
||||
|
||||
if value.count > 1 {
|
||||
let index = value.index(value.startIndex, offsetBy: 1)
|
||||
icon = String(value[index])
|
||||
|
|
@ -106,7 +105,7 @@ struct WaypointForm: View {
|
|||
iconIsFocused = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Toggle(isOn: $expires) {
|
||||
Label("Expires", systemImage: "clock.badge.xmark")
|
||||
|
|
@ -168,7 +167,7 @@ struct WaypointForm: View {
|
|||
.controlSize(.regular)
|
||||
.disabled(bleManager.connectedPeripheral == nil)
|
||||
.padding(.bottom)
|
||||
|
||||
|
||||
Button(role: .cancel) {
|
||||
dismiss()
|
||||
} label: {
|
||||
|
|
@ -178,9 +177,9 @@ struct WaypointForm: View {
|
|||
.buttonBorderShape(.capsule)
|
||||
.controlSize(.regular)
|
||||
.padding(.bottom)
|
||||
|
||||
|
||||
if waypoint.id > 0 && bleManager.isConnected {
|
||||
|
||||
|
||||
Menu {
|
||||
Button("For me", action: {
|
||||
bleManager.context!.delete(waypoint)
|
||||
|
|
@ -211,7 +210,7 @@ struct WaypointForm: View {
|
|||
}
|
||||
newWaypoint.expire = UInt32(1)
|
||||
if bleManager.sendWaypoint(waypoint: newWaypoint) {
|
||||
|
||||
|
||||
bleManager.context!.delete(waypoint)
|
||||
do {
|
||||
try bleManager.context!.save()
|
||||
|
|
@ -237,7 +236,7 @@ struct WaypointForm: View {
|
|||
}
|
||||
} else {
|
||||
VStack {
|
||||
HStack {
|
||||
HStack {
|
||||
CircleText(text: String(UnicodeScalar(Int(waypoint.icon)) ?? "📍"), color: Color.orange, circleSize: 65)
|
||||
Spacer()
|
||||
Text(waypoint.name ?? "?")
|
||||
|
|
@ -258,7 +257,7 @@ struct WaypointForm: View {
|
|||
}
|
||||
}
|
||||
Divider()
|
||||
VStack (alignment: .leading) {
|
||||
VStack(alignment: .leading) {
|
||||
// Description
|
||||
if (waypoint.longDescription ?? "").count > 0 {
|
||||
Label {
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ struct NodeDetail: View {
|
|||
|
||||
let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral?.num ?? -1, context: context)
|
||||
NavigationStack {
|
||||
GeometryReader { bounds in
|
||||
GeometryReader { _ in
|
||||
VStack {
|
||||
ScrollView {
|
||||
NodeInfoItem(node: node)
|
||||
|
|
@ -78,12 +78,12 @@ struct NodeDetail: View {
|
|||
Image(systemName: "flipphone")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.font(.title)
|
||||
|
||||
|
||||
Text("Device Metrics Log")
|
||||
.font(.title3)
|
||||
}
|
||||
.disabled(!node.hasDeviceMetrics)
|
||||
|
||||
|
||||
Divider()
|
||||
NavigationLink {
|
||||
if #available (iOS 17, macOS 14, *) {
|
||||
|
|
@ -91,12 +91,12 @@ struct NodeDetail: View {
|
|||
} else {
|
||||
NodeMapMapkit(node: node)
|
||||
}
|
||||
|
||||
|
||||
} label: {
|
||||
Image(systemName: "map")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.font(.title)
|
||||
|
||||
|
||||
Text("Node Map")
|
||||
.font(.title3)
|
||||
}
|
||||
|
|
@ -108,7 +108,7 @@ struct NodeDetail: View {
|
|||
Image(systemName: "mappin.and.ellipse")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.font(.title)
|
||||
|
||||
|
||||
Text("Position Log")
|
||||
.font(.title3)
|
||||
}
|
||||
|
|
@ -120,7 +120,7 @@ struct NodeDetail: View {
|
|||
Image(systemName: "cloud.sun.rain")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.font(.title)
|
||||
|
||||
|
||||
Text("Environment Metrics Log")
|
||||
.font(.title3)
|
||||
}
|
||||
|
|
@ -133,7 +133,7 @@ struct NodeDetail: View {
|
|||
Image(systemName: "signpost.right.and.left")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.font(.title)
|
||||
|
||||
|
||||
Text("Trace Route Log")
|
||||
.font(.title3)
|
||||
}
|
||||
|
|
@ -146,7 +146,7 @@ struct NodeDetail: View {
|
|||
Image(systemName: "sensor")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.font(.title)
|
||||
|
||||
|
||||
Text("Detection Sensor Log")
|
||||
.font(.title3)
|
||||
}
|
||||
|
|
@ -159,7 +159,7 @@ struct NodeDetail: View {
|
|||
Image(systemName: "figure.walk.motion")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.font(.title)
|
||||
|
||||
|
||||
Text("paxcounter.log")
|
||||
.font(.title3)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,9 +14,9 @@ struct NodeInfoItem: View {
|
|||
@ObservedObject var node: NodeInfoEntity
|
||||
|
||||
var body: some View {
|
||||
|
||||
|
||||
Divider()
|
||||
|
||||
|
||||
HStack {
|
||||
|
||||
VStack(alignment: .center) {
|
||||
|
|
|
|||
|
|
@ -15,9 +15,9 @@ struct NodeInfoItem: View {
|
|||
var modemPreset: ModemPresets = ModemPresets(rawValue: UserDefaults.modemPreset) ?? ModemPresets.longFast
|
||||
|
||||
var body: some View {
|
||||
|
||||
|
||||
Divider()
|
||||
|
||||
|
||||
HStack {
|
||||
|
||||
VStack(alignment: .center) {
|
||||
|
|
|
|||
|
|
@ -20,14 +20,14 @@ struct NodeListFilter: View {
|
|||
@Binding var maximumDistance: Double
|
||||
@Binding var hopsAway: Int
|
||||
@Binding var deviceRole: Int
|
||||
|
||||
|
||||
var body: some View {
|
||||
|
||||
|
||||
NavigationStack {
|
||||
Form {
|
||||
Section(header: Text(filterTitle)) {
|
||||
Toggle(isOn: $viaLora) {
|
||||
|
||||
|
||||
Label {
|
||||
Text("Via Lora")
|
||||
} icon: {
|
||||
|
|
@ -37,7 +37,7 @@ struct NodeListFilter: View {
|
|||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Toggle(isOn: $viaMqtt) {
|
||||
|
||||
|
||||
Label {
|
||||
Text("Via Mqtt")
|
||||
} icon: {
|
||||
|
|
@ -46,9 +46,9 @@ struct NodeListFilter: View {
|
|||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
.listRowSeparator(.visible)
|
||||
|
||||
|
||||
Toggle(isOn: $isOnline) {
|
||||
|
||||
|
||||
Label {
|
||||
Text("Online")
|
||||
} icon: {
|
||||
|
|
@ -59,13 +59,13 @@ struct NodeListFilter: View {
|
|||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
.listRowSeparator(.visible)
|
||||
|
||||
|
||||
Toggle(isOn: $isFavorite) {
|
||||
|
||||
|
||||
Label {
|
||||
Text("Favorites")
|
||||
} icon: {
|
||||
|
||||
|
||||
Image(systemName: "star.fill")
|
||||
.foregroundColor(.yellow)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
|
|
@ -73,9 +73,9 @@ struct NodeListFilter: View {
|
|||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
.listRowSeparator(.visible)
|
||||
|
||||
|
||||
Toggle(isOn: $distanceFilter) {
|
||||
|
||||
|
||||
Label {
|
||||
Text("Distance")
|
||||
} icon: {
|
||||
|
|
|
|||
|
|
@ -9,14 +9,14 @@ import SwiftUI
|
|||
import CoreLocation
|
||||
|
||||
struct NodeListItem: View {
|
||||
|
||||
|
||||
@ObservedObject var node: NodeInfoEntity
|
||||
var connected: Bool
|
||||
var connectedNode: Int64
|
||||
var modemPreset: ModemPresets = ModemPresets(rawValue: UserDefaults.modemPreset) ?? ModemPresets.longFast
|
||||
|
||||
|
||||
var body: some View {
|
||||
|
||||
|
||||
NavigationLink(value: node) {
|
||||
LazyVStack(alignment: .leading) {
|
||||
HStack {
|
||||
|
|
@ -69,7 +69,7 @@ struct NodeListItem: View {
|
|||
Text("Role: \(role?.name ?? "unknown".localized)")
|
||||
.font(UIDevice.current.userInterfaceIdiom == .phone ? .callout : .caption)
|
||||
.foregroundColor(.gray)
|
||||
|
||||
|
||||
}
|
||||
if node.isStoreForwardRouter {
|
||||
HStack {
|
||||
|
|
@ -82,14 +82,29 @@ struct NodeListItem: View {
|
|||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if node.positions?.count ?? 0 > 0 && connectedNode != node.num {
|
||||
HStack {
|
||||
let lastPostion = node.positions!.reversed()[0] 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 {
|
||||
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(.hierarchical)
|
||||
.frame(width: 30)
|
||||
DistanceText(meters: metersAway)
|
||||
.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 {
|
||||
let nodeCoord = CLLocation(latitude: lastPostion.nodeCoordinate!.latitude, longitude: lastPostion.nodeCoordinate!.longitude)
|
||||
let metersAway = nodeCoord.distance(from: myCoord)
|
||||
Image(systemName: "lines.measurement.horizontal")
|
||||
|
|
@ -101,20 +116,6 @@ struct NodeListItem: View {
|
|||
.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 {
|
||||
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(.hierarchical)
|
||||
.frame(width: 30)
|
||||
DistanceText(meters: metersAway)
|
||||
.font(UIDevice.current.userInterfaceIdiom == .phone ? .callout : .caption)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -131,7 +132,7 @@ struct NodeListItem: View {
|
|||
.font(UIDevice.current.userInterfaceIdiom == .phone ? .callout : .caption)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if node.viaMqtt && connectedNode != node.num {
|
||||
Image(systemName: "dot.radiowaves.up.forward")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
|
|
|
|||
|
|
@ -13,11 +13,9 @@ import Foundation
|
|||
import MapKit
|
||||
#endif
|
||||
|
||||
|
||||
|
||||
@available(iOS 17.0, macOS 14.0, *)
|
||||
struct MeshMap: View {
|
||||
|
||||
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@EnvironmentObject var bleManager: BLEManager
|
||||
@StateObject var appState = AppState.shared
|
||||
|
|
@ -29,7 +27,7 @@ struct MeshMap: View {
|
|||
@AppStorage("mapLayer") private var selectedMapLayer: MapLayer = .standard
|
||||
// Map Configuration
|
||||
@Namespace var mapScope
|
||||
@State var mapStyle: MapStyle = MapStyle.standard(elevation: .flat, emphasis: MapStyle.StandardEmphasis.muted ,pointsOfInterest: .excludingAll, showsTraffic: false)
|
||||
@State var mapStyle: MapStyle = MapStyle.standard(elevation: .flat, emphasis: MapStyle.StandardEmphasis.muted, pointsOfInterest: .excludingAll, showsTraffic: false)
|
||||
@State var position = MapCameraPosition.automatic
|
||||
@State var isEditingSettings = false
|
||||
@State var selectedPosition: PositionEntity?
|
||||
|
|
@ -39,15 +37,14 @@ struct MeshMap: View {
|
|||
@State var newWaypointCoord: CLLocationCoordinate2D?
|
||||
@State var isMeshMap = true
|
||||
|
||||
|
||||
var body: some View {
|
||||
|
||||
|
||||
NavigationStack {
|
||||
ZStack {
|
||||
MapReader { reader in
|
||||
Map(position: $position, bounds: MapCameraBounds(minimumDistance: 1, maximumDistance: .infinity), scope: mapScope) {
|
||||
MeshMapContent(showUserLocation: $showUserLocation, showTraffic: $showTraffic, showPointsOfInterest: $showPointsOfInterest, selectedMapLayer: $selectedMapLayer, selectedPosition: $selectedPosition, selectedWaypoint: $selectedWaypoint)
|
||||
|
||||
|
||||
}
|
||||
.mapScope(mapScope)
|
||||
.mapStyle(mapStyle)
|
||||
|
|
@ -60,7 +57,7 @@ struct MeshMap: View {
|
|||
.mapControlVisibility(.automatic)
|
||||
}
|
||||
.controlSize(.regular)
|
||||
.onTapGesture(count: 1, perform: { position in
|
||||
.onTapGesture(count: 1, perform: { position in
|
||||
newWaypointCoord = reader.convert(position, from: .local) ?? CLLocationCoordinate2D.init()
|
||||
})
|
||||
.gesture(
|
||||
|
|
@ -73,7 +70,7 @@ struct MeshMap: View {
|
|||
print("Unable to retreive tap location from gesture data.")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
guard let coordinate = reader.convert(point, from: .local) else {
|
||||
print("Unable to convert local point to coordinate on map.")
|
||||
return
|
||||
|
|
@ -162,7 +159,7 @@ struct MeshMap: View {
|
|||
}
|
||||
.tint(Color(UIColor.secondarySystemBackground))
|
||||
.foregroundColor(.accentColor)
|
||||
.buttonStyle(.borderedProminent)
|
||||
.buttonStyle(.borderedProminent)
|
||||
}
|
||||
.controlSize(.regular)
|
||||
.padding(5)
|
||||
|
|
@ -176,9 +173,9 @@ struct MeshMap: View {
|
|||
if self.bleManager.context == nil {
|
||||
self.bleManager.context = context
|
||||
}
|
||||
|
||||
|
||||
// let wayPointEntity = getWaypoint(id: Int64(deepLinkManager.waypointId) ?? -1, context: context)
|
||||
//if wayPointEntity.id > 0 {
|
||||
// if wayPointEntity.id > 0 {
|
||||
// position = .camera(MapCamera(centerCoordinate: wayPointEntity.coordinate, distance: 1000, heading: 0, pitch: 60))
|
||||
switch selectedMapLayer {
|
||||
case .standard:
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import SwiftUI
|
|||
import CoreLocation
|
||||
|
||||
struct NodeList: View {
|
||||
|
||||
|
||||
@StateObject var appState = AppState.shared
|
||||
@State private var columnVisibility = NavigationSplitViewVisibility.all
|
||||
@State private var selectedNode: NodeInfoEntity?
|
||||
|
|
@ -26,25 +26,25 @@ struct NodeList: View {
|
|||
@State private var maxDistance: Double = 800000
|
||||
@State private var hopsAway: Int = -1
|
||||
@State private var deviceRole: Int = -1
|
||||
|
||||
|
||||
@State var isEditingFilters = false
|
||||
|
||||
|
||||
@SceneStorage("selectedDetailView") var selectedDetailView: String?
|
||||
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@EnvironmentObject var bleManager: BLEManager
|
||||
|
||||
@FetchRequest(
|
||||
sortDescriptors: [NSSortDescriptor(key: "favorite", ascending: false),
|
||||
sortDescriptors: [NSSortDescriptor(key: "favorite", ascending: false),
|
||||
NSSortDescriptor(key: "lastHeard", ascending: false),
|
||||
NSSortDescriptor(key: "user.longName", ascending: true)],
|
||||
animation: .default)
|
||||
|
||||
var nodes: FetchedResults<NodeInfoEntity>
|
||||
|
||||
|
||||
var body: some View {
|
||||
NavigationSplitView(columnVisibility: $columnVisibility) {
|
||||
|
||||
|
||||
// HStack {
|
||||
// Button("Open Node") {
|
||||
// UIApplication
|
||||
|
|
@ -52,19 +52,19 @@ struct NodeList: View {
|
|||
// .open(URL(string: "meshtastic://nodes?nodeNum=530606484")!)
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
let connectedNodeNum = Int(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral?.num ?? 0 : 0)
|
||||
let connectedNode = nodes.first(where: { $0.num == connectedNodeNum })
|
||||
List(nodes, id: \.self, selection: $selectedNode) { node in
|
||||
|
||||
NodeListItem(node: node,
|
||||
|
||||
NodeListItem(node: node,
|
||||
connected: bleManager.connectedPeripheral != nil && bleManager.connectedPeripheral?.num ?? -1 == node.num,
|
||||
connectedNode: (bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral?.num ?? -1 : -1))
|
||||
.contextMenu {
|
||||
|
||||
|
||||
Button {
|
||||
if !node.favorite {
|
||||
|
||||
|
||||
let success = bleManager.setFavoriteNode(node: node, connectedNodeNum: Int64(connectedNodeNum))
|
||||
if success {
|
||||
node.favorite = !node.favorite
|
||||
|
|
@ -89,7 +89,7 @@ struct NodeList: View {
|
|||
print("Favorited a node")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
} label: {
|
||||
Label(node.favorite ? "Un-Favorite" : "Favorite", systemImage: node.favorite ? "star.slash.fill" : "star.fill")
|
||||
}
|
||||
|
|
@ -132,14 +132,14 @@ struct NodeList: View {
|
|||
isPresentingTraceRouteSentAlert = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
} label: {
|
||||
Label("Trace Route", systemImage: "signpost.right.and.left")
|
||||
}
|
||||
if node.isStoreForwardRouter {
|
||||
|
||||
Button {
|
||||
let success = bleManager.requestStoreAndForwardClientHistory(fromUser: connectedNode!.user!, toUser: node.user!)
|
||||
let success = bleManager.requestStoreAndForwardClientHistory(fromUser: connectedNode!.user!, toUser: node.user!)
|
||||
if success {
|
||||
isPresentingClientHistorySentAlert = true
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
|
||||
|
|
@ -152,7 +152,7 @@ struct NodeList: View {
|
|||
}
|
||||
}
|
||||
if bleManager.connectedPeripheral != nil {
|
||||
Button (role: .destructive) {
|
||||
Button(role: .destructive) {
|
||||
deleteNodeId = node.num
|
||||
isPresentingDeleteNodeAlert = true
|
||||
} label: {
|
||||
|
|
@ -212,7 +212,7 @@ struct NodeList: View {
|
|||
.disableAutocorrection(true)
|
||||
.scrollDismissesKeyboard(.immediately)
|
||||
.navigationTitle(String.localizedStringWithFormat("nodes %@".localized, String(nodes.count)))
|
||||
|
||||
|
||||
.listStyle(.plain)
|
||||
.confirmationDialog(
|
||||
|
||||
|
|
@ -251,7 +251,7 @@ struct NodeList: View {
|
|||
.navigationBarItems(
|
||||
trailing:
|
||||
ZStack {
|
||||
if (UIDevice.current.userInterfaceIdiom != .phone) {
|
||||
if UIDevice.current.userInterfaceIdiom != .phone {
|
||||
Button {
|
||||
columnVisibility = .detailOnly
|
||||
} label: {
|
||||
|
|
@ -264,7 +264,7 @@ struct NodeList: View {
|
|||
name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?", phoneOnly: true)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
} else {
|
||||
if #available (iOS 17, *) {
|
||||
ContentUnavailableView("select.node", systemImage: "flipphone")
|
||||
|
|
@ -278,7 +278,7 @@ struct NodeList: View {
|
|||
} else {
|
||||
Text("Select something to view")
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
.navigationSplitViewStyle(.balanced)
|
||||
.onChange(of: searchText) { _ in
|
||||
|
|
@ -315,19 +315,18 @@ struct NodeList: View {
|
|||
searchNodeList()
|
||||
}
|
||||
.onChange(of: (appState.navigationPath)) { newPath in
|
||||
|
||||
|
||||
guard let deepLink = newPath else {
|
||||
return
|
||||
}
|
||||
if deepLink.hasPrefix("meshtastic://nodes") {
|
||||
|
||||
|
||||
if let urlComponent = URLComponents(string: deepLink) {
|
||||
let queryItems = urlComponent.queryItems
|
||||
let nodeNum = queryItems?.first(where: { $0.name == "nodenum" })?.value
|
||||
if nodeNum == nil {
|
||||
print("nodeNum not found")
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
selectedNode = nodes.first(where: { $0.num == Int64(nodeNum ?? "-1") })
|
||||
AppState.shared.navigationPath = nil
|
||||
}
|
||||
|
|
@ -371,7 +370,7 @@ struct NodeList: View {
|
|||
let hopsAwayPredicate = NSPredicate(format: "hopsAway == %i", Int32(hopsAway))
|
||||
predicates.append(hopsAwayPredicate)
|
||||
}
|
||||
|
||||
|
||||
/// Online
|
||||
if isOnline {
|
||||
let isOnlinePredicate = NSPredicate(format: "lastHeard >= %@", Calendar.current.date(byAdding: .minute, value: -15, to: Date())! as NSDate)
|
||||
|
|
@ -385,22 +384,22 @@ struct NodeList: View {
|
|||
/// Distance
|
||||
if distanceFilter {
|
||||
let pointOfInterest = LocationHelper.currentLocation
|
||||
|
||||
|
||||
if pointOfInterest.latitude != LocationHelper.DefaultLocation.latitude && pointOfInterest.longitude != LocationHelper.DefaultLocation.longitude {
|
||||
let D: Double = maxDistance * 1.1
|
||||
let R: Double = 6371009
|
||||
let d: Double = maxDistance * 1.1
|
||||
let r: Double = 6371009
|
||||
let meanLatitidue = pointOfInterest.latitude * .pi / 180
|
||||
let deltaLatitude = D / R * 180 / .pi
|
||||
let deltaLongitude = D / (R * cos(meanLatitidue)) * 180 / .pi
|
||||
let deltaLatitude = d / r * 180 / .pi
|
||||
let deltaLongitude = d / (r * cos(meanLatitidue)) * 180 / .pi
|
||||
let minLatitude: Double = pointOfInterest.latitude - deltaLatitude
|
||||
let maxLatitude: Double = pointOfInterest.latitude + deltaLatitude
|
||||
let minLongitude: Double = pointOfInterest.longitude - deltaLongitude
|
||||
let maxLongitude: Double = pointOfInterest.longitude + deltaLongitude
|
||||
let distancePredicate = NSPredicate(format: "(SUBQUERY(positions, $position, $position.latest == TRUE && (%lf <= ($position.longitudeI / 1e7)) AND (($position.longitudeI / 1e7) <= %lf) AND (%lf <= ($position.latitudeI / 1e7)) AND (($position.latitudeI / 1e7) <= %lf))).@count > 0", minLongitude, maxLongitude,minLatitude, maxLatitude)
|
||||
let distancePredicate = NSPredicate(format: "(SUBQUERY(positions, $position, $position.latest == TRUE && (%lf <= ($position.longitudeI / 1e7)) AND (($position.longitudeI / 1e7) <= %lf) AND (%lf <= ($position.latitudeI / 1e7)) AND (($position.latitudeI / 1e7) <= %lf))).@count > 0", minLongitude, maxLongitude, minLatitude, maxLatitude)
|
||||
predicates.append(distancePredicate)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if predicates.count > 0 || !searchText.isEmpty {
|
||||
if !searchText.isEmpty {
|
||||
let filterPredicates = NSCompoundPredicate(type: .and, subpredicates: predicates)
|
||||
|
|
|
|||
|
|
@ -25,13 +25,13 @@ struct PaxCounterLog: View {
|
|||
var body: some View {
|
||||
VStack {
|
||||
if node.hasPax {
|
||||
|
||||
|
||||
let oneWeekAgo = Calendar.current.date(byAdding: .day, value: -7, to: Date())
|
||||
let pax = node.pax?.reversed() as? [PaxCounterEntity] ?? []
|
||||
let chartData = pax
|
||||
.filter { $0.time != nil && $0.time! >= oneWeekAgo! }
|
||||
.sorted { $0.time! < $1.time! }
|
||||
let maxValue = (chartData.map{ $0.wifi }.max() ?? 0) + (chartData.map{ $0.ble }.max() ?? 0) + 5
|
||||
let maxValue = (chartData.map { $0.wifi }.max() ?? 0) + (chartData.map { $0.ble }.max() ?? 0) + 5
|
||||
if chartData.count > 0 {
|
||||
GroupBox(label: Label("\(pax.count) Readings Total", systemImage: "chart.xyaxis.line")) {
|
||||
|
||||
|
|
@ -47,7 +47,7 @@ struct PaxCounterLog: View {
|
|||
.accessibilityValue("X: \(point.time!), Y: \(point.wifi + point.ble)")
|
||||
.foregroundStyle(paxChartColor)
|
||||
.interpolationMethod(.cardinal)
|
||||
|
||||
|
||||
Plot {
|
||||
PointMark(
|
||||
x: .value("x", point.time!),
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ struct PositionLog: View {
|
|||
@ObservedObject var node: NodeInfoEntity
|
||||
@State private var isPresentingClearLogConfirm = false
|
||||
@State private var sortOrder = [KeyPathComparator(\PositionEntity.time)]
|
||||
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
if node.hasPositions {
|
||||
|
|
@ -62,7 +62,7 @@ struct PositionLog: View {
|
|||
}
|
||||
.width(min: 180)
|
||||
}
|
||||
|
||||
|
||||
} else {
|
||||
ScrollView {
|
||||
// Use a grid on iOS as a table only shows a single column
|
||||
|
|
@ -91,19 +91,21 @@ struct PositionLog: View {
|
|||
.font(.caption2)
|
||||
.fontWeight(.bold)
|
||||
}
|
||||
ForEach(node.positions!.reversed() as! [PositionEntity], id: \.self) { (mappin: PositionEntity) in
|
||||
let altitude = Measurement(value: Double(mappin.altitude), unit: UnitLength.meters)
|
||||
GridRow {
|
||||
Text(String(format: "%.5f", mappin.latitude ?? 0))
|
||||
.font(.caption2)
|
||||
Text(String(format: "%.5f", mappin.longitude ?? 0))
|
||||
.font(.caption2)
|
||||
Text(String(mappin.satsInView))
|
||||
.font(.caption2)
|
||||
Text(altitude.formatted())
|
||||
.font(.caption2)
|
||||
Text(mappin.time?.formattedDate(format: dateFormatString) ?? "unknown.age".localized)
|
||||
.font(.caption2)
|
||||
if let positions = node.positions?.reversed() as? [PositionEntity] {
|
||||
ForEach(positions, id: \.self) { (mappin: PositionEntity) in
|
||||
let altitude = Measurement(value: Double(mappin.altitude), unit: UnitLength.meters)
|
||||
GridRow {
|
||||
Text(String(format: "%.5f", mappin.latitude ?? 0))
|
||||
.font(.caption2)
|
||||
Text(String(format: "%.5f", mappin.longitude ?? 0))
|
||||
.font(.caption2)
|
||||
Text(String(mappin.satsInView))
|
||||
.font(.caption2)
|
||||
Text(altitude.formatted())
|
||||
.font(.caption2)
|
||||
Text(mappin.time?.formattedDate(format: dateFormatString) ?? "unknown.age".localized)
|
||||
.font(.caption2)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -160,7 +162,7 @@ struct PositionLog: View {
|
|||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
} else {
|
||||
if #available (iOS 17, *) {
|
||||
ContentUnavailableView("No Positions", systemImage: "mappin.slash")
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ struct TraceRouteLog: View {
|
|||
@ObservedObject var locationsHandler = LocationsHandler.shared
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@EnvironmentObject var bleManager: BLEManager
|
||||
|
||||
|
||||
@State private var isPresentingClearLogConfirm: Bool = false
|
||||
@State var isExporting = false
|
||||
@State var exportString = ""
|
||||
|
|
@ -23,16 +23,16 @@ struct TraceRouteLog: View {
|
|||
@State private var selectedRoute: TraceRouteEntity?
|
||||
// Map Configuration
|
||||
@Namespace var mapScope
|
||||
@State var mapStyle: MapStyle = MapStyle.standard(elevation: .realistic, emphasis: MapStyle.StandardEmphasis.muted ,pointsOfInterest: .all, showsTraffic: true)
|
||||
@State var mapStyle: MapStyle = MapStyle.standard(elevation: .realistic, emphasis: MapStyle.StandardEmphasis.muted, pointsOfInterest: .all, showsTraffic: true)
|
||||
@State var position = MapCameraPosition.automatic
|
||||
let distanceFormatter = MKDistanceFormatter()
|
||||
|
||||
|
||||
var body: some View {
|
||||
HStack (alignment: .top) {
|
||||
HStack(alignment: .top) {
|
||||
VStack {
|
||||
VStack {
|
||||
List(node.traceRoutes?.reversed() as? [TraceRouteEntity] ?? [], id: \.self, selection: $selectedRoute) { route in
|
||||
|
||||
|
||||
Label {
|
||||
Text("\(route.time?.formatted() ?? "unknown".localized) - \(route.response ? (route.hops?.count == 0 && route.response ? "Direct" : "\(route.hops?.count ?? 0) \(route.hops?.count ?? 0 == 1 ? "Hop": "Hops")") : "No Response")")
|
||||
} icon: {
|
||||
|
|
@ -63,12 +63,12 @@ struct TraceRouteLog: View {
|
|||
}
|
||||
.font(.title2)
|
||||
}
|
||||
|
||||
|
||||
let hopsArray = selectedRoute?.hops?.array as? [TraceRouteHopEntity] ?? []
|
||||
let lineCoords = hopsArray.compactMap({(hop) -> CLLocationCoordinate2D in
|
||||
return hop.coordinate ?? LocationHelper.DefaultLocation
|
||||
})
|
||||
if selectedRoute?.response ?? false {
|
||||
if selectedRoute?.response ?? false {
|
||||
if selectedRoute?.hasPositions ?? false {
|
||||
Map(position: $position, bounds: MapCameraBounds(minimumDistance: 1, maximumDistance: .infinity), scope: mapScope) {
|
||||
Annotation("You", coordinate: selectedRoute?.coordinate ?? LocationHelper.DefaultLocation) {
|
||||
|
|
@ -82,8 +82,7 @@ struct TraceRouteLog: View {
|
|||
.annotationTitles(.automatic)
|
||||
// Direct Trace Route
|
||||
if selectedRoute?.response ?? false && selectedRoute?.hops?.count ?? 0 == 0 {
|
||||
if selectedRoute?.node?.positions?.count ?? 0 > 0 {
|
||||
let mostRecent = selectedRoute?.node?.positions?.lastObject as! PositionEntity
|
||||
if selectedRoute?.node?.positions?.count ?? 0 > 0, let mostRecent = selectedRoute?.node?.positions?.lastObject as? PositionEntity {
|
||||
var traceRouteCoords: [CLLocationCoordinate2D] = [selectedRoute?.coordinate ?? LocationsHandler.DefaultLocation, mostRecent.coordinate]
|
||||
Annotation(selectedRoute?.node?.user?.shortName ?? "???", coordinate: mostRecent.nodeCoordinate ?? LocationHelper.DefaultLocation) {
|
||||
ZStack {
|
||||
|
|
@ -101,19 +100,21 @@ struct TraceRouteLog: View {
|
|||
.stroke(.blue, style: dashed)
|
||||
}
|
||||
} else if selectedRoute?.hops?.count ?? 0 == 0 {
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
}
|
||||
VStack {
|
||||
/// Distance
|
||||
if selectedRoute?.node?.positions?.count ?? 0 > 0 && selectedRoute?.coordinate != nil {
|
||||
let mostRecent = selectedRoute?.node?.positions?.lastObject as! PositionEntity
|
||||
if selectedRoute?.node?.positions?.count ?? 0 > 0,
|
||||
selectedRoute?.coordinate != nil,
|
||||
let mostRecent = selectedRoute?.node?.positions?.lastObject as? PositionEntity {
|
||||
|
||||
let startPoint = CLLocation(latitude: selectedRoute?.coordinate?.latitude ?? LocationsHandler.DefaultLocation.latitude, longitude: selectedRoute?.coordinate?.longitude ?? LocationsHandler.DefaultLocation.longitude)
|
||||
|
||||
|
||||
if startPoint.distance(from: CLLocation(latitude: LocationsHandler.DefaultLocation.latitude, longitude: LocationsHandler.DefaultLocation.longitude)) > 0.0 {
|
||||
let metersAway = selectedRoute?.coordinate?.distance(from:CLLocationCoordinate2D(latitude: mostRecent.latitude ?? LocationsHandler.DefaultLocation.latitude, longitude: mostRecent.longitude ?? LocationsHandler.DefaultLocation.longitude))
|
||||
let metersAway = selectedRoute?.coordinate?.distance(from: CLLocationCoordinate2D(latitude: mostRecent.latitude ?? LocationsHandler.DefaultLocation.latitude, longitude: mostRecent.longitude ?? LocationsHandler.DefaultLocation.longitude))
|
||||
Label {
|
||||
Text("distance".localized + ": \(distanceFormatter.string(fromDistance: Double(metersAway ?? 0)))")
|
||||
.foregroundColor(.primary)
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ struct AboutMeshtastic: View {
|
|||
|
||||
}
|
||||
Section(header: Text("Apple Apps")) {
|
||||
|
||||
|
||||
if locale.region?.identifier ?? "US" == "US" {
|
||||
HStack {
|
||||
Image("SOLAR_NODE")
|
||||
|
|
@ -48,7 +48,7 @@ struct AboutMeshtastic: View {
|
|||
}
|
||||
}
|
||||
.font(.title2)
|
||||
|
||||
|
||||
Text("Version: \(Bundle.main.appVersionLong) (\(Bundle.main.appBuild)) ")
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ struct AppSettings: View {
|
|||
VStack {
|
||||
Form {
|
||||
Section(header: Text("App Settings")) {
|
||||
Button("Open Settings", systemImage: "gear") {
|
||||
Button("Open Settings", systemImage: "gear") {
|
||||
// Get the settings URL and open it
|
||||
if let url = URL(string: UIApplication.openSettingsURLString) {
|
||||
UIApplication.shared.open(url)
|
||||
|
|
|
|||
|
|
@ -44,10 +44,10 @@ struct Channels: View {
|
|||
@State var positionsEnabled = true
|
||||
@State var supportedVersion = true
|
||||
@State var selectedChannel: ChannelEntity?
|
||||
|
||||
|
||||
/// Minimum Version for granular position configuration
|
||||
@State var minimumVersion = "2.2.24"
|
||||
|
||||
|
||||
@FetchRequest(
|
||||
sortDescriptors: [NSSortDescriptor(key: "favorite", ascending: false),
|
||||
NSSortDescriptor(key: "lastHeard", ascending: false),
|
||||
|
|
@ -90,7 +90,7 @@ struct Channels: View {
|
|||
positionPrecision = 32
|
||||
preciseLocation = true
|
||||
positionsEnabled = true
|
||||
|
||||
|
||||
} else if !supportedVersion && channelRole == 2 {
|
||||
positionPrecision = 0
|
||||
preciseLocation = false
|
||||
|
|
@ -135,7 +135,7 @@ struct Channels: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.sheet(item: $selectedChannel) { selection in
|
||||
.sheet(item: $selectedChannel) { _ in
|
||||
#if targetEnvironment(macCatalyst)
|
||||
Text("channel")
|
||||
.font(.largeTitle)
|
||||
|
|
@ -159,7 +159,7 @@ struct Channels: View {
|
|||
channel.settings.uplinkEnabled = uplink
|
||||
channel.settings.downlinkEnabled = downlink
|
||||
channel.settings.moduleSettings.positionPrecision = UInt32(positionPrecision)
|
||||
|
||||
|
||||
selectedChannel!.role = Int32(channelRole)
|
||||
selectedChannel!.index = channelIndex
|
||||
selectedChannel!.name = channelName
|
||||
|
|
@ -187,20 +187,21 @@ struct Channels: View {
|
|||
print("💥 Unresolved Core Data error in the channel editor. Error: \(nsError)")
|
||||
}
|
||||
} else {
|
||||
guard let channelEntity = node?.myInfo?.channels?.first(where: { ($0 as! ChannelEntity).index == channelIndex }) else {
|
||||
guard let channelEntities = node?.myInfo?.channels as? [ChannelEntity],
|
||||
let channelEntity = channelEntities.first(where: { $0.index == channelIndex }) else {
|
||||
return
|
||||
}
|
||||
|
||||
let objects = (channelEntity as! ChannelEntity).allPrivateMessages
|
||||
|
||||
let objects = channelEntity.allPrivateMessages
|
||||
for object in objects {
|
||||
context.delete(object)
|
||||
}
|
||||
for node in nodes {
|
||||
if node.channel == (channelEntity as AnyObject).index {
|
||||
if node.channel == channelEntity.index {
|
||||
context.delete(node)
|
||||
}
|
||||
}
|
||||
context.delete(channelEntity as! ChannelEntity)
|
||||
context.delete(channelEntity)
|
||||
do {
|
||||
try context.save()
|
||||
print("💾 Deleted Channel: \(channel.settings.name)")
|
||||
|
|
@ -259,7 +260,7 @@ struct Channels: View {
|
|||
uplink = false
|
||||
downlink = false
|
||||
hasChanges = true
|
||||
|
||||
|
||||
let newChannel = ChannelEntity(context: context)
|
||||
newChannel.id = channelIndex
|
||||
newChannel.index = channelIndex
|
||||
|
|
@ -297,10 +298,8 @@ func firstMissingChannelIndex(_ indexes: [Int]) -> Int {
|
|||
var smallestIndex = 1
|
||||
if indexes.isEmpty { return smallestIndex }
|
||||
if smallestIndex <= indexes.count {
|
||||
for element in smallestIndex...indexes.count {
|
||||
if !indexes.contains(element) {
|
||||
return element
|
||||
}
|
||||
for element in smallestIndex...indexes.count where !indexes.contains(element) {
|
||||
return element
|
||||
}
|
||||
}
|
||||
return indexes.count + 1
|
||||
|
|
@ -333,7 +332,7 @@ enum PositionPrecision: Int, CaseIterable, Identifiable {
|
|||
case twentyfour = 24
|
||||
|
||||
var id: Int { self.rawValue }
|
||||
|
||||
|
||||
var precisionMeters: Double {
|
||||
switch self {
|
||||
case .two:
|
||||
|
|
@ -384,7 +383,7 @@ enum PositionPrecision: Int, CaseIterable, Identifiable {
|
|||
return 1.413763999910884
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var description: String {
|
||||
let distanceFormatter = MKDistanceFormatter()
|
||||
return String.localizedStringWithFormat("position.precision %@".localized, String(distanceFormatter.string(fromDistance: precisionMeters)))
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@
|
|||
// Copyright(c) Garth Vander Houwen 3/17/24.
|
||||
//
|
||||
|
||||
|
||||
import SwiftUI
|
||||
#if canImport(MapKit)
|
||||
import MapKit
|
||||
|
|
@ -26,9 +25,9 @@ struct ChannelForm: View {
|
|||
@Binding var hasChanges: Bool
|
||||
@Binding var hasValidKey: Bool
|
||||
@Binding var supportedVersion: Bool
|
||||
|
||||
|
||||
var body: some View {
|
||||
|
||||
|
||||
NavigationStack {
|
||||
Form {
|
||||
Section(header: Text("channel details")) {
|
||||
|
|
@ -98,15 +97,14 @@ struct ChannelForm: View {
|
|||
Color.clear :
|
||||
Color.red
|
||||
, lineWidth: 2.0)
|
||||
|
||||
|
||||
)
|
||||
.onChange(of: channelKey, perform: { _ in
|
||||
|
||||
|
||||
let tempKey = Data(base64Encoded: channelKey) ?? Data()
|
||||
if tempKey.count == channelKeySize || channelKeySize == -1{
|
||||
if tempKey.count == channelKeySize || channelKeySize == -1 {
|
||||
hasValidKey = true
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
hasValidKey = false
|
||||
}
|
||||
hasChanges = true
|
||||
|
|
@ -131,9 +129,9 @@ struct ChannelForm: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Section(header: Text("position")) {
|
||||
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
Toggle(isOn: $positionsEnabled) {
|
||||
Label(channelRole == 1 ? "Positions Enabled" : "Allow Position Requests", systemImage: positionsEnabled ? "mappin" : "mappin.slash")
|
||||
|
|
@ -141,7 +139,7 @@ struct ChannelForm: View {
|
|||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
.disabled(!supportedVersion)
|
||||
}
|
||||
|
||||
|
||||
if positionsEnabled {
|
||||
VStack(alignment: .leading) {
|
||||
Toggle(isOn: $preciseLocation) {
|
||||
|
|
@ -156,7 +154,7 @@ struct ChannelForm: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if !preciseLocation {
|
||||
VStack(alignment: .leading) {
|
||||
Label("Approximate Location", systemImage: "location.slash.circle.fill")
|
||||
|
|
@ -236,8 +234,7 @@ struct ChannelForm: View {
|
|||
let tempKey = Data(base64Encoded: channelKey) ?? Data()
|
||||
if tempKey.count == channelKeySize || channelKeySize == -1 {
|
||||
hasValidKey = true
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
hasValidKey = false
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ struct DeviceConfig: View {
|
|||
.font(.callout)
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
Picker("Rebroadcast Mode", selection: $rebroadcastMode ) {
|
||||
ForEach(RebroadcastModes.allCases) { rm in
|
||||
|
|
@ -58,13 +58,13 @@ struct DeviceConfig: View {
|
|||
.font(.callout)
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
|
||||
|
||||
Toggle(isOn: $isManaged) {
|
||||
Label("Managed Device", systemImage: "gearshape.arrow.triangle.2.circlepath")
|
||||
Text("Enabling Managed mode will restrict access to all radio configurations, such as short/long names, regions, channels, modules, etc. and will only be accessible through the Admin channel. To avoid being locked out, make sure the Admin channel is working properly before enabling it.")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
|
||||
|
||||
Picker("Node Info Broadcast Interval", selection: $nodeInfoBroadcastSecs ) {
|
||||
ForEach(UpdateIntervals.allCases) { ui in
|
||||
if ui.rawValue >= 3600 {
|
||||
|
|
@ -75,13 +75,13 @@ struct DeviceConfig: View {
|
|||
.pickerStyle(DefaultPickerStyle())
|
||||
}
|
||||
Section(header: Text("Hardware")) {
|
||||
|
||||
|
||||
Toggle(isOn: $doubleTapAsButtonPress) {
|
||||
Label("Double Tap as Button", systemImage: "hand.tap")
|
||||
Text("Treat double tap on supported accelerometers as a user button press.")
|
||||
}
|
||||
.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.")
|
||||
|
|
@ -110,7 +110,7 @@ struct DeviceConfig: View {
|
|||
}
|
||||
})
|
||||
.foregroundColor(.gray)
|
||||
|
||||
|
||||
}
|
||||
.keyboardType(.default)
|
||||
.disableAutocorrection(true)
|
||||
|
|
@ -165,7 +165,7 @@ struct DeviceConfig: View {
|
|||
bleManager.disconnectPeripheral()
|
||||
clearCoreDataDatabase(context: context, includeRoutes: false)
|
||||
}
|
||||
|
||||
|
||||
} else {
|
||||
print("NodeDB Reset Failed")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ struct DisplayConfig: View {
|
|||
Text(dm.description)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Text("Override automatic OLED screen detection.")
|
||||
.foregroundColor(.gray)
|
||||
.font(.callout)
|
||||
|
|
@ -54,13 +54,13 @@ struct DisplayConfig: View {
|
|||
Text("Requires that there be an accelerometer on your device.")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
|
||||
|
||||
Toggle(isOn: $flipScreen) {
|
||||
Label("Flip Screen", systemImage: "pip.swap")
|
||||
Text("Flip screen vertically")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
Picker("OLED Type", selection: $oledType ) {
|
||||
ForEach(OledTypes.allCases) { ot in
|
||||
|
|
@ -85,20 +85,20 @@ struct DisplayConfig: View {
|
|||
.font(.callout)
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
Picker("Carousel Interval", selection: $screenCarouselInterval ) {
|
||||
ForEach(ScreenCarouselIntervals.allCases) { sci in
|
||||
Text(sci.description)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Text("Automatically toggles to the next page on the screen like a carousel, based the specified interval.")
|
||||
.foregroundColor(.gray)
|
||||
.font(.callout)
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
Picker("GPS Format", selection: $gpsFormat ) {
|
||||
ForEach(GpsFormats.allCases) { lu in
|
||||
|
|
@ -110,7 +110,7 @@ struct DisplayConfig: View {
|
|||
.font(.callout)
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
Picker("Display Units", selection: $units ) {
|
||||
ForEach(Units.allCases) { un in
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ struct LoRaConfig: View {
|
|||
.font(.callout)
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
|
||||
|
||||
Toggle(isOn: $usePreset) {
|
||||
Label("Use Preset", systemImage: "list.bullet.rectangle")
|
||||
}
|
||||
|
|
@ -91,7 +91,7 @@ struct LoRaConfig: View {
|
|||
}
|
||||
}
|
||||
Section(header: Text("Advanced")) {
|
||||
|
||||
|
||||
Toggle(isOn: $ignoreMqtt) {
|
||||
Label("Ignore MQTT", systemImage: "server.rack")
|
||||
}
|
||||
|
|
@ -140,7 +140,7 @@ struct LoRaConfig: View {
|
|||
.font(.callout)
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
HStack {
|
||||
Text("Frequency Slot")
|
||||
|
|
@ -163,12 +163,12 @@ struct LoRaConfig: View {
|
|||
.foregroundColor(.gray)
|
||||
.font(.callout)
|
||||
}
|
||||
|
||||
|
||||
Toggle(isOn: $rxBoostedGain) {
|
||||
Label("RX Boosted Gain", systemImage: "waveform.badge.plus")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
|
||||
|
||||
HStack {
|
||||
Label("Frequency Override", systemImage: "waveform.path.ecg")
|
||||
Spacer()
|
||||
|
|
@ -177,7 +177,7 @@ struct LoRaConfig: View {
|
|||
.scrollDismissesKeyboard(.immediately)
|
||||
.focused($focusedField, equals: .frequencyOverride)
|
||||
}
|
||||
|
||||
|
||||
HStack {
|
||||
Image(systemName: "antenna.radiowaves.left.and.right")
|
||||
.foregroundColor(.accentColor)
|
||||
|
|
|
|||
|
|
@ -28,15 +28,15 @@ struct AmbientLightingConfig: View {
|
|||
VStack {
|
||||
Form {
|
||||
ConfigHeader(title: "Ambient Lighting", config: \.ambientLightingConfig, node: node, onAppear: setAmbientLightingConfigValue)
|
||||
|
||||
|
||||
Section(header: Text("options")) {
|
||||
|
||||
|
||||
Toggle(isOn: $ledState) {
|
||||
Label("LED State", systemImage: ledState ? "lightbulb.led.fill" : "lightbulb.led")
|
||||
Text("The state of the LED (on/off)")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
|
||||
|
||||
HStack {
|
||||
Image(systemName: "eyedropper")
|
||||
.foregroundColor(.accentColor)
|
||||
|
|
|
|||
|
|
@ -39,21 +39,21 @@ struct CannedMessagesConfig: View {
|
|||
VStack {
|
||||
Form {
|
||||
ConfigHeader(title: "Canned messages", config: \.cannedMessageConfig, node: node, onAppear: setCannedMessagesValues)
|
||||
|
||||
|
||||
Section(header: Text("options")) {
|
||||
|
||||
|
||||
Toggle(isOn: $enabled) {
|
||||
|
||||
Label("enabled", systemImage: "list.bullet.rectangle.fill")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
|
||||
|
||||
Toggle(isOn: $sendBell) {
|
||||
|
||||
Label("Send Bell", systemImage: "bell")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
|
||||
|
||||
Picker("Configuration Presets", selection: $configPreset ) {
|
||||
ForEach(ConfigPresets.allCases) { cp in
|
||||
Text(cp.description)
|
||||
|
|
|
|||
|
|
@ -44,9 +44,9 @@ struct DetectionSensorConfig: View {
|
|||
VStack {
|
||||
Form {
|
||||
ConfigHeader(title: "Detection Sensor", config: \.detectionSensorConfig, node: node, onAppear: setDetectionSensorValues)
|
||||
|
||||
|
||||
Section(header: Text("options")) {
|
||||
|
||||
|
||||
Toggle(isOn: $enabled) {
|
||||
Label("enabled", systemImage: "dot.radiowaves.right")
|
||||
Text("Enables the detection sensor module, it needs to be enabled on both the node with the sensor, and any nodes that you want to receive detection sensor text messages or view the detection sensor log and chart.")
|
||||
|
|
@ -90,7 +90,7 @@ struct DetectionSensorConfig: View {
|
|||
.autocapitalization(.none)
|
||||
.disableAutocorrection(true)
|
||||
.onChange(of: name, perform: { _ in
|
||||
|
||||
|
||||
let totalBytes = name.utf8.count
|
||||
// Only mess with the value if it is too big
|
||||
if totalBytes > 20 {
|
||||
|
|
@ -102,7 +102,7 @@ struct DetectionSensorConfig: View {
|
|||
Text("Friendly name used to format message sent to mesh. Example: A name \"Motion\" would result in a message \"Motion detected\"")
|
||||
.font(.callout)
|
||||
.foregroundStyle(.gray)
|
||||
|
||||
|
||||
Picker("GPIO Pin to monitor", selection: $monitorPin) {
|
||||
ForEach(0..<49) {
|
||||
if $0 == 0 {
|
||||
|
|
@ -113,13 +113,13 @@ struct DetectionSensorConfig: View {
|
|||
}
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
|
||||
|
||||
Toggle(isOn: $detectionTriggeredHigh) {
|
||||
Label("Detection trigger High", systemImage: "dial.high")
|
||||
Text("Whether or not the GPIO pin state detection is triggered on HIGH (1) or LOW (0)")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
|
||||
|
||||
Toggle(isOn: $usePullup) {
|
||||
Label("Uses pullup resistor", systemImage: "arrow.up.to.line")
|
||||
Text(" Whether or not use INPUT_PULLUP mode for GPIO pin. Only applicable if the board uses pull-up resistors on the pin")
|
||||
|
|
|
|||
|
|
@ -38,28 +38,28 @@ struct ExternalNotificationConfig: View {
|
|||
ConfigHeader(title: "External notification", config: \.externalNotificationConfig, node: node, onAppear: setExternalNotificationValues)
|
||||
|
||||
Section(header: Text("options")) {
|
||||
|
||||
|
||||
Toggle(isOn: $enabled) {
|
||||
Label("enabled", systemImage: "megaphone")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
|
||||
|
||||
Toggle(isOn: $alertBell) {
|
||||
Label("Alert when receiving a bell", systemImage: "bell")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
|
||||
|
||||
Toggle(isOn: $alertMessage) {
|
||||
Label("Alert when receiving a message", systemImage: "message")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
|
||||
|
||||
Toggle(isOn: $usePWM) {
|
||||
Label("Use PWM Buzzer", systemImage: "light.beacon.max.fill")
|
||||
Text("Use a PWM output (like the RAK Buzzer) for tunes instead of an on/off output. This will ignore the output, output duration and active settings and use the device config buzzer GPIO option instead.")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
|
||||
|
||||
Toggle(isOn: $useI2SAsBuzzer) {
|
||||
Label("Use I2S As Buzzer", systemImage: "light.beacon.max.fill")
|
||||
Text("Enables devices with native I2S audio output to use the RTTTL over speaker like a buzzer. T-Watch S3 and T-Deck for example have this capability.")
|
||||
|
|
@ -76,7 +76,7 @@ struct ExternalNotificationConfig: View {
|
|||
Text("If enabled, the 'output' Pin will be pulled active high, disabled means active low.")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
|
||||
|
||||
Picker("Output pin GPIO", selection: $output) {
|
||||
ForEach(0..<49) {
|
||||
if $0 == 0 {
|
||||
|
|
@ -88,7 +88,7 @@ struct ExternalNotificationConfig: View {
|
|||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
.listRowSeparator(.visible)
|
||||
|
||||
|
||||
Picker("GPIO Output Duration", selection: $outputMilliseconds ) {
|
||||
ForEach(OutputIntervals.allCases) { oi in
|
||||
Text(oi.description)
|
||||
|
|
@ -100,7 +100,7 @@ struct ExternalNotificationConfig: View {
|
|||
.foregroundColor(.gray)
|
||||
.font(.callout)
|
||||
.listRowSeparator(.visible)
|
||||
|
||||
|
||||
Picker("Nag timeout", selection: $nagTimeout ) {
|
||||
ForEach(NagIntervals.allCases) { oi in
|
||||
Text(oi.description)
|
||||
|
|
@ -112,7 +112,7 @@ struct ExternalNotificationConfig: View {
|
|||
.foregroundColor(.gray)
|
||||
.font(.callout)
|
||||
}
|
||||
|
||||
|
||||
Section(header: Text("Optional GPIO")
|
||||
.font(.caption)
|
||||
.foregroundColor(.gray)
|
||||
|
|
|
|||
|
|
@ -32,8 +32,7 @@ struct MQTTConfig: View {
|
|||
@State var mapPublishIntervalSecs = 3600
|
||||
@State var preciseLocation: Bool = false
|
||||
@State var mapPositionPrecision: Double = 13.0
|
||||
|
||||
|
||||
|
||||
let locale = Locale.current
|
||||
|
||||
var body: some View {
|
||||
|
|
@ -41,7 +40,7 @@ struct MQTTConfig: View {
|
|||
Form {
|
||||
if node != nil && node?.loRaConfig != nil {
|
||||
let rc = RegionCodes(rawValue: Int(node?.loRaConfig?.regionCode ?? 0))
|
||||
if rc?.dutyCycle ?? 0 > 0 && rc?.dutyCycle ?? 0 < 100 {
|
||||
if rc?.dutyCycle ?? 0 > 0 && rc?.dutyCycle ?? 0 < 100 {
|
||||
Text("Your region has a \(rc?.dutyCycle ?? 0)% duty cycle. MQTT is not advised when you are duty cycle restricted, the extra traffic will quickly overwhelm your LoRa mesh.")
|
||||
.font(.callout)
|
||||
.foregroundColor(.red)
|
||||
|
|
@ -51,19 +50,19 @@ struct MQTTConfig: View {
|
|||
ConfigHeader(title: "MQTT", config: \.mqttConfig, node: node, onAppear: setMqttValues)
|
||||
|
||||
Section(header: Text("options")) {
|
||||
|
||||
|
||||
Toggle(isOn: $enabled) {
|
||||
Label("enabled", systemImage: "dot.radiowaves.up.forward")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
|
||||
|
||||
Toggle(isOn: $proxyToClientEnabled) {
|
||||
|
||||
|
||||
Label("mqtt.clientproxy", systemImage: "iphone.radiowaves.left.and.right")
|
||||
Text("Utilizes the network connection on your phone to connect to MQTT.")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
|
||||
|
||||
if enabled && proxyToClientEnabled && node!.mqttConfig!.proxyToClientEnabled == true {
|
||||
Toggle(isOn: $mqttConnected) {
|
||||
Label(mqttConnected ? "mqtt.disconnect".localized : "mqtt.connect".localized, systemImage: "server.rack")
|
||||
|
|
@ -72,25 +71,25 @@ struct MQTTConfig: View {
|
|||
.fixedSize(horizontal: false, vertical: true)
|
||||
.foregroundColor(.red)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
}
|
||||
|
||||
|
||||
Toggle(isOn: $encryptionEnabled) {
|
||||
Label("Encryption Enabled", systemImage: "lock.icloud")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
|
||||
|
||||
Toggle(isOn: $jsonEnabled) {
|
||||
Label("JSON Enabled", systemImage: "ellipsis.curlybraces")
|
||||
Text("JSON mode is a limited, unencrypted MQTT output for locally integrating with home assistant")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
}
|
||||
|
||||
|
||||
Section(header: Text("Map Report")) {
|
||||
|
||||
|
||||
Toggle(isOn: $mapReportingEnabled) {
|
||||
Label("enabled", systemImage: "map")
|
||||
}
|
||||
|
|
@ -104,7 +103,7 @@ struct MQTTConfig: View {
|
|||
}
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
Toggle(isOn: $preciseLocation) {
|
||||
Label("Precise Location", systemImage: "scope")
|
||||
|
|
@ -119,7 +118,7 @@ struct MQTTConfig: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if !preciseLocation {
|
||||
VStack(alignment: .leading) {
|
||||
Label("Approximate Location", systemImage: "location.slash.circle.fill")
|
||||
|
|
@ -157,7 +156,7 @@ struct MQTTConfig: View {
|
|||
Text("The root topic to use for MQTT.")
|
||||
.foregroundColor(.gray)
|
||||
.font(.callout)
|
||||
|
||||
|
||||
if nearbyTopics.count > 0 {
|
||||
Picker("Nearby Topics", selection: $selectedTopic ) {
|
||||
ForEach(nearbyTopics, id: \.self) { nt in
|
||||
|
|
@ -171,7 +170,7 @@ struct MQTTConfig: View {
|
|||
.font(.callout)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Section(header: Text("Server")) {
|
||||
HStack {
|
||||
Label("Address", systemImage: "server.rack")
|
||||
|
|
@ -190,7 +189,7 @@ struct MQTTConfig: View {
|
|||
.keyboardType(.default)
|
||||
}
|
||||
.autocorrectionDisabled()
|
||||
|
||||
|
||||
HStack {
|
||||
Label("mqtt.username", systemImage: "person.text.rectangle")
|
||||
TextField("mqtt.username", text: $username)
|
||||
|
|
@ -198,9 +197,9 @@ struct MQTTConfig: View {
|
|||
.autocapitalization(.none)
|
||||
.disableAutocorrection(true)
|
||||
.onChange(of: username, perform: { _ in
|
||||
|
||||
|
||||
let totalBytes = username.utf8.count
|
||||
|
||||
|
||||
// Only mess with the value if it is too big
|
||||
if totalBytes > 62 {
|
||||
username = String(username.dropLast())
|
||||
|
|
@ -218,7 +217,7 @@ struct MQTTConfig: View {
|
|||
.autocapitalization(.none)
|
||||
.disableAutocorrection(true)
|
||||
.onChange(of: password, perform: { _ in
|
||||
|
||||
|
||||
let totalBytes = password.utf8.count
|
||||
// Only mess with the value if it is too big
|
||||
if totalBytes > 62 {
|
||||
|
|
@ -371,20 +370,20 @@ 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) -> Void in
|
||||
geocoder.reverseGeocodeLocation(LocationsHandler.shared.locationsArray.first!, completionHandler: {(placemarks, error) in
|
||||
if error != nil {
|
||||
print("Failed to reverse geocode location")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if let placemarks = placemarks, let placemark = placemarks.first {
|
||||
let cc = locale.region?.identifier ?? "UNK"
|
||||
/// Country Topic unless you are US
|
||||
|
|
@ -412,9 +411,7 @@ struct MQTTConfig: View {
|
|||
if !neightborhoodTopic.isEmpty {
|
||||
nearbyTopics.append(neightborhoodTopic)
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
} else {
|
||||
print("No Location")
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ struct PaxCounterConfig: View {
|
|||
if self.bleManager.context == nil {
|
||||
self.bleManager.context = context
|
||||
}
|
||||
|
||||
|
||||
setPaxValues()
|
||||
// Need to request a PAX Counter module config from the remote node before allowing changes
|
||||
if bleManager.connectedPeripheral != nil && node?.paxCounterConfig == nil {
|
||||
|
|
@ -80,7 +80,7 @@ struct PaxCounterConfig: View {
|
|||
hasChanges = $0 != val
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
SaveConfigButton(node: node, hasChanges: $hasChanges) {
|
||||
guard let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context),
|
||||
let fromUser = connectedNode.user,
|
||||
|
|
@ -91,7 +91,7 @@ struct PaxCounterConfig: View {
|
|||
var config = ModuleConfig.PaxcounterConfig()
|
||||
config.enabled = enabled
|
||||
config.paxcounterUpdateInterval = UInt32(paxcounterUpdateInterval)
|
||||
|
||||
|
||||
let adminMessageId = bleManager.savePaxcounterModuleConfig(
|
||||
config: config,
|
||||
fromUser: fromUser,
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ struct RangeTestConfig: View {
|
|||
VStack {
|
||||
Form {
|
||||
ConfigHeader(title: "Range", config: \.rangeTestConfig, node: node, onAppear: setRangeTestValues)
|
||||
|
||||
|
||||
Section(header: Text("options")) {
|
||||
Toggle(isOn: $enabled) {
|
||||
Label("enabled", systemImage: "figure.walk")
|
||||
|
|
@ -41,14 +41,14 @@ struct RangeTestConfig: View {
|
|||
Text("This device will send out range test messages on the selected interval.")
|
||||
.foregroundColor(.gray)
|
||||
.font(.callout)
|
||||
|
||||
|
||||
Toggle(isOn: $save) {
|
||||
Label("save", systemImage: "square.and.arrow.down.fill")
|
||||
Text("Saves a CSV with the range test message details, currently only available on ESP32 devices with a web server.")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
.disabled(!(node != nil && node?.metadata?.hasWifi ?? false))
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
.disabled(self.bleManager.connectedPeripheral == nil || node?.rangeTestConfig == nil)
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ struct RtttlConfig: View {
|
|||
VStack {
|
||||
Form {
|
||||
ConfigHeader(title: "ringtone", config: \.rtttlConfig, node: node, onAppear: setRtttLConfigValue)
|
||||
|
||||
|
||||
Section(header: Text("options")) {
|
||||
HStack {
|
||||
Label("ringtone", systemImage: "music.quarternote.3")
|
||||
|
|
|
|||
|
|
@ -25,12 +25,12 @@ struct SerialConfig: View {
|
|||
@State var timeout = 0
|
||||
@State var overrideConsoleSerialPort = false
|
||||
@State var mode = 0
|
||||
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Form {
|
||||
ConfigHeader(title: "Serial", config: \.serialConfig, node: node, onAppear: setSerialValues)
|
||||
|
||||
|
||||
Section(header: Text("options")) {
|
||||
|
||||
Toggle(isOn: $enabled) {
|
||||
|
|
|
|||
|
|
@ -32,9 +32,9 @@ struct StoreForwardConfig: View {
|
|||
VStack {
|
||||
Form {
|
||||
ConfigHeader(title: "storeforward", config: \.storeForwardConfig, node: node, onAppear: setStoreAndForwardValues)
|
||||
|
||||
|
||||
Section(header: Text("options")) {
|
||||
|
||||
|
||||
Toggle(isOn: $enabled) {
|
||||
Label("enabled", systemImage: "envelope.arrow.triangle.branch")
|
||||
Text("Enables the store and forward module. Store and forward must be enabled on both client and router devices.")
|
||||
|
|
@ -66,7 +66,7 @@ struct StoreForwardConfig: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if isRouter {
|
||||
Section(header: Text("Router Options")) {
|
||||
Toggle(isOn: $heartbeat) {
|
||||
|
|
@ -119,7 +119,7 @@ struct StoreForwardConfig: View {
|
|||
print("Failed to save isRouter")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var sfc = ModuleConfig.StoreForwardConfig()
|
||||
sfc.enabled = self.enabled
|
||||
sfc.heartbeat = self.heartbeat
|
||||
|
|
@ -144,7 +144,7 @@ struct StoreForwardConfig: View {
|
|||
if self.bleManager.context == nil {
|
||||
self.bleManager.context = context
|
||||
}
|
||||
|
||||
|
||||
// Need to request a Detection Sensor Module Config from the remote node before allowing changes
|
||||
if bleManager.connectedPeripheral != nil && node?.storeForwardConfig == nil {
|
||||
print("empty store and forward module config")
|
||||
|
|
|
|||
|
|
@ -24,13 +24,12 @@ struct TelemetryConfig: View {
|
|||
@State var powerMeasurementEnabled = false
|
||||
@State var powerUpdateInterval = 0
|
||||
@State var powerScreenEnabled = false
|
||||
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Form {
|
||||
ConfigHeader(title: "Telemetry", config: \.telemetryConfig, node: node, onAppear: setTelemetryValues)
|
||||
|
||||
|
||||
Section(header: Text("update.interval")) {
|
||||
Picker("Device Metrics", selection: $deviceUpdateInterval ) {
|
||||
ForEach(UpdateIntervals.allCases) { ui in
|
||||
|
|
@ -176,7 +175,7 @@ struct TelemetryConfig: View {
|
|||
if node != nil && node?.telemetryConfig != nil {
|
||||
if newPowerUpdateInterval != node!.telemetryConfig!.powerUpdateInterval { hasChanges = true }
|
||||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: powerScreenEnabled) { newPowerScreenEnabled in
|
||||
if node != nil && node?.telemetryConfig != nil {
|
||||
if newPowerScreenEnabled != node!.telemetryConfig!.powerScreenEnabled { hasChanges = true }
|
||||
|
|
|
|||
|
|
@ -28,16 +28,16 @@ struct NetworkConfig: View {
|
|||
VStack {
|
||||
Form {
|
||||
ConfigHeader(title: "Network", config: \.networkConfig, node: node, onAppear: setNetworkValues)
|
||||
|
||||
if (node != nil && node?.metadata?.hasWifi ?? false) {
|
||||
|
||||
if node != nil && node?.metadata?.hasWifi ?? false {
|
||||
Section(header: Text("WiFi Options")) {
|
||||
|
||||
|
||||
Toggle(isOn: $wifiEnabled) {
|
||||
Label("enabled", systemImage: "wifi")
|
||||
Text("Enabling WiFi will disable the bluetooth connection to the app.")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
|
||||
|
||||
HStack {
|
||||
Label("ssid", systemImage: "network")
|
||||
TextField("ssid", text: $wifiSsid)
|
||||
|
|
@ -74,7 +74,7 @@ struct NetworkConfig: View {
|
|||
.keyboardType(.default)
|
||||
}
|
||||
}
|
||||
if (node != nil && node?.metadata?.hasEthernet ?? false) {
|
||||
if node != nil && node?.metadata?.hasEthernet ?? false {
|
||||
Section(header: Text("Ethernet Options")) {
|
||||
Toggle(isOn: $ethEnabled) {
|
||||
Label("enabled", systemImage: "network")
|
||||
|
|
|
|||
|
|
@ -76,8 +76,8 @@ struct PositionConfig: View {
|
|||
@State var minimumVersion = "2.3.3"
|
||||
@State private var supportedVersion = true
|
||||
@State private var showingSetFixedAlert = false
|
||||
//@State private var showingRemoveFixedAlert = false
|
||||
|
||||
// @State private var showingRemoveFixedAlert = false
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Form {
|
||||
|
|
@ -98,12 +98,12 @@ struct PositionConfig: View {
|
|||
.foregroundColor(.gray)
|
||||
.font(.callout)
|
||||
}
|
||||
|
||||
|
||||
Toggle(isOn: $smartPositionEnabled) {
|
||||
Label("Smart Position", systemImage: "brain")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
|
||||
|
||||
if smartPositionEnabled {
|
||||
VStack(alignment: .leading) {
|
||||
Picker("Minimum Interval", selection: $broadcastSmartMinimumIntervalSecs) {
|
||||
|
|
@ -147,8 +147,7 @@ struct PositionConfig: View {
|
|||
.padding(.top, 5)
|
||||
.padding(.bottom, 5)
|
||||
if gpsMode == 1 {
|
||||
|
||||
|
||||
|
||||
Text("Positions will be provided by your device GPS, if you select disabled or not present you can set a fixed position.")
|
||||
.foregroundColor(.gray)
|
||||
.font(.callout)
|
||||
|
|
@ -170,7 +169,7 @@ struct PositionConfig: View {
|
|||
if !(node?.positionConfig?.fixedPosition ?? false) {
|
||||
Text("Your current location will be set as the fixed position and broadcast over the mesh on the position interval.")
|
||||
} else {
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
|
|
@ -215,7 +214,7 @@ struct PositionConfig: View {
|
|||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
}
|
||||
Section(header: Text("Advanced Position Flags")) {
|
||||
|
||||
|
||||
if includeAltitude {
|
||||
Toggle(isOn: $includeAltitudeMsl) {
|
||||
Label("Altitude is Mean Sea Level", systemImage: "arrow.up.to.line.compact")
|
||||
|
|
@ -239,7 +238,7 @@ struct PositionConfig: View {
|
|||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if gpsMode == 1 {
|
||||
Section(header: Text("Advanced Device GPS")) {
|
||||
Picker("GPS Receive GPIO", selection: $rxGpio) {
|
||||
|
|
|
|||
|
|
@ -17,9 +17,9 @@ struct PowerConfig: View {
|
|||
@State private var waitBluetoothSecs = 60
|
||||
@State private var lsSecs = 300
|
||||
@State private var minWakeSecs = 10
|
||||
|
||||
|
||||
@State private var currentDevice: DeviceHardware?
|
||||
|
||||
|
||||
@State private var hasChanges: Bool = false
|
||||
@FocusState private var isFocused: Bool
|
||||
|
||||
|
|
@ -28,7 +28,7 @@ struct PowerConfig: View {
|
|||
ConfigHeader(title: "config.power.title", config: \.powerConfig, node: node, onAppear: setPowerValues)
|
||||
|
||||
Section {
|
||||
if (currentDevice?.architecture == .esp32 || currentDevice?.architecture == .esp32S3) || (currentDevice?.architecture == .nrf52840 && (node?.deviceConfig?.role ?? 0 == 5 || node?.deviceConfig?.role ?? 0 == 6)) {
|
||||
if (currentDevice?.architecture == .esp32 || currentDevice?.architecture == .esp32S3) || (currentDevice?.architecture == .nrf52840 && (node?.deviceConfig?.role ?? 0 == 5 || node?.deviceConfig?.role ?? 0 == 6)) {
|
||||
Toggle(isOn: $isPowerSaving) {
|
||||
Label("config.power.saving", systemImage: "bolt")
|
||||
Text("config.power.saving.description")
|
||||
|
|
@ -120,13 +120,13 @@ struct PowerConfig: View {
|
|||
if self.bleManager.context == nil {
|
||||
self.bleManager.context = context
|
||||
}
|
||||
|
||||
|
||||
Api().loadDeviceHardwareData { (hw) in
|
||||
|
||||
|
||||
for device in hw {
|
||||
let currentHardware = node?.user?.hwModel ?? "UNSET"
|
||||
let deviceString = device.hwModelSlug.replacingOccurrences(of: "_", with: "")
|
||||
if deviceString == currentHardware {
|
||||
if deviceString == currentHardware {
|
||||
currentDevice = device
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,14 +17,14 @@ struct Firmware: View {
|
|||
@State private var currentDevice: DeviceHardware?
|
||||
@State private var latestStable: FirmwareRelease?
|
||||
@State private var latestAlpha: FirmwareRelease?
|
||||
|
||||
|
||||
var body: some View {
|
||||
|
||||
|
||||
let supportedVersion = self.minimumVersion.compare(bleManager.connectedVersion, options: .numeric) == .orderedAscending || minimumVersion.compare(bleManager.connectedVersion, options: .numeric) == .orderedSame
|
||||
ScrollView {
|
||||
VStack(alignment: .leading) {
|
||||
let deviceString = currentDevice?.hwModelSlug.replacingOccurrences(of: "_", with: "")
|
||||
|
||||
|
||||
HStack {
|
||||
VStack {
|
||||
Image(systemName: currentDevice?.activelySupported ?? false ? "checkmark.seal.fill" : "x.circle")
|
||||
|
|
@ -45,7 +45,7 @@ struct Firmware: View {
|
|||
.frame(width: 300, height: 300)
|
||||
.cornerRadius(5)
|
||||
}
|
||||
|
||||
|
||||
if supportedVersion {
|
||||
Text("Your Firmware is up to date")
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
|
@ -72,7 +72,7 @@ struct Firmware: View {
|
|||
.fixedSize(horizontal: false, vertical: true)
|
||||
.font(.title2)
|
||||
.padding(.bottom)
|
||||
|
||||
|
||||
Text("Get the latest stable firmware")
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.font(.callout)
|
||||
|
|
@ -81,7 +81,7 @@ struct Firmware: View {
|
|||
Link("Release Notes", destination: URL(string: "\(latestStable?.pageURL ?? "https://meshtastic.org")")!)
|
||||
.font(.caption)
|
||||
.padding(.bottom)
|
||||
|
||||
|
||||
Text("Get the latest alpha firmware")
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.font(.callout)
|
||||
|
|
@ -90,10 +90,10 @@ struct Firmware: View {
|
|||
Link("Release Notes", destination: URL(string: "\(latestAlpha?.pageURL ?? "https://meshtastic.org")")!)
|
||||
.font(.caption)
|
||||
.padding(.bottom)
|
||||
|
||||
|
||||
if currentDevice?.architecture == Meshtastic.Architecture.nrf52840 {
|
||||
VStack(alignment: .leading) {
|
||||
|
||||
|
||||
Text("Drag & Drop is the recommended way to update firmware for NRF devices. If your iPhone or iPad is USB-C it will work with your regular USB-C charging cable, for lightning devices you need the Apple Lightning to USB camera adaptor.")
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.foregroundStyle(.gray)
|
||||
|
|
@ -109,7 +109,7 @@ struct Firmware: View {
|
|||
Button {
|
||||
let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral?.num ?? 0, context: context)
|
||||
if connectedNode != nil {
|
||||
|
||||
|
||||
if bleManager.sendEnterDfuMode(fromUser: connectedNode!.user!, toUser: node!.user!) {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||||
bleManager.disconnectPeripheral(reconnect: false)
|
||||
|
|
@ -178,18 +178,18 @@ struct Firmware: View {
|
|||
.font(.title3)
|
||||
Text(node?.user?.hwModel ?? "UNSET")
|
||||
.font(.title3)
|
||||
Text ( currentDevice?.architecture.rawValue ?? "UNKNOWN")
|
||||
Text( currentDevice?.architecture.rawValue ?? "UNKNOWN")
|
||||
.font(.title3)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.padding(.bottom, 5)
|
||||
.onAppear() {
|
||||
.onAppear {
|
||||
Api().loadDeviceHardwareData { (hw) in
|
||||
for device in hw {
|
||||
let currentHardware = node?.user?.hwModel ?? "UNSET"
|
||||
let deviceString = device.hwModelSlug.replacingOccurrences(of: "_", with: "")
|
||||
if deviceString == currentHardware {
|
||||
if deviceString == currentHardware {
|
||||
currentDevice = device
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue