diff --git a/Meshtastic Client.xcodeproj/project.pbxproj b/Meshtastic Client.xcodeproj/project.pbxproj index 90634861..1620757e 100644 --- a/Meshtastic Client.xcodeproj/project.pbxproj +++ b/Meshtastic Client.xcodeproj/project.pbxproj @@ -20,8 +20,6 @@ DD47E3CE26F103C600029299 /* NodeList.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD47E3CD26F103C600029299 /* NodeList.swift */; }; DD47E3D626F17ED900029299 /* CircleText.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD47E3D526F17ED900029299 /* CircleText.swift */; }; DD47E3D926F3093800029299 /* MessageBubble.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD47E3D826F3093800029299 /* MessageBubble.swift */; }; - DD47E3DB26F3901B00029299 /* Channels.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD47E3DA26F3901A00029299 /* Channels.swift */; }; - DD47E3DD26F390A000029299 /* Messages.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD47E3DC26F390A000029299 /* Messages.swift */; }; DD4A911E2708C65400501B7E /* AppSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4A911D2708C65400501B7E /* AppSettings.swift */; }; DD4DED9027AD2975004BA27E /* cannedmessages.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4DED8F27AD2975004BA27E /* cannedmessages.pb.swift */; }; DD5394FC276993AD00AD86B1 /* SwiftProtobuf in Frameworks */ = {isa = PBXBuildFile; productRef = DD5394FB276993AD00AD86B1 /* SwiftProtobuf */; }; @@ -89,8 +87,6 @@ DD47E3CD26F103C600029299 /* NodeList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeList.swift; sourceTree = ""; }; DD47E3D526F17ED900029299 /* CircleText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircleText.swift; sourceTree = ""; }; DD47E3D826F3093800029299 /* MessageBubble.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageBubble.swift; sourceTree = ""; }; - DD47E3DA26F3901A00029299 /* Channels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Channels.swift; sourceTree = ""; }; - DD47E3DC26F390A000029299 /* Messages.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Messages.swift; sourceTree = ""; }; DD4A911D2708C65400501B7E /* AppSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettings.swift; sourceTree = ""; }; DD4DED8F27AD2975004BA27E /* cannedmessages.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = cannedmessages.pb.swift; sourceTree = ""; }; DD5394FD276BA0EF00AD86B1 /* PositionEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PositionEntityExtension.swift; sourceTree = ""; }; @@ -333,8 +329,6 @@ DDC2E18B26CE25A70042C5E4 /* Messages */ = { isa = PBXGroup; children = ( - DD47E3DA26F3901A00029299 /* Channels.swift */, - DD47E3DC26F390A000029299 /* Messages.swift */, DD882F5C2772E4640005BF05 /* Contacts.swift */, DD1BF2F82776FE2E008C8D2F /* UserMessageList.swift */, ); @@ -542,7 +536,6 @@ DDAF8C5326EB1DF10058C060 /* BLEManager.swift in Sources */, DDC4D568275499A500A4208E /* Persistence.swift in Sources */, DD90860E26F69BAE00DC5189 /* NodeMap.swift in Sources */, - DD47E3DB26F3901B00029299 /* Channels.swift in Sources */, DD8169FF272476C700F4AB02 /* LogDocument.swift in Sources */, DDAF8C6926ED0D070058C060 /* deviceonly.pb.swift in Sources */, DDAF8C6B26ED0DD80058C060 /* environmental_measurement.pb.swift in Sources */, @@ -573,7 +566,6 @@ DD539502276DAA6A00AD86B1 /* MapLocation.swift in Sources */, DDAF8C6726ED0C8C0058C060 /* remote_hardware.pb.swift in Sources */, DDAF8C6526ED0A490058C060 /* channel.pb.swift in Sources */, - DD47E3DD26F390A000029299 /* Messages.swift in Sources */, DDC2E15826CE248E0042C5E4 /* MeshtasticClientApp.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -738,7 +730,7 @@ CODE_SIGN_ENTITLEMENTS = MeshtasticClient/MeshtasticClient.entitlements; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 13; + CURRENT_PROJECT_VERSION = 15; DEVELOPMENT_ASSET_PATHS = "\"MeshtasticClient/Preview Content\""; DEVELOPMENT_TEAM = GCH7VS5Y9R; ENABLE_PREVIEWS = YES; @@ -769,7 +761,7 @@ CODE_SIGN_ENTITLEMENTS = MeshtasticClient/MeshtasticClient.entitlements; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 13; + CURRENT_PROJECT_VERSION = 15; DEVELOPMENT_ASSET_PATHS = "\"MeshtasticClient/Preview Content\""; DEVELOPMENT_TEAM = GCH7VS5Y9R; ENABLE_PREVIEWS = YES; diff --git a/MeshtasticClient/Helpers/BLEManager.swift b/MeshtasticClient/Helpers/BLEManager.swift index 50ba0728..31c5bfef 100644 --- a/MeshtasticClient/Helpers/BLEManager.swift +++ b/MeshtasticClient/Helpers/BLEManager.swift @@ -98,7 +98,6 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph self.centralManager.stopScan() self.isScanning = self.centralManager.isScanning - print("🛑 Stopped Scanning") } } @@ -177,7 +176,7 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph peripheralName = name } - let newPeripheral = Peripheral(id: peripheral.identifier.uuidString, num: 0, name: peripheralName, shortName: String(peripheralName.suffix(3)), longName: peripheralName, firmwareVersion: "Unknown", rssi: RSSI.intValue, channelUtilization: nil, airTime: nil, subscribed: false, peripheral: peripheral) + let newPeripheral = Peripheral(id: peripheral.identifier.uuidString, num: 0, name: peripheralName, shortName: String(peripheralName.suffix(3)), longName: peripheralName, firmwareVersion: "Unknown", rssi: RSSI.intValue, channelUtilization: nil, airTime: nil, lastUpdate: Date(), subscribed: false, peripheral: peripheral) let peripheralIndex = peripherals.firstIndex(where: { $0.id == newPeripheral.id }) if peripheralIndex != nil && newPeripheral.peripheral.state != CBPeripheralState.connected { @@ -187,13 +186,17 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph peripherals.append(newPeripheral) } else { - + if newPeripheral.peripheral.state != CBPeripheralState.connected { peripherals.append(newPeripheral) print("ℹ️ Adding peripheral: \(peripheralName)") } } + + let today = Date() + let fiveMinutesAgo = Calendar.current.date(byAdding: .minute, value: -5, to: today)! + peripherals.removeAll(where: { $0.lastUpdate <= fiveMinutesAgo}) } // Called when a peripheral is connected @@ -379,28 +382,15 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph } switch characteristic.uuid { - case FROMNUM_UUID: - //Dont need to call readValue here, the value is already here from the notify - //readValue in turn calls this callback so we end up spamming the node with fromnum reads continuously - //peripheral.readValue(for: FROMNUM_characteristic) - - let characteristicValue: [UInt8] = [UInt8](characteristic.value!) - let bigEndianUInt32 = characteristicValue.withUnsafeBytes { $0.load(as: UInt32.self) } - let returnValue = CFByteOrderGetCurrent() == CFByteOrder(CFByteOrderLittleEndian.rawValue) - ? UInt32(bigEndian: bigEndianUInt32) : bigEndianUInt32 - // print(returnValue) case FROMRADIO_UUID: if characteristic.value == nil || characteristic.value!.isEmpty { return } - // print(characteristic.value ?? "no value") - // print(characteristic.value?.hexDescription ?? "no value") + var decodedInfo = FromRadio() decodedInfo = try! FromRadio(serializedData: characteristic.value!) - // print("Print DecodedInfo") - // print(decodedInfo) // MARK: Incoming MyInfo Packet if decodedInfo.myInfo.myNodeNum != 0 { @@ -417,8 +407,8 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph myInfo.hasGps = decodedInfo.myInfo.hasGps_p myInfo.channelUtilization = decodedInfo.myInfo.channelUtilization - // Swift does strings weird, this does work - let lastDotIndex = decodedInfo.myInfo.firmwareVersion.lastIndex(of: ".")//.lastIndex(of: ".", offsetBy: -1) + // Swift does strings weird, this does work to get the version without the github hash + let lastDotIndex = decodedInfo.myInfo.firmwareVersion.lastIndex(of: ".") var version = decodedInfo.myInfo.firmwareVersion[...(lastDotIndex ?? String.Index(utf16Offset: 6, in: decodedInfo.myInfo.firmwareVersion))] version = version.dropLast() myInfo.firmwareVersion = String(version) @@ -494,15 +484,18 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph print("💥 Fetch MyInfo Error") } - // Use a timer to keep track of position updates, context to pass the radio name with the timer and the RunLoop to prevent - // the timer from running on the main UI thread - if self.positionTimer != nil { - self.positionTimer!.invalidate() + // MARK: Share Location Position Update Timer + // Use context to pass the radio name with the timer + // Use a RunLoop to prevent the timer from running on the main UI thread + if userSettings?.provideLocation ?? false { + + if self.positionTimer != nil { + self.positionTimer!.invalidate() + } + let context = ["name": "@\(peripheral.name ?? "Unknown")"] + self.positionTimer = Timer.scheduledTimer(timeInterval: TimeInterval((userSettings?.provideLocationInterval ?? 900)), target: self, selector: #selector(positionTimerFired), userInfo: context, repeats: true) + RunLoop.current.add(self.positionTimer!, forMode: .common) } - let context = ["name": "@\(peripheral.name ?? "Unknown")"] - self.positionTimer = Timer.scheduledTimer(timeInterval: 30.0, target: self, selector: #selector(positionTimerFired), userInfo: context, repeats: true) - RunLoop.current.add(self.positionTimer!, forMode: .common) - } // MARK: Incoming Node Info Packet @@ -801,8 +794,8 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph try context!.save() - if meshLoggingEnabled { MeshLogger.log("💾 Updated NodeInfo SNR and Time from Node Info App Packet For: \(fetchedNode[0].num)")} - print("💾 Updated NodeInfo SNR and Time from Packet For: \(fetchedNode[0].num)") + if meshLoggingEnabled { MeshLogger.log("💾 Updated NodeInfo SNR \(decodedInfo.packet.rxSnr) and Time from Node Info App Packet For: \(fetchedNode[0].num)")} + print("💾 Updated NodeInfo SNR \(decodedInfo.packet.rxSnr) and Time from Packet For: \(fetchedNode[0].num)") } catch { @@ -873,9 +866,9 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph try context!.save() if meshLoggingEnabled { - MeshLogger.log("💾 Updated NodeInfo Position Coordinates, SNR and Time from Position App Packet For: \(fetchedNode[0].num)") + MeshLogger.log("💾 Updated NodeInfo Position Coordinates, SNR \(decodedInfo.packet.rxSnr) and Time from Position App Packet For: \(fetchedNode[0].num)") } - print("💾 Updated NodeInfo Position Coordinates, SNR and Time from Position App Packet For:: \(fetchedNode[0].num)") + print("💾 Updated NodeInfo Position Coordinates, SNR \(decodedInfo.packet.rxSnr) and Time from Position App Packet For:: \(fetchedNode[0].num)") } catch { @@ -919,10 +912,11 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph let nsError = error as NSError print("💥 Error Saving ACK for message MessageID \(decodedInfo.packet.id) Error: \(nsError)") } - } + } else { - if meshLoggingEnabled { MeshLogger.log("ℹ️ MESH PACKET received for Routing App UNHANDLED \(try decodedInfo.packet.jsonString())") } - print("ℹ️ MESH PACKET received for Routing App UNHANDLED \(try decodedInfo.packet.jsonString())") + if meshLoggingEnabled { MeshLogger.log("ℹ️ MESH PACKET received for Routing App UNHANDLED \(try decodedInfo.packet.jsonString())") } + print("ℹ️ MESH PACKET received for Routing App UNHANDLED \(try decodedInfo.packet.jsonString())") + } } else if decodedInfo.packet.decoded.portnum == PortNum.environmentalMeasurementApp { @@ -1194,10 +1188,7 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph print("Failed to send positon to device") } - } - // Request config to update MyNodeInfo data periodically as well as all nodes - } } } diff --git a/MeshtasticClient/Model/PeripheralModel.swift b/MeshtasticClient/Model/PeripheralModel.swift index ea6a7c7a..1b921104 100644 --- a/MeshtasticClient/Model/PeripheralModel.swift +++ b/MeshtasticClient/Model/PeripheralModel.swift @@ -11,10 +11,11 @@ struct Peripheral: Identifiable { var rssi: Int var channelUtilization: Float? var airTime: Float? + var lastUpdate: Date var subscribed: Bool var peripheral: CBPeripheral - init(id: String, num: Int64, name: String, shortName: String, longName: String, firmwareVersion: String, rssi: Int, channelUtilization: Float?, airTime: Float?, subscribed: Bool, peripheral: CBPeripheral) { + init(id: String, num: Int64, name: String, shortName: String, longName: String, firmwareVersion: String, rssi: Int, channelUtilization: Float?, airTime: Float?, lastUpdate: Date, subscribed: Bool, peripheral: CBPeripheral) { self.id = id self.num = num self.name = name @@ -24,6 +25,7 @@ struct Peripheral: Identifiable { self.rssi = rssi self.channelUtilization = channelUtilization self.airTime = airTime + self.lastUpdate = lastUpdate self.subscribed = subscribed self.peripheral = peripheral } diff --git a/MeshtasticClient/Views/Messages/Channels.swift b/MeshtasticClient/Views/Messages/Channels.swift deleted file mode 100644 index 3068fa13..00000000 --- a/MeshtasticClient/Views/Messages/Channels.swift +++ /dev/null @@ -1,44 +0,0 @@ -//import Foundation -//import SwiftUI -//import CoreBluetooth -// -//struct Channels: View { -// -// @State private var isShowingDetailView = true -// -// var body: some View { -// -// NavigationView { -// -// NavigationLink(destination: Messages(), isActive: $isShowingDetailView) { -// -// List { -// -// HStack { -// -// Image(systemName: "megaphone.fill") -// .font(.largeTitle) -// .symbolRenderingMode(.hierarchical) -// .padding(.trailing) -// .foregroundColor(.accentColor) -// -// Text("All - Broadcast") -// .font(.largeTitle) -// -// }.padding() -// } -// } -// .navigationTitle("Contacts") -// } -// .navigationViewStyle(DoubleColumnNavigationViewStyle()) -// } -//} -// -//struct MessageList_Previews: PreviewProvider { -// -// static var previews: some View { -// Group { -// Channels() -// } -// } -//} diff --git a/MeshtasticClient/Views/Messages/Messages.swift b/MeshtasticClient/Views/Messages/Messages.swift deleted file mode 100644 index 6ffe5aae..00000000 --- a/MeshtasticClient/Views/Messages/Messages.swift +++ /dev/null @@ -1,204 +0,0 @@ -//import SwiftUI -//import MapKit -//import Foundation -//import CoreLocation -// -//struct Messages: View { -// -// enum Field: Hashable { -// case messageText -// } -// -// // CoreData -// @Environment(\.managedObjectContext) var context -// @EnvironmentObject var bleManager: BLEManager -// -// @FetchRequest( -// sortDescriptors: [NSSortDescriptor(keyPath: \MessageEntity.messageTimestamp, ascending: true)], -// animation: .default) -// var messages: FetchedResults -// -// // Keyboard State -// @State var typingMessage: String = "" -// @State private var totalBytes = 0 -// private var maxbytes = 228 -// @State private var lastTypingMessage = "" -// @FocusState private var focusedField: Field? -// -// @State var showDeleteMessageAlert = false -// @State private var deleteMessageId: Int64 = 0 -// -// public var broadcastNodeId: UInt32 = 4294967295 -// -// var body: some View { -// -// Text("\(messages.count) Messages").font(.caption) -// GeometryReader { bounds in -// -// VStack { -// -// ScrollViewReader { scrollView in -// -// if self.messages.count > 0 { -// -// ScrollView { -// -// ForEach(messages) { message in -// -// HStack(alignment: .top) { -// let currentUser: Bool = (bleManager.connectedPeripheral == nil) ? false : ((bleManager.connectedPeripheral != nil && bleManager.connectedPeripheral.num == message.fromUser?.num) ? true : false ) -// -// CircleText(text: (message.fromUser?.shortName ?? "???"), color: currentUser ? .accentColor : Color(.darkGray)).padding(.all, 5) -// .gesture(LongPressGesture(minimumDuration: 2) -// .onEnded {_ in -// print(messages) -// print("I want to delete message: \(message.messageId)") -// self.showDeleteMessageAlert = true -// self.deleteMessageId = message.messageId -// -// print(deleteMessageId) -// }) -// -// VStack(alignment: .leading) { -// Text(message.messagePayload ?? "EMPTY MESSAGE") -// .textSelection(.enabled) -// .padding(10) -// .foregroundColor(.white) -// .background(currentUser ? Color.blue : Color(.darkGray)) -// .cornerRadius(10) -// HStack(spacing: 4) { -// -// let time = Int32(message.messageTimestamp) -// let messageDate = Date(timeIntervalSince1970: TimeInterval(time)) -// -// if time != 0 { -// Text(messageDate, style: .date).font(.caption2).foregroundColor(.gray) -// Text(messageDate, style: .time).font(.caption2).foregroundColor(.gray) -// } else { -// Text("Unknown").font(.caption2).foregroundColor(.gray) -// } -// } -// .padding(.bottom, 10) -// } -// Spacer() -// } -// .alert(isPresented: $showDeleteMessageAlert) { -// Alert(title: Text("Are you sure you want to delete this message?"), message: Text("This action is permanent."), -// primaryButton: .destructive(Text("Delete")) { -// print("OK button tapped") -// if deleteMessageId > 0 { -// -// let message = messages.first(where: { $0.messageId == deleteMessageId }) -// -// context.delete(message!) -// do { -// try context.save() -// -// deleteMessageId = 0 -// -// } catch { -// print("Failed to delete message \(deleteMessageId)") -// } -// -// } -// }, -// secondaryButton: .cancel() -// ) -// } -// } -// .onChange(of: messages.count, perform: { newValue in -// -// if messages.count > 0 { -// -// scrollView.scrollTo(messages[messages.count-1].id, anchor: .bottom) -// } -// } -// ) -// .onAppear(perform: { -// -// self.bleManager.context = context -// if messages.count > 0 { -// -// scrollView.scrollTo(messages[messages.count-1].id, anchor: .bottom) -// } -// }) -// } -// .padding(.horizontal) -// } -// } -// -// HStack(alignment: .top) { -// -// ZStack { -// -// let kbType = UIKeyboardType(rawValue: UserDefaults.standard.object(forKey: "keyboardType") as? Int ?? 0) -// TextEditor(text: $typingMessage) -// .onChange(of: typingMessage, perform: { value in -// -// let size = value.utf8.count -// totalBytes = size -// if totalBytes <= maxbytes { -// // Allow the user to type -// lastTypingMessage = typingMessage -// } else { -// // Set the message back and remove the bytes over the count -// self.typingMessage = lastTypingMessage -// } -// }) -// .keyboardType(kbType!) -// .toolbar { -// ToolbarItemGroup(placement: .keyboard) { -// -// Button("Dismiss Keyboard") { -// focusedField = nil -// } -// .font(.subheadline) -// -// Spacer() -// -// ProgressView("Bytes: \(totalBytes) / \(maxbytes)", value: Double(totalBytes), total: Double(maxbytes)) -// .frame(width: 130) -// .padding(5) -// .font(.subheadline) -// .accentColor(.accentColor) -// } -// } -// .padding(.horizontal, 8) -// .focused($focusedField, equals: .messageText) -// .multilineTextAlignment(.leading) -// .frame(minHeight: bounds.size.height / 4, maxHeight: bounds.size.height / 4) -// -// Text(typingMessage).opacity(0).padding(.all, 0) -// -// } -// .overlay(RoundedRectangle(cornerRadius: 20).stroke(.tertiary, lineWidth: 1)) -// .padding(.bottom, 15) -// -// Button(action: { -// if self.bleManager.sendMessage(message: typingMessage, toUserNum: Int64(self.bleManager.broadcastNodeNum)) { -// typingMessage = "" -// focusedField = nil -// } -// -// }) { -// Image(systemName: "arrow.up.circle.fill").font(.largeTitle).foregroundColor(.blue) -// } -// -// } -// .padding(.all, 15) -// } -// } -// .navigationTitle("All - Broadcast") -// .navigationBarTitleDisplayMode(.inline) -// .navigationBarItems(trailing: -// -// ZStack { -// -// ConnectedDevice( -// bluetoothOn: self.bleManager.isSwitchedOn, -// deviceConnected: self.bleManager.connectedPeripheral != nil, -// name: (self.bleManager.connectedPeripheral != nil) ? self.bleManager.connectedPeripheral.shortName : "???") -// } -// ) -// } -//} diff --git a/MeshtasticClient/Views/Messages/UserMessageList.swift b/MeshtasticClient/Views/Messages/UserMessageList.swift index 1a6615fb..f9308824 100644 --- a/MeshtasticClient/Views/Messages/UserMessageList.swift +++ b/MeshtasticClient/Views/Messages/UserMessageList.swift @@ -335,20 +335,17 @@ struct UserMessageList: View { self.bleManager.context = context self.bleManager.userSettings = userSettings - if allMessages.count > 2 { + if allMessages.count > 0 { - scrollView.scrollTo(allMessages.firstIndex(of: allMessages.last! ), anchor: .bottom) + withAnimation(Animation.spring().delay(0.5)) { + scrollView.scrollTo(allMessages.firstIndex(of: allMessages.last! ), anchor: .bottom) + } } }) .onChange(of: allMessages.count, perform: { count in - //self.context.refresh(user, mergeChanges: true) - - let index = count - 1 - - if index > 3 { - - scrollView.scrollTo(index, anchor: .bottom) + withAnimation(Animation.spring().delay(0.5)) { + scrollView.scrollTo(allMessages.firstIndex(of: allMessages.last! ), anchor: .bottom) } }) } diff --git a/MeshtasticClient/Views/Settings/AppSettings.swift b/MeshtasticClient/Views/Settings/AppSettings.swift index 3b4f286b..caf7f255 100644 --- a/MeshtasticClient/Views/Settings/AppSettings.swift +++ b/MeshtasticClient/Views/Settings/AppSettings.swift @@ -53,6 +53,31 @@ enum MeshMapType: String, CaseIterable, Identifiable { } } +enum LocationUpdateInterval: Int, CaseIterable, Identifiable { + + case oneMinute = 60 + case fiveMinutes = 300 + case tenMinutes = 600 + case fifteenMinutes = 900 + + var id: Int { self.rawValue } + var description: String { + get { + switch self { + case .oneMinute: + return "One Minute" + case .fiveMinutes: + return "Five Minutes" + case .tenMinutes: + return "Ten Minutes" + case .fifteenMinutes: + return "Fifteen Minutes" + } + } + } +} + + class UserSettings: ObservableObject { @Published var meshtasticUsername: String { didSet { @@ -74,6 +99,11 @@ class UserSettings: ObservableObject { UserDefaults.standard.set(provideLocation, forKey: "provideLocation") } } + @Published var provideLocationInterval: Int { + didSet { + UserDefaults.standard.set(provideLocationInterval, forKey: "provideLocationInterval") + } + } @Published var keyboardType: Int { didSet { UserDefaults.standard.set(keyboardType, forKey: "keyboardType") @@ -90,7 +120,6 @@ class UserSettings: ObservableObject { UserDefaults.standard.set(meshMapType, forKey: "meshMapType") } } - @Published var meshMapCustomTileServer: String { didSet { UserDefaults.standard.set(meshMapCustomTileServer, forKey: "meshMapCustomTileServer") @@ -103,6 +132,7 @@ class UserSettings: ObservableObject { self.preferredPeripheralName = UserDefaults.standard.object(forKey: "preferredPeripheralName") as? String ?? "" self.preferredPeripheralId = UserDefaults.standard.object(forKey: "preferredPeripheralId") as? String ?? "" self.provideLocation = UserDefaults.standard.object(forKey: "provideLocation") as? Bool ?? false + self.provideLocationInterval = UserDefaults.standard.object(forKey: "provideLocationInterval") as? Int ?? 900 self.keyboardType = UserDefaults.standard.object(forKey: "keyboardType") as? Int ?? 0 self.meshActivityLog = UserDefaults.standard.object(forKey: "meshActivityLog") as? Bool ?? false self.meshMapType = UserDefaults.standard.string(forKey: "meshMapType") ?? "hybrid" @@ -139,23 +169,42 @@ struct AppSettings: View { .keyboardType(.asciiCapable) .disableAutocorrection(true) .listRowSeparator(.visible) + + HStack { + Label("Radio", systemImage: "flipphone") + Text(userSettings.preferredPeripheralName) + .foregroundColor(.gray) + + } + Text("This option is set via the preferred radio toggle for the connected device on the bluetooth tab.") + .font(.caption) + .listRowSeparator(.hidden) + Text("The preferred radio will automatically reconnect if it becomes disconnected and is still within range.") + .font(.caption2) + .foregroundColor(.gray) + + } + Section(header: Text("LOCATION OPTIONS")) { + Toggle(isOn: $userSettings.provideLocation) { Label("Provide location to mesh", systemImage: "location.circle.fill") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - .listRowSeparator(.visible) - Label("Preferred Radio", systemImage: "flipphone") - .listRowSeparator(.hidden) - Text(userSettings.preferredPeripheralName) - .foregroundColor(.gray) - Text("This option is set via the preferred radio toggle for the connected device on the bluetooth tab.") - .font(.caption) - .listRowSeparator(.hidden) - Text("The preferred radio will automatically reconnect if it becomes disconnected and is still within range. This device is assumed to be the primary radio used for messaging.") - .font(.caption2) - .foregroundColor(.gray) - + + if userSettings.provideLocation { + + Picker(" Update Interval", selection: $userSettings.provideLocationInterval) { + ForEach(LocationUpdateInterval.allCases) { lu in + Text(lu.description) + } + } + .pickerStyle(DefaultPickerStyle()) + + Text("How frequently your phone will send your location to the device, location updates to the mesh are managed by the device.") + .font(.caption) + .listRowSeparator(.visible) + } } Section(header: Text("MESSAGING OPTIONS")) { @@ -181,10 +230,10 @@ struct AppSettings: View { Label("Log all Mesh activity", systemImage: "network") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - if userSettings.meshActivityLog { - NavigationLink(destination: MeshLog()) { - Text("View Mesh Log") - } + if userSettings.meshActivityLog { + NavigationLink(destination: MeshLog()) { + Text("View Mesh Log") + } .listRowSeparator(.visible) } }