Localize route recorder buttons

Actually record data with the route recorder
Add version number to about view
Make speed faster
This commit is contained in:
Garth Vander Houwen 2023-12-25 20:26:50 -08:00
parent 91407e65ea
commit 7302dc3e35
11 changed files with 148 additions and 56 deletions

View file

@ -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 = "<group>"; };
DDF924C926FBB953009FE055 /* ConnectedDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectedDevice.swift; sourceTree = "<group>"; };
DDFEB3BA29900C1200EE7472 /* CurrentConditionsCompact.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentConditionsCompact.swift; sourceTree = "<group>"; };
DDFFA7462B3A7F3C004730DB /* Bundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bundle.swift; sourceTree = "<group>"; };
/* 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 = "<group>";
@ -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 = "";

View file

@ -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 ?? "⚠️" }
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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"="储存 & 转发设置";