mirror of
https://github.com/meshtastic/Meshtastic-Apple.git
synced 2026-04-20 22:13:56 +00:00
Merge remote-tracking branch 'refs/remotes/origin/2.6.12'
This commit is contained in:
commit
870fe5a13c
12 changed files with 381 additions and 112 deletions
|
|
@ -4160,6 +4160,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"App Notifications" : {
|
||||
|
||||
},
|
||||
"App Settings" : {
|
||||
"localizations" : {
|
||||
|
|
@ -8257,6 +8260,12 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Configure Location Permissions" : {
|
||||
|
||||
},
|
||||
"Configure notification permissions" : {
|
||||
|
||||
},
|
||||
"Confirm" : {
|
||||
"localizations" : {
|
||||
|
|
@ -9209,6 +9218,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Critical Alerts" : {
|
||||
|
||||
},
|
||||
"Current" : {
|
||||
"localizations" : {
|
||||
|
|
@ -16022,6 +16034,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Get started" : {
|
||||
|
||||
},
|
||||
"Get the latest alpha firmware" : {
|
||||
"localizations" : {
|
||||
|
|
@ -20796,6 +20811,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Meshtastic" : {
|
||||
|
||||
},
|
||||
"Meshtastic Node %@ has shared channels with you" : {
|
||||
"localizations" : {
|
||||
|
|
@ -20830,6 +20848,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Meshtastic uses your phone's location to enable a number of features. You can update your location permissions at any time from Settings > App Settings > Open Settings." : {
|
||||
|
||||
},
|
||||
"Meshtastic® Copyright Meshtastic LLC" : {
|
||||
"localizations" : {
|
||||
|
|
@ -26059,6 +26080,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Phone Location" : {
|
||||
|
||||
},
|
||||
"Pin %lld" : {
|
||||
"localizations" : {
|
||||
|
|
@ -32107,6 +32131,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Send Notifications" : {
|
||||
|
||||
},
|
||||
"Send Reboot OTA" : {
|
||||
"localizations" : {
|
||||
|
|
@ -41148,6 +41175,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Welcome to" : {
|
||||
|
||||
},
|
||||
"What does the lock mean?" : {
|
||||
"localizations" : {
|
||||
|
|
|
|||
|
|
@ -147,6 +147,7 @@
|
|||
DD6F65792C6EADE60053C113 /* DirectMessagesHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6F65782C6EADE60053C113 /* DirectMessagesHelp.swift */; };
|
||||
DD6F657B2C6EC2900053C113 /* LockLegend.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6F657A2C6EC2900053C113 /* LockLegend.swift */; };
|
||||
DD73FD1128750779000852D6 /* PositionLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD73FD1028750779000852D6 /* PositionLog.swift */; };
|
||||
DD74ED0D2DC6A0C90059AC10 /* DeviceOnboarding.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD74ED0C2DC6A0B80059AC10 /* DeviceOnboarding.swift */; };
|
||||
DD769E0328D18BF1001A3F05 /* DeviceMetricsLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD769E0228D18BF0001A3F05 /* DeviceMetricsLog.swift */; };
|
||||
DD77093B2AA1ABB8007A8BF0 /* BluetoothTips.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD77093A2AA1ABB8007A8BF0 /* BluetoothTips.swift */; };
|
||||
DD77093D2AA1AFA3007A8BF0 /* ChannelTips.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD77093C2AA1AFA3007A8BF0 /* ChannelTips.swift */; };
|
||||
|
|
@ -202,7 +203,6 @@
|
|||
DDC2E15C26CE248F0042C5E4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DDC2E15B26CE248F0042C5E4 /* Assets.xcassets */; };
|
||||
DDC2E15F26CE248F0042C5E4 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DDC2E15E26CE248F0042C5E4 /* Preview Assets.xcassets */; };
|
||||
DDC2E18F26CE25FE0042C5E4 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC2E18E26CE25FE0042C5E4 /* ContentView.swift */; };
|
||||
DDC2E1A726CEB3400042C5E4 /* LocationHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC2E1A626CEB3400042C5E4 /* LocationHelper.swift */; };
|
||||
DDC4C9FF2A8D982900CE201C /* DetectionSensorConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC4C9FE2A8D982900CE201C /* DetectionSensorConfig.swift */; };
|
||||
DDC4D568275499A500A4208E /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC4D567275499A500A4208E /* Persistence.swift */; };
|
||||
DDC94FC129CE063B0082EA6E /* BatteryLevel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC94FC029CE063B0082EA6E /* BatteryLevel.swift */; };
|
||||
|
|
@ -447,6 +447,7 @@
|
|||
DD6F65782C6EADE60053C113 /* DirectMessagesHelp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectMessagesHelp.swift; sourceTree = "<group>"; };
|
||||
DD6F657A2C6EC2900053C113 /* LockLegend.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockLegend.swift; sourceTree = "<group>"; };
|
||||
DD73FD1028750779000852D6 /* PositionLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PositionLog.swift; sourceTree = "<group>"; };
|
||||
DD74ED0C2DC6A0B80059AC10 /* DeviceOnboarding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceOnboarding.swift; sourceTree = "<group>"; };
|
||||
DD769E0228D18BF0001A3F05 /* DeviceMetricsLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceMetricsLog.swift; sourceTree = "<group>"; };
|
||||
DD77093A2AA1ABB8007A8BF0 /* BluetoothTips.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothTips.swift; sourceTree = "<group>"; };
|
||||
DD77093C2AA1AFA3007A8BF0 /* ChannelTips.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelTips.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -516,7 +517,6 @@
|
|||
DDC2E15E26CE248F0042C5E4 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
|
||||
DDC2E16526CE248F0042C5E4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
DDC2E18E26CE25FE0042C5E4 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
||||
DDC2E1A626CEB3400042C5E4 /* LocationHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationHelper.swift; sourceTree = "<group>"; };
|
||||
DDC4C9FE2A8D982900CE201C /* DetectionSensorConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetectionSensorConfig.swift; sourceTree = "<group>"; };
|
||||
DDC4CA012A8DAA3800CE201C /* MeshtasticDataModelV16.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV16.xcdatamodel; sourceTree = "<group>"; };
|
||||
DDC4D567275499A500A4208E /* Persistence.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -875,6 +875,14 @@
|
|||
path = Help;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DD74ED0B2DC6A0900059AC10 /* Onboarding */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DD74ED0C2DC6A0B80059AC10 /* DeviceOnboarding.swift */,
|
||||
);
|
||||
path = Onboarding;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DD7709392AA1ABA1007A8BF0 /* Tips */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
|
@ -1014,11 +1022,12 @@
|
|||
DDC2E18726CE24E40042C5E4 /* Views */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DD6D5A312CA1176A00ED3032 /* Layouts */,
|
||||
DDC2E18D26CE25CB0042C5E4 /* Helpers */,
|
||||
DD47E3D726F2F21A00029299 /* Bluetooth */,
|
||||
DDC2E18D26CE25CB0042C5E4 /* Helpers */,
|
||||
DD6D5A312CA1176A00ED3032 /* Layouts */,
|
||||
DDC2E18B26CE25A70042C5E4 /* Messages */,
|
||||
DD47E3CA26F0E50300029299 /* Nodes */,
|
||||
DD74ED0B2DC6A0900059AC10 /* Onboarding */,
|
||||
DD4A911C2708C57100501B7E /* Settings */,
|
||||
DDC2E18E26CE25FE0042C5E4 /* ContentView.swift */,
|
||||
);
|
||||
|
|
@ -1094,7 +1103,6 @@
|
|||
DDD43FE12A78C86B0083A3E9 /* Mqtt */,
|
||||
DDAF8C5226EB1DF10058C060 /* BLEManager.swift */,
|
||||
DD1BEF492E0292220090CE24 /* KeychainHelper.swift */,
|
||||
DDC2E1A626CEB3400042C5E4 /* LocationHelper.swift */,
|
||||
DD913638270DFF4C00D7ACF3 /* LocalNotificationManager.swift */,
|
||||
DDA6B2E828419CF2003E8C16 /* MeshPackets.swift */,
|
||||
DD964FBC296E6B01007C176F /* EmojiOnlyTextField.swift */,
|
||||
|
|
@ -1425,7 +1433,6 @@
|
|||
6DEDA55A2A957B8E00321D2E /* DetectionSensorLog.swift in Sources */,
|
||||
DD798B072915928D005217CD /* ChannelMessageList.swift in Sources */,
|
||||
231B3F272D0885240069A07D /* MetricsColumnDetail.swift in Sources */,
|
||||
DDC2E1A726CEB3400042C5E4 /* LocationHelper.swift in Sources */,
|
||||
DD77093D2AA1AFA3007A8BF0 /* ChannelTips.swift in Sources */,
|
||||
6D825E622C34786C008DBEE4 /* CommonRegex.swift in Sources */,
|
||||
DD913639270DFF4C00D7ACF3 /* LocalNotificationManager.swift in Sources */,
|
||||
|
|
@ -1474,6 +1481,7 @@
|
|||
DDDB263F2AABEE20003AFCB7 /* NodeList.swift in Sources */,
|
||||
DDD5BB0B2C285E45007E03CA /* LogDetail.swift in Sources */,
|
||||
DDA0B6B2294CDC55001356EC /* Channels.swift in Sources */,
|
||||
DD74ED0D2DC6A0C90059AC10 /* DeviceOnboarding.swift in Sources */,
|
||||
DDE9659C2B1C3B6A00531070 /* RouteRecorder.swift in Sources */,
|
||||
B399E8A42B6F486400E4488E /* RetryButton.swift in Sources */,
|
||||
DDB8F4102A9EE5B400230ECE /* Messages.swift in Sources */,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"originHash" : "0dabe052e9e56f8514254d01df9aa7245e16b28a649d59bac6781d4ac9a79efa",
|
||||
"originHash" : "fd71b247ba909b0eb360db5530e1068363839c5e169dea6f6a9974b2d98276f4",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "cocoamqtt",
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ enum UserTrackingModes: Int, CaseIterable, Identifiable {
|
|||
var description: String {
|
||||
switch self {
|
||||
case .none:
|
||||
return "None".localized
|
||||
return "map.usertrackingmode.none".localized
|
||||
case .follow:
|
||||
return "Follow".localized
|
||||
case .followWithHeading:
|
||||
|
|
|
|||
|
|
@ -74,6 +74,8 @@ extension UserDefaults {
|
|||
case environmentEnableWeatherKit
|
||||
case enableAdministration
|
||||
case mapReportingOptIn
|
||||
case firstLaunch
|
||||
case showDeviceOnboarding
|
||||
case usageDataAndCrashReporting
|
||||
case testIntEnum
|
||||
}
|
||||
|
|
@ -159,6 +161,11 @@ extension UserDefaults {
|
|||
|
||||
@UserDefault(.usageDataAndCrashReporting, defaultValue: true)
|
||||
static var usageDataAndCrashReporting: Bool
|
||||
@UserDefault(.firstLaunch, defaultValue: true)
|
||||
static var firstLaunch: Bool
|
||||
|
||||
@UserDefault(.showDeviceOnboarding, defaultValue: false)
|
||||
static var showDeviceOnboarding: Bool
|
||||
|
||||
@UserDefault(.testIntEnum, defaultValue: .one)
|
||||
static var testIntEnum: TestIntEnum
|
||||
|
|
|
|||
|
|
@ -1103,6 +1103,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
isSubscribed = true
|
||||
allowDisconnect = true
|
||||
Logger.mesh.info("🤜 [BLE] Want Config Complete. ID:\(decodedInfo.configCompleteID, privacy: .public)")
|
||||
if UserDefaults.firstLaunch {
|
||||
UserDefaults.showDeviceOnboarding = true
|
||||
}
|
||||
if sendTime() {
|
||||
}
|
||||
peripherals.removeAll(where: { $0.peripheral.state == CBPeripheralState.disconnected })
|
||||
|
|
|
|||
|
|
@ -1,76 +0,0 @@
|
|||
import Foundation
|
||||
import CoreLocation
|
||||
import MapKit
|
||||
import OSLog
|
||||
|
||||
class LocationHelper: NSObject, ObservableObject, CLLocationManagerDelegate {
|
||||
static let shared = LocationHelper()
|
||||
var locationManager = CLLocationManager()
|
||||
|
||||
// @Published var region = MKCoordinateRegion()
|
||||
@Published var authorizationStatus: CLAuthorizationStatus?
|
||||
override init() {
|
||||
super.init()
|
||||
locationManager.delegate = self
|
||||
locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters
|
||||
locationManager.pausesLocationUpdatesAutomatically = true
|
||||
locationManager.allowsBackgroundLocationUpdates = true
|
||||
locationManager.activityType = .other
|
||||
}
|
||||
// Apple Park
|
||||
static let DefaultLocation = CLLocationCoordinate2D(latitude: 37.3346, longitude: -122.0090)
|
||||
static var currentLocation: CLLocationCoordinate2D {
|
||||
guard let location = shared.locationManager.location else {
|
||||
return DefaultLocation
|
||||
}
|
||||
return location.coordinate
|
||||
}
|
||||
static var satsInView: Int {
|
||||
// If we have a position we have a sat
|
||||
var sats = 1
|
||||
if shared.locationManager.location?.verticalAccuracy ?? 0 > 0 {
|
||||
sats = 4
|
||||
if 0...5 ~= shared.locationManager.location?.horizontalAccuracy ?? 0 {
|
||||
sats = 12
|
||||
} else if 6...15 ~= shared.locationManager.location?.horizontalAccuracy ?? 0 {
|
||||
sats = 10
|
||||
} else if 16...30 ~= shared.locationManager.location?.horizontalAccuracy ?? 0 {
|
||||
sats = 9
|
||||
} else if 31...45 ~= shared.locationManager.location?.horizontalAccuracy ?? 0 {
|
||||
sats = 7
|
||||
} else if 46...60 ~= shared.locationManager.location?.horizontalAccuracy ?? 0 {
|
||||
sats = 5
|
||||
}
|
||||
} else if shared.locationManager.location?.verticalAccuracy ?? 0 < 0 && 60...300 ~= shared.locationManager.location?.horizontalAccuracy ?? 0 {
|
||||
sats = 3
|
||||
} else if shared.locationManager.location?.verticalAccuracy ?? 0 < 0 && shared.locationManager.location?.horizontalAccuracy ?? 0 > 300 {
|
||||
sats = 2
|
||||
}
|
||||
return sats
|
||||
}
|
||||
|
||||
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
|
||||
switch manager.authorizationStatus {
|
||||
case .authorizedAlways:
|
||||
authorizationStatus = .authorizedAlways
|
||||
case .authorizedWhenInUse:
|
||||
authorizationStatus = .authorizedWhenInUse
|
||||
locationManager.requestLocation()
|
||||
case .restricted:
|
||||
authorizationStatus = .restricted
|
||||
case .denied:
|
||||
authorizationStatus = .denied
|
||||
case .notDetermined:
|
||||
authorizationStatus = .notDetermined
|
||||
locationManager.requestAlwaysAuthorization()
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
|
||||
|
||||
}
|
||||
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
|
||||
Logger.services.error("Location manager error: \(error.localizedDescription, privacy: .public)")
|
||||
}
|
||||
}
|
||||
|
|
@ -10,14 +10,14 @@ import CoreLocation
|
|||
import OSLog
|
||||
|
||||
// Shared state that manages the `CLLocationManager` and `CLBackgroundActivitySession`.
|
||||
@MainActor class LocationsHandler: ObservableObject {
|
||||
@MainActor class LocationsHandler: NSObject, ObservableObject, @preconcurrency CLLocationManagerDelegate {
|
||||
|
||||
static let shared = LocationsHandler() // Create a single, shared instance of the object.
|
||||
private let manager: CLLocationManager
|
||||
public var manager = CLLocationManager()
|
||||
private var background: CLBackgroundActivitySession?
|
||||
var enableSmartPosition: Bool = UserDefaults.enableSmartPosition
|
||||
|
||||
@Published var locationsArray: [CLLocation]
|
||||
@Published var locationsArray: [CLLocation] = [CLLocation]()
|
||||
@Published var isStationary = false
|
||||
@Published var count = 0
|
||||
@Published var isRecording = false
|
||||
|
|
@ -38,16 +38,29 @@ import OSLog
|
|||
UserDefaults.standard.set(backgroundActivity, forKey: "BGActivitySessionStarted")
|
||||
}
|
||||
}
|
||||
// The continuation we will use to asynchronously ask the user permission to track their location.
|
||||
private var permissionContinuation: CheckedContinuation<CLAuthorizationStatus, Never>?
|
||||
func requestLocationAlwaysPermissions() async -> CLAuthorizationStatus {
|
||||
return await withCheckedContinuation { continuation in
|
||||
self.permissionContinuation = continuation
|
||||
manager.requestAlwaysAuthorization()
|
||||
}
|
||||
}
|
||||
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
|
||||
// This is the line you need to add
|
||||
permissionContinuation?.resume(returning: manager.authorizationStatus)
|
||||
}
|
||||
|
||||
private init() {
|
||||
self.manager = CLLocationManager() // Creating a location manager instance is safe to call here in `MainActor`.
|
||||
override init() {
|
||||
super.init()
|
||||
self.manager.delegate = self
|
||||
self.manager.allowsBackgroundLocationUpdates = true
|
||||
locationsArray = [CLLocation]()
|
||||
}
|
||||
|
||||
func startLocationUpdates() {
|
||||
if self.manager.authorizationStatus == .notDetermined {
|
||||
self.manager.requestWhenInUseAuthorization()
|
||||
let status = self.manager.authorizationStatus
|
||||
guard status == .authorizedAlways || status == .authorizedWhenInUse else {
|
||||
return
|
||||
}
|
||||
Logger.services.info("📍 [App] Starting location updates")
|
||||
Task {
|
||||
|
|
|
|||
|
|
@ -1027,7 +1027,7 @@ func textMessageAppPacket(
|
|||
subtitle: "AKA \(newMessage.fromUser?.shortName ?? "?")",
|
||||
content: messageText!,
|
||||
target: "messages",
|
||||
path: "meshtastic:///messages?userNum=\(newMessage.fromUser?.num ?? 0)&messageId=\(newMessage.messageId)",
|
||||
path: "meshtastic:///messages?userNum=\(newMessage.fromUser?.num ?? 0)&messageId=\(newMessage.isEmoji ? newMessage.replyID : newMessage.messageId)",
|
||||
messageId: newMessage.messageId,
|
||||
channel: newMessage.channel,
|
||||
userNum: Int64(packet.from),
|
||||
|
|
@ -1058,7 +1058,7 @@ func textMessageAppPacket(
|
|||
subtitle: "AKA \(newMessage.fromUser?.shortName ?? "?")",
|
||||
content: messageText!,
|
||||
target: "messages",
|
||||
path: "meshtastic:///messages?channelId=\(newMessage.channel)&messageId=\(newMessage.messageId)",
|
||||
path: "meshtastic:///messages?channelId=\(newMessage.channel)&messageId=\(newMessage.isEmoji ? newMessage.replyID : newMessage.messageId)",
|
||||
messageId: newMessage.messageId,
|
||||
channel: newMessage.channel,
|
||||
userNum: Int64(newMessage.fromUser?.userId ?? "0"),
|
||||
|
|
|
|||
|
|
@ -27,20 +27,6 @@ struct Connect: View {
|
|||
@State var presentingSwitchPreferredPeripheral = false
|
||||
@State var selectedPeripherialId = ""
|
||||
|
||||
init () {
|
||||
let notificationCenter = UNUserNotificationCenter.current()
|
||||
notificationCenter.getNotificationSettings(completionHandler: { (settings) in
|
||||
if settings.authorizationStatus == .notDetermined {
|
||||
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound, .criticalAlert]) { success, error in
|
||||
if success {
|
||||
Logger.services.info("Notifications are all set!")
|
||||
} else if let error = error {
|
||||
Logger.services.error("\(error.localizedDescription, privacy: .public)")
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
VStack {
|
||||
|
|
|
|||
|
|
@ -5,11 +5,10 @@
|
|||
import SwiftUI
|
||||
|
||||
struct ContentView: View {
|
||||
@ObservedObject
|
||||
var appState: AppState
|
||||
@ObservedObject var appState: AppState
|
||||
|
||||
@ObservedObject
|
||||
var router: Router
|
||||
@ObservedObject var router: Router
|
||||
@State var isShowingDeviceOnboardingFlow: Bool = false
|
||||
|
||||
init(appState: AppState, router: Router) {
|
||||
self.appState = appState
|
||||
|
|
@ -58,6 +57,21 @@ struct ContentView: View {
|
|||
.font(.title)
|
||||
}
|
||||
.tag(NavigationState.Tab.settings)
|
||||
}.sheet(
|
||||
isPresented: $isShowingDeviceOnboardingFlow,
|
||||
onDismiss: {
|
||||
UserDefaults.firstLaunch = false
|
||||
}, content: {
|
||||
DeviceOnboarding()
|
||||
}
|
||||
)
|
||||
.onAppear {
|
||||
if UserDefaults.firstLaunch {
|
||||
isShowingDeviceOnboardingFlow = true
|
||||
}
|
||||
}
|
||||
.onChange(of: UserDefaults.showDeviceOnboarding) { newValue in
|
||||
isShowingDeviceOnboardingFlow = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
284
Meshtastic/Views/Onboarding/DeviceOnboarding.swift
Normal file
284
Meshtastic/Views/Onboarding/DeviceOnboarding.swift
Normal file
|
|
@ -0,0 +1,284 @@
|
|||
import CoreBluetooth
|
||||
import OSLog
|
||||
import SwiftUI
|
||||
import Foundation
|
||||
import MapKit
|
||||
|
||||
struct DeviceOnboarding: View {
|
||||
enum SetupGuide: Hashable {
|
||||
case notifications
|
||||
case location
|
||||
}
|
||||
|
||||
@EnvironmentObject var bleManager: BLEManager
|
||||
@State var navigationPath: [SetupGuide] = []
|
||||
@State var locationStatus = LocationsHandler.shared.manager.authorizationStatus
|
||||
|
||||
@Environment(\.dismiss) var dismiss
|
||||
|
||||
/// The Title View
|
||||
var title: some View {
|
||||
VStack {
|
||||
Text("Welcome to")
|
||||
.font(.title2.bold())
|
||||
.multilineTextAlignment(.center)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
Text("Meshtastic")
|
||||
.font(.largeTitle.bold())
|
||||
.multilineTextAlignment(.center)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
}
|
||||
|
||||
var welcomeView: some View {
|
||||
VStack {
|
||||
ScrollView(.vertical, showsIndicators: false) {
|
||||
VStack {
|
||||
// Title
|
||||
title
|
||||
.padding(.top)
|
||||
// Onboarding
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
makeRow(
|
||||
icon: "antenna.radiowaves.left.and.right",
|
||||
title: "Stay Connected Anywhere",
|
||||
subtitle: "Communicate off-the-grid with your friends and community without cell service."
|
||||
)
|
||||
|
||||
makeRow(
|
||||
icon: "point.3.connected.trianglepath.dotted",
|
||||
title: "Create Your Own Networks",
|
||||
subtitle: "Easily set up private mesh networks for secure and reliable communication in remote areas."
|
||||
)
|
||||
|
||||
makeRow(
|
||||
icon: "location",
|
||||
title: "Track and Share Locations",
|
||||
subtitle: "Share your location in real-time and keep your group coordinated with integrated GPS features."
|
||||
)
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.interactiveDismissDisabled()
|
||||
}
|
||||
Spacer()
|
||||
if bleManager.isSwitchedOn {
|
||||
Button {
|
||||
Task {
|
||||
await goToNextStep(after: nil)
|
||||
}
|
||||
} label: {
|
||||
Text("Get started")
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.buttonBorderShape(.capsule)
|
||||
.controlSize(.large)
|
||||
.padding()
|
||||
.buttonStyle(.borderedProminent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var notificationView: some View {
|
||||
VStack {
|
||||
VStack {
|
||||
Text("App Notifications")
|
||||
.font(.largeTitle.bold())
|
||||
.multilineTextAlignment(.center)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
Spacer()
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
Text("Send Notifications")
|
||||
.font(.title2.bold())
|
||||
.multilineTextAlignment(.center)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
makeRow(
|
||||
icon: "message",
|
||||
title: "Incoming Messages",
|
||||
subtitle: "Meshtastic notifications for channel messages and direct messages"
|
||||
)
|
||||
makeRow(
|
||||
icon: "flipphone",
|
||||
title: "New Nodes",
|
||||
subtitle: "Allow Meshtastic to send notifications for messages, newly discovered nodes and low battery alerts for the connected device."
|
||||
)
|
||||
makeRow(
|
||||
icon: "battery.25percent",
|
||||
title: "Low Battery",
|
||||
subtitle: "Allow Meshtastic to send notifications for messages, newly discovered nodes and low battery alerts for the connected device."
|
||||
)
|
||||
Text("Critical Alerts")
|
||||
.font(.title2.bold())
|
||||
.multilineTextAlignment(.center)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
makeRow(
|
||||
icon: "exclamationmark.triangle.fill",
|
||||
subtitle: "Select packets sent as critical will ignore the mute switch and Do Not Disturb settings in the OS notification center."
|
||||
)
|
||||
}
|
||||
.padding()
|
||||
Spacer()
|
||||
Button {
|
||||
Task {
|
||||
await requestNotificationsPermissions()
|
||||
await goToNextStep(after: .notifications)
|
||||
}
|
||||
} label: {
|
||||
Text("Configure notification permissions")
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.buttonBorderShape(.capsule)
|
||||
.controlSize(.large)
|
||||
.padding()
|
||||
.buttonStyle(.borderedProminent)
|
||||
}
|
||||
}
|
||||
|
||||
var locationView: some View {
|
||||
VStack {
|
||||
VStack {
|
||||
Text("Phone Location")
|
||||
.font(.largeTitle.bold())
|
||||
.multilineTextAlignment(.center)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
Text("Meshtastic uses your phone's location to enable a number of features. You can update your location permissions at any time from Settings > App Settings > Open Settings.")
|
||||
.font(.body.bold())
|
||||
.multilineTextAlignment(.center)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
makeRow(
|
||||
icon: "location",
|
||||
title: "Share Location",
|
||||
subtitle: "Use your phone GPS to send locations to your node to instead of using a hardware GPS on your node."
|
||||
)
|
||||
makeRow(
|
||||
icon: "lines.measurement.horizontal",
|
||||
title: "Distance Measurements",
|
||||
subtitle: "Used to display the distance between your phone and other Meshtastic nodes where positions are available."
|
||||
)
|
||||
makeRow(
|
||||
icon: "line.3.horizontal.decrease.circle",
|
||||
title: "Distance Filters",
|
||||
subtitle: "Filter the node list and mesh map based on proximity to your phone."
|
||||
)
|
||||
makeRow(
|
||||
icon: "mappin",
|
||||
title: "Mesh Map Location",
|
||||
subtitle: "Enables the blue location dot for your phone in the mesh map."
|
||||
)
|
||||
}
|
||||
.padding()
|
||||
Spacer()
|
||||
Button {
|
||||
Task {
|
||||
await requestLocationPermissions()
|
||||
}
|
||||
} label: {
|
||||
Text("Configure Location Permissions")
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.padding()
|
||||
.buttonBorderShape(.capsule)
|
||||
.controlSize(.large)
|
||||
.padding()
|
||||
.buttonStyle(.borderedProminent)
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationStack(path: $navigationPath) {
|
||||
welcomeView
|
||||
.navigationDestination(for: SetupGuide.self) { guide in
|
||||
switch guide {
|
||||
case .notifications:
|
||||
notificationView
|
||||
case .location:
|
||||
locationView
|
||||
}
|
||||
}
|
||||
}
|
||||
.toolbar(.hidden)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func makeRow(
|
||||
icon: String,
|
||||
title: String = "",
|
||||
subtitle: String
|
||||
) -> some View {
|
||||
HStack(alignment: .center) {
|
||||
Image(systemName: icon)
|
||||
.resizable()
|
||||
.symbolRenderingMode(.multicolor)
|
||||
.font(.subheadline)
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.padding()
|
||||
.frame(width: 72, height: 72)
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
Text(title)
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.foregroundColor(.primary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
Text(subtitle)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}.multilineTextAlignment(.leading)
|
||||
}.accessibilityElement(children: .combine)
|
||||
}
|
||||
|
||||
// MARK: Navigation
|
||||
func goToNextStep(after step: SetupGuide?) async {
|
||||
switch step {
|
||||
case .none:
|
||||
let status = await UNUserNotificationCenter.current().notificationSettings().authorizationStatus
|
||||
let criticalAlert = await UNUserNotificationCenter.current().notificationSettings().criticalAlertSetting
|
||||
if status == .notDetermined && criticalAlert == .notSupported {
|
||||
navigationPath.append(.notifications)
|
||||
} else {
|
||||
fallthrough
|
||||
}
|
||||
case .notifications:
|
||||
locationStatus = LocationsHandler.shared.manager.authorizationStatus
|
||||
if locationStatus == .notDetermined || locationStatus == .restricted || locationStatus == .denied {
|
||||
navigationPath.append(.location)
|
||||
} else {
|
||||
fallthrough
|
||||
}
|
||||
case .location:
|
||||
let status = LocationsHandler.shared.manager.authorizationStatus
|
||||
if status != .notDetermined && status != .restricted && status != .denied {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Permission Checks
|
||||
func requestNotificationsPermissions() async {
|
||||
let center = UNUserNotificationCenter.current()
|
||||
do {
|
||||
let success = try await center.requestAuthorization(options: [.alert, .badge, .sound, .criticalAlert])
|
||||
if success {
|
||||
Logger.services.info("Notification permissions are enabled")
|
||||
} else {
|
||||
Logger.services.info("Notification permissions denied")
|
||||
}
|
||||
} catch {
|
||||
Logger.services.error("Notification permissions error: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
|
||||
func requestLocationPermissions() async {
|
||||
locationStatus = await LocationsHandler.shared.requestLocationAlwaysPermissions()
|
||||
if locationStatus != .notDetermined {
|
||||
Logger.services.info("Location permissions are enabled")
|
||||
} else {
|
||||
Logger.services.info("Location permissions denied")
|
||||
}
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue