diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index c2d1d4b9..ed6be43b 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -180,6 +180,7 @@ DDF6B2482A9AEBF500BA6931 /* StoreForward.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF6B2472A9AEBF500BA6931 /* StoreForward.swift */; }; DDF924CA26FBB953009FE055 /* ConnectedDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF924C926FBB953009FE055 /* ConnectedDevice.swift */; }; DDFEB3BB29900C1200EE7472 /* CurrentConditionsCompact.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDFEB3BA29900C1200EE7472 /* CurrentConditionsCompact.swift */; }; + DDFFA7472B3A7F3C004730DB /* Bundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDFFA7462B3A7F3C004730DB /* Bundle.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -421,6 +422,7 @@ DDF6B24B2A9C2FC800BA6931 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/Localizable.strings; sourceTree = ""; }; DDF924C926FBB953009FE055 /* ConnectedDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectedDevice.swift; sourceTree = ""; }; DDFEB3BA29900C1200EE7472 /* CurrentConditionsCompact.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentConditionsCompact.swift; sourceTree = ""; }; + DDFFA7462B3A7F3C004730DB /* Bundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bundle.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -905,6 +907,7 @@ DDB75A0E2A05920E006ED576 /* FileManager.swift */, DDB75A102A059258006ED576 /* Url.swift */, DD1933772B084F4200771CD5 /* Measurement.swift */, + DDFFA7462B3A7F3C004730DB /* Bundle.swift */, ); path = Extensions; sourceTree = ""; @@ -1123,6 +1126,7 @@ buildActionMask = 2147483647; files = ( DDDB444829F8A9C900EE2349 /* String.swift in Sources */, + DDFFA7472B3A7F3C004730DB /* Bundle.swift in Sources */, DD5E520C298EE33B00D21B61 /* portnums.pb.swift in Sources */, DD457188293C7E63000C49FB /* BLESignalStrengthIndicator.swift in Sources */, DDFEB3BB29900C1200EE7472 /* CurrentConditionsCompact.swift in Sources */, @@ -1486,7 +1490,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.2.17; + MARKETING_VERSION = 2.2.18; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1520,7 +1524,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.2.17; + MARKETING_VERSION = 2.2.18; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1642,7 +1646,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.2.17; + MARKETING_VERSION = 2.2.18; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1675,7 +1679,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.2.17; + MARKETING_VERSION = 2.2.18; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/Meshtastic/Extensions/Bundle.swift b/Meshtastic/Extensions/Bundle.swift new file mode 100644 index 00000000..b3bd85bf --- /dev/null +++ b/Meshtastic/Extensions/Bundle.swift @@ -0,0 +1,22 @@ +// +// Bundle.swift +// Meshtastic +// +// Created by Garth Vander Houwen on 12/25/23. +// + +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") } + + fileprivate func getInfo(_ str: String) -> String { infoDictionary?[str] as? String ?? "⚠️" } +} diff --git a/Meshtastic/Helpers/LocationsHandler.swift b/Meshtastic/Helpers/LocationsHandler.swift index 56e1ca18..263964cd 100644 --- a/Meshtastic/Helpers/LocationsHandler.swift +++ b/Meshtastic/Helpers/LocationsHandler.swift @@ -8,11 +8,10 @@ import SwiftUI import CoreLocation - // Shared state that manages the `CLLocationManager` and `CLBackgroundActivitySession`. @available(iOS 17.0, macOS 14.0, *) @MainActor class LocationsHandler: ObservableObject { - + static let shared = LocationsHandler() // Create a single, shared instance of the object. private let manager: CLLocationManager private var background: CLBackgroundActivitySession? diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 92139355..40221a44 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -290,7 +290,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 * UInt32(3.6)) position.heading = Int32(nodeInfo.position.groundTrack) position.time = Date(timeIntervalSince1970: TimeInterval(Int64(nodeInfo.position.time))) var newPostions = [PositionEntity]() diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index f39420e9..0794cbea 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -250,7 +250,7 @@ func upsertPositionPacket (packet: MeshPacket, context: NSManagedObjectContext) position.longitudeI = positionMessage.longitudeI position.altitude = positionMessage.altitude position.satsInView = Int32(positionMessage.satsInView) - position.speed = Int32(positionMessage.groundSpeed) + position.speed = Int32(positionMessage.groundSpeed * UInt32(3.6)) position.heading = Int32(positionMessage.groundTrack) if positionMessage.timestamp != 0 { position.time = Date(timeIntervalSince1970: TimeInterval(Int64(positionMessage.timestamp))) diff --git a/Meshtastic/Views/Settings/About.swift b/Meshtastic/Views/Settings/About.swift index 4bcb44e8..9bcb5b72 100644 --- a/Meshtastic/Views/Settings/About.swift +++ b/Meshtastic/Views/Settings/About.swift @@ -50,6 +50,12 @@ struct AboutMeshtastic: View { } } .font(.title2) + + Text("Version: \(Bundle.main.appVersionLong) (\(Bundle.main.appBuild)) ") + + Text(Bundle.main.copyright) + .font(.system(size: 10, weight: .thin)) + .multilineTextAlignment(.center) } Section(header: Text("Project information")) { diff --git a/Meshtastic/Views/Settings/RouteRecorder.swift b/Meshtastic/Views/Settings/RouteRecorder.swift index 27eaa641..405d9fb1 100644 --- a/Meshtastic/Views/Settings/RouteRecorder.swift +++ b/Meshtastic/Views/Settings/RouteRecorder.swift @@ -11,53 +11,51 @@ import MapKit import CoreLocation import CoreMotion -struct TimerDisplayObject { - var seconds: Int = 0 - var minutes: Int = 0 - var hours: Int = 0 - - var display: String { - if self.seconds == 0 { - "\(String(format: "%02d", self.hours)):\(String(format: "%02d", self.minutes)):00" - } else { - "\(String(format: "%02d", self.hours)):\(String(format: "%02d", self.minutes)):\(String(format: "%02d", self.seconds))" - } - } - - var timeMinuteCalculator: Float { Float(hours*60+seconds/60+minutes) } -} - @available(iOS 17.0, macOS 14.0, *) struct RouteRecorder: View { @ObservedObject var locationsHandler: LocationsHandler = LocationsHandler.shared @Environment(\.managedObjectContext) var context @State private var position: MapCameraPosition = .userLocation(followsHeading: true, fallback: .automatic) + //@State var mapStyle: MapStyle = MapStyle.hybrid(elevation: .realistic, pointsOfInterest: .all, showsTraffic: true) + @State var mapStyle: MapStyle = MapStyle.standard(elevation: .realistic) @State var isShowingDetails = false @Namespace var namespace @Namespace var routerecorderscope + @State var recording: RouteEntity? var body: some View { VStack { - VStack { + ZStack { Map(position: $position, scope: routerecorderscope) { UserAnnotation() -// ForEach(locations, id: \.id) { location in -// Marker(location.name, systemImage: location.icon, coordinate: location.location) -// .tint(location.colour) -// } + /// Route Lines + let lineCoords = locationsHandler.locationsArray.compactMap({(position) -> CLLocationCoordinate2D in + return position.coordinate + }) + let gradient = LinearGradient( + colors: [.blue], + startPoint: .leading, endPoint: .trailing + ) + let dashed = StrokeStyle( + lineWidth: 3, + lineCap: .round, lineJoin: .round, dash: [10, 10] + ) + MapPolyline(coordinates: lineCoords) + .stroke(gradient, style: dashed) + } + .mapStyle(mapStyle) } + .ignoresSafeArea(.all, edges: [.top, .leading, .trailing]) .mapScope(routerecorderscope) - .mapControls { - MapUserLocationButton() - MapCompass() - MapScaleView() - MapPitchToggle() - } - .mapStyle(.hybrid(elevation: .realistic, showsTraffic: true)) +// .mapControls { +// MapScaleView(scope: routerecorderscope) +// MapUserLocationButton(scope: routerecorderscope) +// MapPitchToggle(scope: routerecorderscope) +// MapCompass(scope: routerecorderscope) +// } .transition(.slide) - .mapControlVisibility(.visible) .safeAreaInset(edge: .bottom) { ZStack { VStack { @@ -90,12 +88,16 @@ struct RouteRecorder: View { HStack (alignment: .center) { Image(systemName: "record.circle.fill") .symbolRenderingMode(.multicolor) - .font(.title3) + .font(.title) .foregroundColor(.red) - Text("Recording route - \(locationsHandler.count) locations") - .font(.title3) + Text("Recording route") + .font(.title) + Spacer() + Text("\(locationsHandler.count)") + .foregroundColor(.red) + .font(.title2) } - .padding(.top) + .padding() } else if locationsHandler.isRecordingPaused { HStack (alignment: .center) { @@ -104,18 +106,17 @@ struct RouteRecorder: View { .font(.title3) .foregroundColor(.red) Text("Route recording paused") - .font(.title3) + .font(.title) } .padding(.top) } - if locationsHandler.isRecording || locationsHandler.isRecordingPaused { Divider() HStack { VStack { Text(locationsHandler.recordingStarted ?? Date(), style: .timer) - .font(.largeTitle) + .font(.title) .fixedSize() Text("Time") .font(.callout) @@ -126,7 +127,7 @@ struct RouteRecorder: View { VStack { let distance = Measurement(value: locationsHandler.distanceTraveled, unit: UnitLength.meters) Text("\(distance.formatted())") - .font(.largeTitle) + .font(.title) .fixedSize() Text("Distance") .font(.callout) @@ -137,12 +138,11 @@ struct RouteRecorder: View { VStack { let gain = Measurement(value: locationsHandler.elevationGain, unit: UnitLength.meters) Text(gain.formatted()) - .font(.largeTitle) + .font(.title) Text("Elev. Gain") .font(.callout) } .padding(.horizontal) - } .frame(maxHeight: 90) } @@ -155,7 +155,7 @@ struct RouteRecorder: View { HStack { Spacer() if !locationsHandler.isRecording && !locationsHandler.isRecordingPaused { - /// We are not recording or paused, show start recording button a new recording + /// We are not recording or paused, show start recording button Button { locationsHandler.isRecording = true locationsHandler.count = 0 @@ -163,8 +163,23 @@ struct RouteRecorder: View { locationsHandler.elevationGain = 0.0 locationsHandler.locationsArray.removeAll() locationsHandler.recordingStarted = Date() + let newRoute = RouteEntity(context: context) + newRoute.name = String("Route Recording - \(Date().formatted())") + newRoute.id = Int32.random(in: Int32(Int8.max) ... Int32.max) + newRoute.color = Int64(UIColor.random.hex) + newRoute.date = Date() + newRoute.enabled = true + self.recording = newRoute + do { + try context.save() + print("💾 Saved a new route") + } catch { + context.rollback() + let nsError = error as NSError + print("💥 Error Saving RouteEntity from the Route Recorder \(nsError)") + } } label: { - Label("start", systemImage: "start") + Label("start", systemImage: "play") } .buttonStyle(.bordered) .buttonBorderShape(.capsule) @@ -184,7 +199,7 @@ struct RouteRecorder: View { .controlSize(.large) .padding(.bottom) } else if locationsHandler.isRecordingPaused { - /// We are recording show pause button + /// We are paused show resume button Button { locationsHandler.isRecording = true locationsHandler.isRecordingPaused = false @@ -198,8 +213,7 @@ struct RouteRecorder: View { } if locationsHandler.isRecording || locationsHandler.isRecordingPaused { - - /// We are recording show pause button + /// We are recording or paused, show finish button Button { locationsHandler.isRecording = false locationsHandler.isRecordingPaused = false @@ -215,7 +229,7 @@ struct RouteRecorder: View { .controlSize(.large) .padding(.bottom) } - +#if targetEnvironment(macCatalyst) Button(role: .cancel) { isShowingDetails = false } label: { @@ -225,14 +239,41 @@ struct RouteRecorder: View { .buttonBorderShape(.capsule) .controlSize(.large) .padding(.bottom) +#endif Spacer() } + + } + } + } + .presentationDetents([.fraction(0.30), .fraction(0.65)]) + .presentationDragIndicator(.hidden) + .interactiveDismissDisabled(false) + .onChange(of: locationsHandler.locationsArray.last) { newLoc in + if locationsHandler.isRecording { + if let loc = newLoc { + if recording != nil { + let locationEntity = LocationEntity(context: context) + locationEntity.routeLocation = recording + locationEntity.id = Int32(locationsHandler.count) + locationEntity.altitude = Int32(loc.altitude) + locationEntity.heading = Int32(loc.course) + locationEntity.speed = Int32(loc.speed) + locationEntity.latitudeI = Int32(loc.coordinate.latitude * 1e7) + locationEntity.longitudeI = Int32(loc.coordinate.longitude * 1e7) + do { + try context.save() + print("💾 Saved a new route location") + //print("💾 Updated Canned Messages Messages For: \(fetchedNode[0].num)") + } catch { + context.rollback() + let nsError = error as NSError + print("💥 Error Saving LocationEntity from the Route Recorder \(nsError)") + } + } } } } - .presentationDetents([.fraction(0.65)]) - .presentationDragIndicator(.hidden) - .interactiveDismissDisabled() } } } diff --git a/de.lproj/Localizable.strings b/de.lproj/Localizable.strings index 54c5d6f3..1b6b3060 100644 --- a/de.lproj/Localizable.strings +++ b/de.lproj/Localizable.strings @@ -84,6 +84,7 @@ "encrypted"="Verschlüsselt"; "external.notification"="Externe Benachrichtigung"; "external.notification.config"="Einstellungen der externen Benachrichtigung"; +"finish"="Finish"; "firmware.version"="Firmware Version"; "firmware.version.unsupported"="Nicht unterstützte Firmware Version erkannt. Kann nicht verbinden."; "gas"="Gas"; @@ -213,6 +214,7 @@ "on.boot"="Nur beim Starten"; "options"="Optionen"; "password"="Passwort"; +"pause"="Pause"; "phone.gps"="Telefon GPS"; "phone.gps.interval.description"="Wie häufig das Telefon den Standort an das Gerät sendet. Standortaktualisierungen an das Mesh werden vom Gerät verwaltet."; "position"="Position"; @@ -228,8 +230,10 @@ "reply"="Antworten"; "received.ack"="Empfangsbestätigung"; "received.ack.real"="Recipient Ack"; +"resume"="Resume"; "ringtone"="Ringtone"; "ringtone.config"="Ringtone Config"; +"route.recorder"="Route Recorder"; "routes"="Routes"; "routing.acknowledged"="Bestätigt"; "routing.noroute"="Keine Route"; @@ -264,6 +268,7 @@ "set.region"="Setze LoRa Region"; "standard"="Standard"; "standard.muted"="Standard Muted"; +"start"="Start"; "storeforward"="Store & Forward"; "storeforward.config"="Store & Forward Config"; "storeforward.heartbeat"="Send Heartbeat"; diff --git a/en.lproj/Localizable.strings b/en.lproj/Localizable.strings index 355a9c26..aeb1b117 100644 --- a/en.lproj/Localizable.strings +++ b/en.lproj/Localizable.strings @@ -88,6 +88,7 @@ "encrypted"="Encrypted"; "external.notification"="External Notification"; "external.notification.config"="External Notification Config"; +"finish"="Finish"; "firmware.version"="Firmware Version"; "firmware.version.unsupported"="Unsupported Firmware Version Detected, unable to connect to device."; "gas"="Gas"; @@ -217,6 +218,7 @@ "on.boot"="On Boot Only"; "options"="Options"; "password"="Password"; +"pause"="Pause"; "phone.gps"="Phone GPS"; "phone.gps.interval.description"="How frequently your phone will send your location to the device, location updates to the mesh are managed by the device."; "position"="Position"; @@ -232,8 +234,10 @@ "reboot.node"="Reboot node?"; "received.ack"="Received Ack"; "received.ack.real"="Recipient Ack"; +"resume"="Resume"; "ringtone"="Ringtone"; "ringtone.config"="Ringtone Config"; +"route.recorder"="Route Recorder"; "routes"="Routes"; "routing.acknowledged"="Acknowledged"; "routing.noroute"="No Route"; @@ -268,6 +272,7 @@ "set.region"="Set LoRa Region"; "standard"="Standard"; "standard.muted"="Standard Muted"; +"start"="Start"; "storeforward"="Store & Forward"; "storeforward.config"="Store & Forward Config"; "storeforward.heartbeat"="Send Heartbeat"; diff --git a/pl.lproj/Localizable.strings b/pl.lproj/Localizable.strings index 95ccadb5..db76ed47 100644 --- a/pl.lproj/Localizable.strings +++ b/pl.lproj/Localizable.strings @@ -86,6 +86,7 @@ "encrypted"="Zaszyfrowany"; "external.notification"="Zewnętrzne Powiadomienie"; "external.notification.config"="Konfiguracja Zewnętrznego Powiadomienia"; +"finish"="Finish"; "firmware.version"="Wersja Oprogramowania"; "firmware.version.unsupported"="Wykryto nieobsługiwany wersję oprogramowania, brak możliwości połączenia z urządzeniem."; "gas"="Gaz"; @@ -214,6 +215,7 @@ "on.boot"="Tylko przy uruchomieniu"; "options"="Opcje"; "password"="Hasło"; +"pause"="Pause"; "phone.gps"="GPS telefonu"; "phone.gps.interval.description"="Jak często Twój telefon będzie wysyłał swoją lokalizację do urządzenia, aktualizacje lokalizacji w sieci są zarządzane przez urządzenie."; "position"="Pozycja"; @@ -229,8 +231,10 @@ "reboot.node"="Uruchomić ponownie węzeł?"; "received.ack"="Odebrano potwierdzenie"; "received.ack.real"="Odbiorca potwierdzenia"; +"resume"="Resume"; "ringtone"="Dzwonek"; "ringtone.config"="Konfiguracja dzwonka"; +"route.recorder"="Route Recorder"; "routes"="Routes"; "routing.acknowledged"="Potwierdzono"; "routing.noroute"="Brak trasy"; @@ -265,6 +269,7 @@ "set.region"="Ustaw region LoRa"; "standard"="Standardowy"; "standard.muted"="Standardowy wyłączony"; +"start"="Start"; "storeforward"="Store & Forward"; "storeforward.config"="Store & Forward Config"; "storeforward.heartbeat"="Send Heartbeat"; diff --git a/zh-Hans.lproj/Localizable.strings b/zh-Hans.lproj/Localizable.strings index 202a0871..ed5d4d3e 100644 --- a/zh-Hans.lproj/Localizable.strings +++ b/zh-Hans.lproj/Localizable.strings @@ -84,6 +84,7 @@ "encrypted"="加密"; "external.notification"="外部通知"; "external.notification.config"="外部通知配置"; +"finish"="Finish"; "firmware.version"="固件版本"; "firmware.version.unsupported"="检测到不支持的固件版本,无法连接到电台。"; "gas"="Gas"; @@ -213,6 +214,7 @@ "on.boot"="仅在启动时"; "options"="选项"; "password"="密码"; +"pause"="Pause"; "phone.gps"="手机 GPS"; "phone.gps.interval.description"="电台通过手机获取定位的时间间隔,但是向 Mesh 网络中刷新定位的时间间隔由电台控制。"; "position"="定位"; @@ -228,8 +230,10 @@ "reboot.node"="重启节点?"; "received.ack"="收到确认"; "received.ack.real"="收件人确认"; +"resume"="Resume"; "ringtone"="铃声"; "ringtone.config"="铃声设置"; +"route.recorder"="Route Recorder"; "routes"="Routes"; "routing.acknowledged"="确认"; "routing.noroute"="找不到目标"; @@ -264,6 +268,7 @@ "set.region"="设置 LoRa 区域"; "standard"="标准"; "standard.muted"="标准静音"; +"start"="Start"; "ssid"="SSID"; "storeforward"="储存 & 转发"; "storeforward.config"="储存 & 转发设置";