Waypoint form mockup

Additional map fixes
Emoji only keyboard and validation for waypoint emoji
This commit is contained in:
Garth Vander Houwen 2023-01-11 13:53:50 -08:00
parent 8bc645412b
commit 02801ab16e
11 changed files with 266 additions and 39 deletions

View file

@ -66,6 +66,8 @@
DD8ED9C8289CE4B900B3B0AB /* RoutingError.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD8ED9C7289CE4B900B3B0AB /* RoutingError.swift */; };
DD90860E26F69BAE00DC5189 /* NodeMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD90860D26F69BAE00DC5189 /* NodeMap.swift */; };
DD913639270DFF4C00D7ACF3 /* LocalNotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD913638270DFF4C00D7ACF3 /* LocalNotificationManager.swift */; };
DD964FBD296E6B01007C176F /* EmojiOnlyTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD964FBC296E6B01007C176F /* EmojiOnlyTextField.swift */; };
DD964FBF296E76EF007C176F /* WaypointFormView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD964FBE296E76EF007C176F /* WaypointFormView.swift */; };
DD97E96628EFD9820056DDA4 /* MeshtasticLogo.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD97E96528EFD9820056DDA4 /* MeshtasticLogo.swift */; };
DD97E96828EFE9A00056DDA4 /* About.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD97E96728EFE9A00056DDA4 /* About.swift */; };
DD994B69295F88B60013760A /* IntervalEnums.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD994B68295F88B60013760A /* IntervalEnums.swift */; };
@ -187,6 +189,8 @@
DD90860A26F645B700DC5189 /* Meshtastic.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Meshtastic.entitlements; sourceTree = "<group>"; };
DD90860D26F69BAE00DC5189 /* NodeMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeMap.swift; sourceTree = "<group>"; };
DD913638270DFF4C00D7ACF3 /* LocalNotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalNotificationManager.swift; sourceTree = "<group>"; };
DD964FBC296E6B01007C176F /* EmojiOnlyTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiOnlyTextField.swift; sourceTree = "<group>"; };
DD964FBE296E76EF007C176F /* WaypointFormView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaypointFormView.swift; sourceTree = "<group>"; };
DD97E96528EFD9820056DDA4 /* MeshtasticLogo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshtasticLogo.swift; sourceTree = "<group>"; };
DD97E96728EFE9A00056DDA4 /* About.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = About.swift; sourceTree = "<group>"; };
DD994B68295F88B60013760A /* IntervalEnums.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntervalEnums.swift; sourceTree = "<group>"; };
@ -273,6 +277,7 @@
C9A88B54278B503C00BD810A /* MapViewModule.swift */,
C9697F9C279336B700250207 /* LocalMBTileOverlay.swift */,
DD2AD8A7296D2DF9001FF0E7 /* MapViewSwiftUI.swift */,
DD964FBE296E76EF007C176F /* WaypointFormView.swift */,
);
path = Map;
sourceTree = "<group>";
@ -546,6 +551,7 @@
DD913638270DFF4C00D7ACF3 /* LocalNotificationManager.swift */,
DD8169F8271F1A6100F4AB02 /* MeshLogger.swift */,
DDA6B2E828419CF2003E8C16 /* MeshPackets.swift */,
DD964FBC296E6B01007C176F /* EmojiOnlyTextField.swift */,
);
path = Helpers;
sourceTree = "<group>";
@ -729,6 +735,7 @@
DDCFF601285453A7005FA625 /* localonly.pb.swift in Sources */,
DD836AE726F6B38600ABCC23 /* Connect.swift in Sources */,
DDAF8C6E26ED19040058C060 /* Extensions.swift in Sources */,
DD964FBF296E76EF007C176F /* WaypointFormView.swift in Sources */,
DD3501892852FC3B000FC853 /* Settings.swift in Sources */,
DD5D0A9C2931B9F200F7EA61 /* EthernetModes.swift in Sources */,
DD798B072915928D005217CD /* ChannelMessageList.swift in Sources */,
@ -743,6 +750,7 @@
DDAF8C5326EB1DF10058C060 /* BLEManager.swift in Sources */,
DDC4D568275499A500A4208E /* Persistence.swift in Sources */,
DD90860E26F69BAE00DC5189 /* NodeMap.swift in Sources */,
DD964FBD296E6B01007C176F /* EmojiOnlyTextField.swift in Sources */,
DD8169FF272476C700F4AB02 /* LogDocument.swift in Sources */,
DD2AD8A8296D2DF9001FF0E7 /* MapViewSwiftUI.swift in Sources */,
DD6193792863875F00E59241 /* SerialConfig.swift in Sources */,

View file

@ -789,9 +789,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject {
var dataMessage = DataMessage()
dataMessage.payload = try! positionPacket.serializedData()
dataMessage.portnum = PortNum.positionApp
//if destNum != emptyNodeNum {
dataMessage.wantResponse = wantResponse
//}
dataMessage.wantResponse = wantResponse
meshPacket.decoded = dataMessage
var toRadio: ToRadio!
@ -809,18 +807,13 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject {
}
@objc func positionTimerFired(timer: Timer) {
// Check for connected node
if connectedPeripheral != nil {
// Send a position out to the mesh if "share location with the mesh" is enabled in settings
if userSettings!.provideLocation {
let success = sendPosition(destNum: connectedPeripheral.num, wantResponse: false)
if !success {
print("Failed to send positon to device")
}
}
}

View file

@ -0,0 +1,74 @@
//
// EmojiKeyboard.swift
// Meshtastic
//
// Copyright(c) Garth Vander Houwen 1/10/23.
//
import SwiftUI
class SwiftUIEmojiTextField: UITextField {
override func awakeFromNib() {
super.awakeFromNib()
}
func setEmoji() {
_ = self.textInputMode
}
override var textInputContextIdentifier: String? {
return ""
}
override var textInputMode: UITextInputMode? {
for mode in UITextInputMode.activeInputModes {
if mode.primaryLanguage == "emoji" {
self.keyboardType = .default // do not remove this
return mode
}
}
return nil
}
}
struct EmojiOnlyTextField: UIViewRepresentable {
@Binding var text: String
var placeholder: String = ""
func makeUIView(context: Context) -> SwiftUIEmojiTextField {
let emojiTextField = SwiftUIEmojiTextField()
emojiTextField.placeholder = placeholder
emojiTextField.text = text
emojiTextField.delegate = context.coordinator
return emojiTextField
}
func updateUIView(_ uiView: SwiftUIEmojiTextField, context: Context) {
uiView.text = text
}
func makeCoordinator() -> Coordinator {
Coordinator(parent: self)
}
class Coordinator: NSObject, UITextFieldDelegate {
var parent: EmojiOnlyTextField
init(parent: EmojiOnlyTextField) {
self.parent = parent
}
func textFieldDidChangeSelection(_ textField: UITextField) {
DispatchQueue.main.async { [weak self] in
self?.parent.text = textField.text ?? ""
}
}
}
}
//struct EmojiContentView: View {
//
// @State private var text: String = ""
//
// var body: some View {
// EmojiTextField(text: $text, placeholder: "Enter emoji")
// }
//}

View file

@ -1,6 +1,13 @@
import Foundation
import SwiftUI
extension Character {
var isEmoji: Bool {
guard let scalar = unicodeScalars.first else { return false }
return scalar.properties.isEmoji && (scalar.value >= 0x203C || unicodeScalars.count > 1)
}
}
extension Data {
var macAddressString: String {
let mac: String = reduce("") {$0 + String(format: "%02x:", $1)}
@ -73,6 +80,10 @@ extension String {
return base64url
}
func onlyEmojis() -> Bool {
return count > 0 && !contains { !$0.isEmoji }
}
func image(fontSize:CGFloat = 40, bgColor:UIColor = UIColor.clear, imageSize:CGSize? = nil) -> UIImage?
{
let font = UIFont.systemFont(ofSize: fontSize)

View file

@ -6,7 +6,6 @@ class LocationHelper: NSObject, ObservableObject {
// Apple Park
static let DefaultLocation = CLLocationCoordinate2D(latitude: 37.3346, longitude: -122.0090)
static let DefaultAltitude = CLLocationDistance(integerLiteral: 0)
static let DefaultSpeed = CLLocationSpeed(integerLiteral: 0)
static let DefaultHeading = CLLocationDirection(integerLiteral: 0)
@ -82,6 +81,7 @@ class LocationHelper: NSObject, ObservableObject {
super.init()
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.allowsBackgroundLocationUpdates = true
locationManager.requestWhenInUseAuthorization()
locationManager.startUpdatingLocation()
}

View file

@ -43,5 +43,6 @@ extension PositionEntity {
extension PositionEntity: MKAnnotation {
public var coordinate: CLLocationCoordinate2D { nodeCoordinate! }
public var subtitle: String? { time?.formatted() }
public var title: String? { nodePosition?.user?.shortName ?? NSLocalizedString("unknown", comment: "Unknown") }
public var subtitle: String? { time?.formatted() }
}

View file

@ -1150,10 +1150,14 @@ struct Waypoint {
/// Name of the waypoint - max 30 chars
var name: String = String()
///*
///
/// Description of the waypoint - max 100 chars
var description_p: String = String()
///
/// Designator icon for the waypoint in the form of a unicode emoji
var emoji: UInt32 = 0
var unknownFields = SwiftProtobuf.UnknownStorage()
init() {}
@ -2778,6 +2782,7 @@ extension Waypoint: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB
5: .same(proto: "locked"),
6: .same(proto: "name"),
7: .same(proto: "description"),
8: .same(proto: "emoji"),
]
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
@ -2793,6 +2798,7 @@ extension Waypoint: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB
case 5: try { try decoder.decodeSingularBoolField(value: &self.locked) }()
case 6: try { try decoder.decodeSingularStringField(value: &self.name) }()
case 7: try { try decoder.decodeSingularStringField(value: &self.description_p) }()
case 8: try { try decoder.decodeSingularFixed32Field(value: &self.emoji) }()
default: break
}
}
@ -2820,6 +2826,9 @@ extension Waypoint: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB
if !self.description_p.isEmpty {
try visitor.visitSingularStringField(value: self.description_p, fieldNumber: 7)
}
if self.emoji != 0 {
try visitor.visitSingularFixed32Field(value: self.emoji, fieldNumber: 8)
}
try unknownFields.traverse(visitor: &visitor)
}
@ -2831,6 +2840,7 @@ extension Waypoint: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB
if lhs.locked != rhs.locked {return false}
if lhs.name != rhs.name {return false}
if lhs.description_p != rhs.description_p {return false}
if lhs.emoji != rhs.emoji {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}

View file

@ -19,8 +19,18 @@ struct MapViewSwiftUI: UIViewRepresentable {
mapView.mapType = mapViewType
mapView.setRegion(region, animated: true)
mapView.isRotateEnabled = true
mapView.isPitchEnabled = true
mapView.showsBuildings = true;
mapView.addAnnotations(positions)
mapView.showsUserLocation = true
mapView.setUserTrackingMode(.followWithHeading, animated: true)
mapView.showsCompass = true
mapView.showsScale = true
mapView.isScrollEnabled = true
mapView.delegate = context.coordinator
let gestureRecognizer = UITapGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.tapMap(sender:)))
mapView.addGestureRecognizer(gestureRecognizer)
return mapView
}
@ -33,14 +43,14 @@ struct MapViewSwiftUI: UIViewRepresentable {
}
final class MapCoordinator: NSObject, MKMapViewDelegate {
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
switch annotation {
case _ as MKClusterAnnotation:
let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "nodeGroup") as? MKMarkerAnnotationView ?? MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: "nodeGroup")
annotationView.markerTintColor = .darkGray
annotationView.markerTintColor = .systemRed
return annotationView
case _ as PositionEntity:
let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "node") as? MKMarkerAnnotationView ?? MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: "Node")
@ -53,5 +63,11 @@ struct MapViewSwiftUI: UIViewRepresentable {
default: return nil
}
}
@objc func tapMap(sender: UITapGestureRecognizer) {
if sender.state == .ended {
//let locationInMap = sender.location(in: control.mapView)
//let coordinateSet = control.mapView.convert(locationInMap, toCoordinateFrom: control.mapView)
}
}
}
}

View file

@ -0,0 +1,127 @@
//
// WaypointFormView.swift
// Meshtastic
//
// Created by Garth Vander Houwen on 1/10/23.
//
import SwiftUI
struct WaypointFormView: View {
@Environment(\.dismiss) private var dismiss
@State private var id: Int32?
@State private var name: String = ""
@State private var description: String = ""
@State private var emoji: String = ""
@FocusState private var emojiIsFocused: Bool
@State private var latitude: Double = 0.0
@State private var longitude: Double = 0.0
@State private var expire: Date = Date.now.addingTimeInterval(60 * 60)
@State private var locked: Bool = false
var body: some View {
Form {
Section(header: Text("Waypoint")) {
Text("Lat/Long ") + Text(" \(String(latitude) + "," + String(longitude))").foregroundColor(Color.gray)
HStack {
Text("Name")
Spacer()
TextField(
"Name",
text: $name
)
.foregroundColor(Color.gray)
.onChange(of: name, perform: { value in
let totalBytes = name.utf8.count
// Only mess with the value if it is too big
if totalBytes > 30 {
let firstNBytes = Data(name.utf8.prefix(30))
if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) {
// Set the name back to the last place where it was the right size
name = maxBytesString
}
}
})
}
HStack {
Text("Description")
Spacer()
TextField(
"Description",
text: $description,
axis: .vertical
)
.foregroundColor(Color.gray)
.onChange(of: description, perform: { value in
let totalBytes = description.utf8.count
// Only mess with the value if it is too big
if totalBytes > 100 {
let firstNBytes = Data(description.utf8.prefix(100))
if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) {
// Set the name back to the last place where it was the right size
description = maxBytesString
}
}
})
}
HStack {
Text("Emoji")
Spacer()
EmojiOnlyTextField(text: $emoji, placeholder: "emoji")
.font(.title)
.focused($emojiIsFocused)
.onChange(of: emoji) { value in
// If you have anything other than emojis in your string make it empty
if !value.onlyEmojis() {
emoji = ""
}
// If a second emoji is entered delete the first one
if value.count >= 1 {
if value.count > 1 {
let index = value.index(value.startIndex, offsetBy: 1)
emoji = String(value[index])
}
emojiIsFocused = false
}
}
}
Toggle(isOn: $locked) {
Label("Locked", systemImage: "lock")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
DatePicker("Expire", selection: $expire, in: Date.now...)
.datePickerStyle(.compact)
.font(.callout)
}
}
HStack {
Button {
dismiss()
} label: {
Label("save", systemImage: "square.and.arrow.down")
}
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)
.controlSize(.large)
.padding()
Button {
dismiss()
} label: {
Label("cancel", systemImage: "xmark")
}
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)
.controlSize(.large)
.padding()
}
}
}

View file

@ -19,6 +19,7 @@ struct NodeDetail: View {
@State var satsInView = 0
@State private var showingShutdownConfirm: Bool = false
@State private var showingRebootConfirm: Bool = false
@State var presentingWaypointForm = false
var node: NodeInfoEntity
@ -35,7 +36,10 @@ struct NodeDetail: View {
ZStack {
let annotations = node.positions?.array as! [PositionEntity]
ZStack {
MapViewSwiftUI(positions: annotations, region: MKCoordinateRegion(center: nodeCoordinatePosition, span: MKCoordinateSpan(latitudeDelta: 0.005, longitudeDelta: 0.005)), mapViewType: mapType)
MapViewSwiftUI(positions: annotations, region: MKCoordinateRegion(center: nodeCoordinatePosition, span: MKCoordinateSpan(latitudeDelta: 0.005, longitudeDelta: 0.005)
//MKCoordinateSpan(latitudeDelta: 0.16405544070813249, longitudeDelta: 0.1232528799585566)
), mapViewType: mapType)
VStack {
Spacer()
Text(mostRecent.satsInView > 0 ? "Sats: \(mostRecent.satsInView)" : " ")
@ -129,7 +133,6 @@ struct NodeDetail: View {
.symbolRenderingMode(.hierarchical)
Text("user").font(.title)+Text(":").font(.title)
}
//Text(node.user?.userId ?? "??????").font(.title).foregroundColor(.gray)
Text("!\(String(format:"%02x", node.num))")
.font(.title).foregroundColor(.gray)
}
@ -173,7 +176,6 @@ struct NodeDetail: View {
.foregroundColor(.gray)
}
}
.padding()
Divider()
} else {
@ -194,7 +196,6 @@ struct NodeDetail: View {
.font(.callout).fixedSize()
}
}
.padding(5)
if node.snr > 0 {
Divider()
@ -210,19 +211,13 @@ struct NodeDetail: View {
.foregroundColor(.gray)
.fixedSize()
}
.padding(5)
}
if node.telemetries?.count ?? 0 >= 1 {
let mostRecent = node.telemetries?.lastObject as! TelemetryEntity
Divider()
VStack(alignment: .center) {
BatteryGauge(batteryLevel: Double(mostRecent.batteryLevel))
if mostRecent.voltage > 0 {
Text(String(format: "%.2f", mostRecent.voltage) + " V")
@ -230,14 +225,11 @@ struct NodeDetail: View {
.foregroundColor(.gray)
.fixedSize()
}
}
}
}
Divider()
HStack(alignment: .center) {
VStack {
HStack {
Image(systemName: "person")
@ -260,7 +252,6 @@ struct NodeDetail: View {
Text(String(node.num)).font(.title3).foregroundColor(.gray)
}
}
.padding(5)
Divider()
HStack {
Image(systemName: "globe")
@ -270,7 +261,7 @@ struct NodeDetail: View {
Text("MAC Address: ")
Text(String(node.user?.macaddr?.macAddressString ?? "not a valid mac address")).foregroundColor(.gray)
}
.padding([.bottom], 0)
.padding([.bottom], 10)
Divider()
}
@ -374,15 +365,17 @@ struct NodeDetail: View {
}
}
}
}
.padding(5)
}
} }
}
//.offset( y:-40)
}
.edgesIgnoringSafeArea([.leading, .trailing])
.sheet(isPresented: $presentingWaypointForm ) {//, onDismiss: didDismissSheet) {
WaypointFormView()
.presentationDetents([.medium, .large])
.presentationDragIndicator(.automatic)
}
.navigationBarTitle(String(node.user?.longName ?? NSLocalizedString("unknown", comment: "")), displayMode: .inline)
.padding(.bottom, 10)
.navigationBarItems(trailing:
ZStack {
ConnectedDevice(

View file

@ -250,16 +250,10 @@ struct Channels: View {
let adminMessageId = bleManager.saveChannel(channel: channel, fromUser: node!.user!, toUser: node!.user!)
if adminMessageId > 0 {
// Should show a saved successfully alert once I know that to be true
// for now just disable the button after a successful save.
self.isPresentingEditView = false
channelName = ""
hasChanges = false
// Would rather send a getChannel but I can't seem serialize it properly yet
bleManager.getChannel(channel: channel, fromUser: node!.user!, toUser: node!.user!)
//bleManager.sendWantConfig()
}
} label: {
Label("save", systemImage: "square.and.arrow.down")