mirror of
https://github.com/meshtastic/Meshtastic-Apple.git
synced 2026-04-20 22:13:56 +00:00
Show which node created a waypoint and which last updated by (#1496)
* Fixed some issues with waypoints and created a createdBy and lastUpdatedBy * Fix suggestions --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
894e9382d8
commit
04ef427ec8
6 changed files with 401 additions and 296 deletions
|
|
@ -13802,6 +13802,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Created by:" : {
|
||||
|
||||
},
|
||||
"Created: %@" : {
|
||||
"localizations" : {
|
||||
|
|
@ -28406,6 +28409,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Last updated by:" : {
|
||||
|
||||
},
|
||||
"Later" : {
|
||||
"comment" : "A button that dismisses an alert without taking any action.",
|
||||
|
|
@ -31372,6 +31378,10 @@
|
|||
"comment" : "A description of the read-only mode feature in TAK Server.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Meshtastic does not collect any personal information. We do anonymously collect usage and crash data to improve the app. This helps us understand how the app is being used and where we can make improvements. The data we collect is non-personally identifiable and cannot be linked to you as an individual. You can opt out of this under app settings." : {
|
||||
"comment" : "Privacy policy text for Meshtastic.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Meshtastic does not collect any personal information. We do anonymously collect usage and crash data to improve the app. You can opt out under app settings." : {
|
||||
"localizations" : {
|
||||
"es" : {
|
||||
|
|
|
|||
|
|
@ -1175,7 +1175,7 @@ actor MeshPackets {
|
|||
// Fetch waypoint by waypointMessage.id, not packet.id
|
||||
let fetchWaypointRequest = WaypointEntity.fetchRequest()
|
||||
fetchWaypointRequest.predicate = NSPredicate(format: "id == %lld", Int64(waypointMessage.id))
|
||||
|
||||
|
||||
let fetchedWaypoint = try context.fetch(fetchWaypointRequest)
|
||||
// Fetch the node info to get the short name
|
||||
var nodeShortName: String = "?"
|
||||
|
|
@ -1199,6 +1199,7 @@ actor MeshPackets {
|
|||
waypoint.longitudeI = waypointMessage.longitudeI
|
||||
waypoint.icon = Int64(waypointMessage.icon)
|
||||
waypoint.locked = Int64(waypointMessage.lockedTo)
|
||||
waypoint.createdBy = Int64(packet.from)
|
||||
if waypointMessage.expire >= 1 {
|
||||
waypoint.expire = Date(timeIntervalSince1970: TimeInterval(Int64(waypointMessage.expire)))
|
||||
} else {
|
||||
|
|
@ -1254,6 +1255,7 @@ actor MeshPackets {
|
|||
existingWaypoint.longitudeI = waypointMessage.longitudeI
|
||||
existingWaypoint.icon = Int64(waypointMessage.icon)
|
||||
existingWaypoint.locked = Int64(waypointMessage.lockedTo)
|
||||
existingWaypoint.lastUpdatedBy = Int64(packet.from)
|
||||
if waypointMessage.expire >= 1 {
|
||||
existingWaypoint.expire = Date(timeIntervalSince1970: TimeInterval(Int64(waypointMessage.expire)))
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -490,10 +490,12 @@
|
|||
</entity>
|
||||
<entity name="WaypointEntity" representedClassName="WaypointEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="created" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="createdBy" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="expire" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="icon" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="id" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="lastUpdated" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="lastUpdatedBy" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="latitudeI" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="locked" attributeType="Integer 64" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="longDescription" optional="YES" attributeType="String" maxValueString="100"/>
|
||||
|
|
|
|||
|
|
@ -177,6 +177,7 @@ struct MeshMapContent: MapContent {
|
|||
}
|
||||
}
|
||||
}
|
||||
.annotationTitles(.automatic)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import MapKit
|
|||
import MeshtasticProtobufs
|
||||
import OSLog
|
||||
import SwiftUI
|
||||
import CoreData
|
||||
|
||||
struct WaypointForm: View {
|
||||
|
||||
|
|
@ -31,134 +32,218 @@ struct WaypointForm: View {
|
|||
@State private var lockedTo: Int64 = 0
|
||||
@State private var selectedDetent: PresentationDetent = .medium
|
||||
@State private var waypointFailedAlert: Bool = false
|
||||
@State private var createdByNode : NodeInfoEntity? = nil
|
||||
@State private var lastUpdatedByNode : NodeInfoEntity? = nil
|
||||
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
if editMode {
|
||||
Text((waypoint.id > 0) ? "Editing Waypoint" : "Create Waypoint")
|
||||
.font(.largeTitle)
|
||||
Divider()
|
||||
Form {
|
||||
if let cl = LocationsHandler.currentLocation {
|
||||
let distance = CLLocation(latitude: cl.latitude, longitude: cl.longitude).distance(from: CLLocation(latitude: waypoint.coordinate.latitude, longitude: waypoint.coordinate.longitude ))
|
||||
Section(header: Text("Coordinate") ) {
|
||||
Group {
|
||||
if editMode {
|
||||
Text((waypoint.id > 0) ? "Editing Waypoint" : "Create Waypoint")
|
||||
.font(.largeTitle)
|
||||
Divider()
|
||||
Form {
|
||||
if let cl = LocationsHandler.currentLocation {
|
||||
let distance = CLLocation(latitude: cl.latitude, longitude: cl.longitude).distance(from: CLLocation(latitude: waypoint.coordinate.latitude, longitude: waypoint.coordinate.longitude ))
|
||||
Section(header: Text("Coordinate") ) {
|
||||
HStack {
|
||||
Text("Location:")
|
||||
.foregroundColor(.secondary)
|
||||
Text("\(String(format: "%.5f", waypoint.coordinate.latitude) + "," + String(format: "%.5f", waypoint.coordinate.longitude))")
|
||||
.textSelection(.enabled)
|
||||
.foregroundColor(.secondary)
|
||||
.font(.caption)
|
||||
|
||||
}
|
||||
Button {
|
||||
waypoint.coordinate.longitude = cl.longitude
|
||||
waypoint.coordinate.latitude = cl.latitude
|
||||
} label: {
|
||||
HStack {
|
||||
Text("Location:")
|
||||
.foregroundColor(.secondary)
|
||||
Text("\(String(format: "%.5f", waypoint.coordinate.latitude) + "," + String(format: "%.5f", waypoint.coordinate.longitude))")
|
||||
.textSelection(.enabled)
|
||||
.foregroundColor(.secondary)
|
||||
.font(.caption)
|
||||
|
||||
}
|
||||
Button {
|
||||
waypoint.coordinate.longitude = cl.longitude
|
||||
waypoint.coordinate.latitude = cl.latitude
|
||||
} label: {
|
||||
HStack {
|
||||
Text("Use my Location")
|
||||
Image(systemName: "location")
|
||||
}
|
||||
}
|
||||
.accessibilityLabel("Set to current location")
|
||||
HStack {
|
||||
if waypoint.coordinate.latitude != 0 && waypoint.coordinate.longitude != 0 {
|
||||
DistanceText(meters: distance)
|
||||
.foregroundColor(Color.gray)
|
||||
}
|
||||
Text("Use my Location")
|
||||
Image(systemName: "location")
|
||||
}
|
||||
}
|
||||
}
|
||||
Section(header: Text("Waypoint Options")) {
|
||||
.accessibilityLabel("Set to current location")
|
||||
HStack {
|
||||
Text("Name")
|
||||
Spacer()
|
||||
TextField(
|
||||
"Name",
|
||||
text: $name,
|
||||
axis: .vertical
|
||||
)
|
||||
.foregroundColor(Color.gray)
|
||||
.onChange(of: name) {
|
||||
var totalBytes = name.utf8.count
|
||||
// Only mess with the value if it is too big
|
||||
while totalBytes > 30 {
|
||||
name = String(name.dropLast())
|
||||
totalBytes = name.utf8.count
|
||||
}
|
||||
waypoint.name = name.count > 0 ? name : "Dropped Pin"
|
||||
if waypoint.coordinate.latitude != 0 && waypoint.coordinate.longitude != 0 {
|
||||
DistanceText(meters: distance)
|
||||
.foregroundColor(Color.gray)
|
||||
}
|
||||
}
|
||||
HStack {
|
||||
Text("Description")
|
||||
Spacer()
|
||||
TextField(
|
||||
"Description",
|
||||
text: $description,
|
||||
axis: .vertical
|
||||
)
|
||||
.foregroundColor(Color.gray)
|
||||
.onChange(of: description) {
|
||||
var totalBytes = description.utf8.count
|
||||
// Only mess with the value if it is too big
|
||||
while totalBytes > 100 {
|
||||
description = String(description.dropLast())
|
||||
totalBytes = description.utf8.count
|
||||
}
|
||||
}
|
||||
}
|
||||
HStack {
|
||||
Text("Icon")
|
||||
Spacer()
|
||||
TextField("Select an emoji", text: $icon)
|
||||
.keyboardType(.emoji)
|
||||
.font(.title)
|
||||
.focused($iconIsFocused)
|
||||
.onChange(of: icon) { _, value in
|
||||
// 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)
|
||||
icon = String(value[index])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Toggle(isOn: $expires) {
|
||||
Label("Expires", systemImage: "clock.badge.xmark")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
if expires {
|
||||
DatePicker("Expire", selection: $expire, in: Date.now...)
|
||||
.datePickerStyle(.compact)
|
||||
.font(.callout)
|
||||
}
|
||||
Toggle(isOn: $locked) {
|
||||
Label("Locked", systemImage: "lock")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
}
|
||||
}
|
||||
.scrollDismissesKeyboard(.immediately)
|
||||
HStack {
|
||||
Button {
|
||||
guard let deviceNum = accessoryManager.activeDeviceNum else {
|
||||
Logger.mesh.warning("Send waypoint failed: No deviceNum")
|
||||
return
|
||||
}
|
||||
if accessoryManager.isConnected {
|
||||
/// Send a new or exiting waypoint
|
||||
var newWaypoint = Waypoint()
|
||||
if waypoint.id == 0 {
|
||||
newWaypoint.id = UInt32.random(in: UInt32(UInt8.max)..<UInt32.max)
|
||||
waypoint.id = Int64(newWaypoint.id)
|
||||
} else {
|
||||
newWaypoint.id = UInt32(waypoint.id)
|
||||
Section(header: Text("Waypoint Options")) {
|
||||
HStack {
|
||||
Text("Name")
|
||||
Spacer()
|
||||
TextField(
|
||||
"Name",
|
||||
text: $name,
|
||||
axis: .vertical
|
||||
)
|
||||
.foregroundColor(Color.gray)
|
||||
.onChange(of: name) {
|
||||
var totalBytes = name.utf8.count
|
||||
// Only mess with the value if it is too big
|
||||
while totalBytes > 30 {
|
||||
name = String(name.dropLast())
|
||||
totalBytes = name.utf8.count
|
||||
}
|
||||
newWaypoint.latitudeI = waypoint.latitudeI
|
||||
newWaypoint.longitudeI = waypoint.longitudeI
|
||||
waypoint.name = name.count > 0 ? name : "Dropped Pin"
|
||||
}
|
||||
}
|
||||
HStack {
|
||||
Text("Description")
|
||||
Spacer()
|
||||
TextField(
|
||||
"Description",
|
||||
text: $description,
|
||||
axis: .vertical
|
||||
)
|
||||
.foregroundColor(Color.gray)
|
||||
.onChange(of: description) {
|
||||
var totalBytes = description.utf8.count
|
||||
// Only mess with the value if it is too big
|
||||
while totalBytes > 100 {
|
||||
description = String(description.dropLast())
|
||||
totalBytes = description.utf8.count
|
||||
}
|
||||
}
|
||||
}
|
||||
HStack {
|
||||
Text("Icon")
|
||||
Spacer()
|
||||
EmojiOnlyTextField(text: $icon, placeholder: "Select an emoji")
|
||||
.font(.title)
|
||||
.focused($iconIsFocused)
|
||||
.onChange(of: icon) {
|
||||
// If it contains non-emoji characters, clear it
|
||||
if !icon.onlyEmojis() {
|
||||
icon = ""
|
||||
return
|
||||
}
|
||||
|
||||
// If multiple emojis are entered or pasted, keep only the last one
|
||||
if icon.count > 1 {
|
||||
icon = String(icon.suffix(1))
|
||||
}
|
||||
iconIsFocused = false
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Toggle(isOn: $expires) {
|
||||
Label("Expires", systemImage: "clock.badge.xmark")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
if expires {
|
||||
DatePicker("Expire", selection: $expire, in: Date.now...)
|
||||
.datePickerStyle(.compact)
|
||||
.font(.callout)
|
||||
}
|
||||
Toggle(isOn: $locked) {
|
||||
Label("Locked", systemImage: "lock")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
}
|
||||
}
|
||||
.scrollContentBackground(.hidden)
|
||||
.scrollDismissesKeyboard(.immediately)
|
||||
HStack {
|
||||
Button {
|
||||
guard let deviceNum = accessoryManager.activeDeviceNum else {
|
||||
Logger.mesh.warning("Send waypoint failed: No deviceNum")
|
||||
return
|
||||
}
|
||||
if accessoryManager.isConnected {
|
||||
/// Send a new or exiting waypoint
|
||||
var newWaypoint = Waypoint()
|
||||
if waypoint.id == 0 {
|
||||
newWaypoint.id = UInt32.random(in: UInt32(UInt8.max)..<UInt32.max)
|
||||
waypoint.createdBy = Int64(deviceNum)
|
||||
waypoint.id = Int64(newWaypoint.id)
|
||||
} else {
|
||||
waypoint.lastUpdatedBy = Int64(deviceNum)
|
||||
newWaypoint.id = UInt32(waypoint.id)
|
||||
}
|
||||
newWaypoint.latitudeI = waypoint.latitudeI
|
||||
newWaypoint.longitudeI = waypoint.longitudeI
|
||||
newWaypoint.name = name.count > 0 ? name : "Dropped Pin"
|
||||
newWaypoint.description_p = description
|
||||
// Unicode scalar value for the icon emoji string
|
||||
let unicodeScalers = icon.unicodeScalars
|
||||
// First element as an UInt32
|
||||
let unicode = unicodeScalers[unicodeScalers.startIndex].value
|
||||
newWaypoint.icon = unicode
|
||||
if locked {
|
||||
if lockedTo == 0 {
|
||||
newWaypoint.lockedTo = UInt32(deviceNum)
|
||||
} else {
|
||||
newWaypoint.lockedTo = UInt32(lockedTo)
|
||||
}
|
||||
}
|
||||
if expires {
|
||||
newWaypoint.expire = UInt32(expire.timeIntervalSince1970)
|
||||
} else {
|
||||
newWaypoint.expire = 0
|
||||
}
|
||||
|
||||
Task {
|
||||
do {
|
||||
try await accessoryManager.sendWaypoint(waypoint: newWaypoint)
|
||||
dismiss()
|
||||
} catch {
|
||||
Logger.mesh.warning("Send waypoint failed: \(error)")
|
||||
Task { @MainActor in
|
||||
waypointFailedAlert = true
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Logger.mesh.warning("Send waypoint failed, node not connected")
|
||||
}
|
||||
} label: {
|
||||
Label("Send", systemImage: "arrow.up")
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.buttonBorderShape(.capsule)
|
||||
.controlSize(.regular)
|
||||
.disabled(!accessoryManager.isConnected)
|
||||
.padding(.bottom)
|
||||
|
||||
Button(role: .cancel) {
|
||||
dismiss()
|
||||
} label: {
|
||||
Label("Cancel", systemImage: "x.circle")
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.buttonBorderShape(.capsule)
|
||||
.controlSize(.regular)
|
||||
.padding(.bottom)
|
||||
|
||||
if waypoint.id > 0 && accessoryManager.isConnected {
|
||||
|
||||
Menu {
|
||||
Button("For me", action: {
|
||||
context.delete(waypoint)
|
||||
do {
|
||||
try context.save()
|
||||
} catch {
|
||||
context.rollback()
|
||||
}
|
||||
dismiss() })
|
||||
Button("For everyone", action: {
|
||||
guard let deviceNum = accessoryManager.activeDeviceNum else {
|
||||
Logger.mesh.error("Unable to set waypoint: No Device num")
|
||||
return
|
||||
}
|
||||
var newWaypoint = Waypoint()
|
||||
newWaypoint.id = UInt32(waypoint.id)
|
||||
newWaypoint.name = name.count > 0 ? name : "Dropped Pin"
|
||||
newWaypoint.description_p = description
|
||||
newWaypoint.latitudeI = waypoint.latitudeI
|
||||
newWaypoint.longitudeI = waypoint.longitudeI
|
||||
// Unicode scalar value for the icon emoji string
|
||||
let unicodeScalers = icon.unicodeScalars
|
||||
// First element as an UInt32
|
||||
|
|
@ -171,101 +256,28 @@ struct WaypointForm: View {
|
|||
newWaypoint.lockedTo = UInt32(lockedTo)
|
||||
}
|
||||
}
|
||||
if expires {
|
||||
newWaypoint.expire = UInt32(expire.timeIntervalSince1970)
|
||||
} else {
|
||||
newWaypoint.expire = 0
|
||||
}
|
||||
|
||||
newWaypoint.expire = UInt32(1)
|
||||
Task {
|
||||
do {
|
||||
try await accessoryManager.sendWaypoint(waypoint: newWaypoint)
|
||||
dismiss()
|
||||
} catch {
|
||||
Logger.mesh.warning("Send waypoint failed: \(error)")
|
||||
Task { @MainActor in
|
||||
context.delete(waypoint)
|
||||
do {
|
||||
try context.save()
|
||||
} catch {
|
||||
context.rollback()
|
||||
}
|
||||
dismiss()
|
||||
}
|
||||
} catch {
|
||||
Logger.mesh.warning("Send waypoint failed")
|
||||
Task {@MainActor in
|
||||
waypointFailedAlert = true
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Logger.mesh.warning("Send waypoint failed, node not connected")
|
||||
}
|
||||
} label: {
|
||||
Label("Send", systemImage: "arrow.up")
|
||||
})
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.buttonBorderShape(.capsule)
|
||||
.controlSize(.regular)
|
||||
.disabled(!accessoryManager.isConnected)
|
||||
.padding(.bottom)
|
||||
|
||||
Button(role: .cancel) {
|
||||
dismiss()
|
||||
} label: {
|
||||
Label("Cancel", systemImage: "x.circle")
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.buttonBorderShape(.capsule)
|
||||
.controlSize(.regular)
|
||||
.padding(.bottom)
|
||||
|
||||
if waypoint.id > 0 && accessoryManager.isConnected {
|
||||
|
||||
Menu {
|
||||
Button("For me", action: {
|
||||
context.delete(waypoint)
|
||||
do {
|
||||
try context.save()
|
||||
} catch {
|
||||
context.rollback()
|
||||
}
|
||||
dismiss() })
|
||||
Button("For everyone", action: {
|
||||
guard let deviceNum = accessoryManager.activeDeviceNum else {
|
||||
Logger.mesh.error("Unable to set waypoint: No Device num")
|
||||
return
|
||||
}
|
||||
var newWaypoint = Waypoint()
|
||||
newWaypoint.id = UInt32(waypoint.id)
|
||||
newWaypoint.name = name.count > 0 ? name : "Dropped Pin"
|
||||
newWaypoint.description_p = description
|
||||
newWaypoint.latitudeI = waypoint.latitudeI
|
||||
newWaypoint.longitudeI = waypoint.longitudeI
|
||||
// Unicode scalar value for the icon emoji string
|
||||
let unicodeScalers = icon.unicodeScalars
|
||||
// First element as an UInt32
|
||||
let unicode = unicodeScalers[unicodeScalers.startIndex].value
|
||||
newWaypoint.icon = unicode
|
||||
if locked {
|
||||
if lockedTo == 0 {
|
||||
newWaypoint.lockedTo = UInt32(deviceNum)
|
||||
} else {
|
||||
newWaypoint.lockedTo = UInt32(lockedTo)
|
||||
}
|
||||
}
|
||||
newWaypoint.expire = UInt32(1)
|
||||
Task {
|
||||
do {
|
||||
try await accessoryManager.sendWaypoint(waypoint: newWaypoint)
|
||||
Task { @MainActor in
|
||||
context.delete(waypoint)
|
||||
do {
|
||||
try context.save()
|
||||
} catch {
|
||||
context.rollback()
|
||||
}
|
||||
dismiss()
|
||||
}
|
||||
} catch {
|
||||
Logger.mesh.warning("Send waypoint failed")
|
||||
Task {@MainActor in
|
||||
waypointFailedAlert = true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
label: {
|
||||
Label("Delete", systemImage: "trash")
|
||||
.foregroundColor(.red)
|
||||
|
|
@ -274,130 +286,167 @@ struct WaypointForm: View {
|
|||
.buttonBorderShape(.capsule)
|
||||
.controlSize(.regular)
|
||||
.padding(.bottom)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
VStack {
|
||||
HStack {
|
||||
CircleText(text: String(UnicodeScalar(Int(waypoint.icon)) ?? "📍"), color: Color.orange, circleSize: 50)
|
||||
Spacer()
|
||||
Text(waypoint.name ?? "?")
|
||||
.font(.largeTitle)
|
||||
Spacer()
|
||||
if waypoint.locked > 0 && waypoint.locked != UInt32(accessoryManager.activeDeviceNum ?? 0) {
|
||||
Image(systemName: "lock.fill")
|
||||
.font(.largeTitle)
|
||||
} else {
|
||||
Button {
|
||||
editMode = true
|
||||
selectedDetent = .fraction(0.85)
|
||||
} label: {
|
||||
Image(systemName: "square.and.pencil" )
|
||||
.font(.largeTitle)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
VStack {
|
||||
HStack {
|
||||
CircleText(text: String(UnicodeScalar(Int(waypoint.icon)) ?? "📍"), color: Color.orange, circleSize: 50)
|
||||
Spacer()
|
||||
Text(waypoint.name ?? "?")
|
||||
.font(.largeTitle)
|
||||
Spacer()
|
||||
if waypoint.locked > 0 && waypoint.locked != UInt32(accessoryManager.activeDeviceNum ?? 0) {
|
||||
Image(systemName: "lock.fill")
|
||||
.font(.largeTitle)
|
||||
} else {
|
||||
Button {
|
||||
editMode = true
|
||||
selectedDetent = .fraction(0.85)
|
||||
} label: {
|
||||
Image(systemName: "square.and.pencil" )
|
||||
.font(.largeTitle)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.foregroundColor(.accentColor)
|
||||
Divider()
|
||||
VStack(alignment: .leading) {
|
||||
|
||||
// Nodes who created/modified
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
if let created = createdByNode {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text("Created by:")
|
||||
.font(.headline)
|
||||
|
||||
HStack(spacing: 8) {
|
||||
CircleText(
|
||||
text: created.user?.shortName ?? "?",
|
||||
color: Color(UIColor(hex: UInt32(created.user?.num ?? 0x808080)))
|
||||
)
|
||||
Text(created.user?.longName ?? "Unknown")
|
||||
.font(.body)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let updated = lastUpdatedByNode {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text("Last updated by:")
|
||||
.font(.headline)
|
||||
|
||||
HStack(spacing: 8) {
|
||||
CircleText(
|
||||
text: updated.user?.shortName ?? "?",
|
||||
color: Color(UIColor(hex: UInt32(updated.user?.num ?? 0x808080)))
|
||||
)
|
||||
Text(updated.user?.longName ?? "Unknown")
|
||||
.font(.body)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Divider()
|
||||
VStack(alignment: .leading) {
|
||||
// Description
|
||||
if (waypoint.longDescription ?? "").count > 0 {
|
||||
Label {
|
||||
Text(waypoint.longDescription ?? "")
|
||||
.foregroundColor(.primary)
|
||||
.multilineTextAlignment(.leading)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
} icon: {
|
||||
Image(systemName: "doc.plaintext")
|
||||
}
|
||||
.padding(.bottom)
|
||||
}
|
||||
/// Coordinate
|
||||
.padding(.bottom)
|
||||
|
||||
// Description
|
||||
if (waypoint.longDescription ?? "").count > 0 {
|
||||
Label {
|
||||
Text("Coordinates:")
|
||||
Text(waypoint.longDescription ?? "")
|
||||
.foregroundColor(.primary)
|
||||
Text("\(String(format: "%.6f", waypoint.coordinate.latitude)), \(String(format: "%.6f", waypoint.coordinate.longitude))")
|
||||
.multilineTextAlignment(.leading)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.textSelection(.enabled)
|
||||
.foregroundColor(.secondary)
|
||||
.font(.caption2)
|
||||
} icon: {
|
||||
Image(systemName: "mappin.circle")
|
||||
Image(systemName: "doc.plaintext")
|
||||
}
|
||||
.padding(.bottom)
|
||||
// Drop Maps Pin
|
||||
Button(action: {
|
||||
if let url = URL(string: "http://maps.apple.com/?ll=\(waypoint.coordinate.latitude),\(waypoint.coordinate.longitude)&q=\(waypoint.name ?? "Dropped Pin")") {
|
||||
UIApplication.shared.open(url)
|
||||
}
|
||||
}) {
|
||||
Label("Drop Pin in Maps", systemImage: "mappin.and.ellipse")
|
||||
}
|
||||
/// Coordinate
|
||||
Label {
|
||||
Text("Coordinates:")
|
||||
.foregroundColor(.primary)
|
||||
Text("\(String(format: "%.6f", waypoint.coordinate.latitude)), \(String(format: "%.6f", waypoint.coordinate.longitude))")
|
||||
.textSelection(.enabled)
|
||||
.foregroundColor(.secondary)
|
||||
.font(.caption2)
|
||||
} icon: {
|
||||
Image(systemName: "mappin.circle")
|
||||
}
|
||||
.padding(.bottom)
|
||||
// Drop Maps Pin
|
||||
Button(action: {
|
||||
if let url = URL(string: "http://maps.apple.com/?ll=\(waypoint.coordinate.latitude),\(waypoint.coordinate.longitude)&q=\(waypoint.name ?? "Dropped Pin")") {
|
||||
UIApplication.shared.open(url)
|
||||
}
|
||||
.padding(.bottom)
|
||||
/// Created
|
||||
}) {
|
||||
Label("Drop Pin in Maps", systemImage: "mappin.and.ellipse")
|
||||
}
|
||||
.padding(.bottom)
|
||||
/// Created
|
||||
Label {
|
||||
Text("Created: \(waypoint.created?.formatted() ?? "?")")
|
||||
.foregroundColor(.primary)
|
||||
} icon: {
|
||||
Image(systemName: "clock.badge.checkmark")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
}
|
||||
.padding(.bottom)
|
||||
/// Updated
|
||||
if waypoint.lastUpdated != nil {
|
||||
Label {
|
||||
Text("Created: \(waypoint.created?.formatted() ?? "?")")
|
||||
Text("Updated: \(waypoint.lastUpdated?.formatted() ?? "?")")
|
||||
.foregroundColor(.primary)
|
||||
} icon: {
|
||||
Image(systemName: "clock.badge.checkmark")
|
||||
Image(systemName: "clock.arrow.circlepath")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
}
|
||||
.padding(.bottom)
|
||||
/// Updated
|
||||
if waypoint.lastUpdated != nil {
|
||||
Label {
|
||||
Text("Updated: \(waypoint.lastUpdated?.formatted() ?? "?")")
|
||||
.foregroundColor(.primary)
|
||||
} icon: {
|
||||
Image(systemName: "clock.arrow.circlepath")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
}
|
||||
.padding(.bottom)
|
||||
}
|
||||
/// Expires
|
||||
if waypoint.expire != nil {
|
||||
Label {
|
||||
Text("Expires: \(waypoint.expire?.formatted() ?? "?")")
|
||||
.foregroundColor(.primary)
|
||||
} icon: {
|
||||
Image(systemName: "hourglass.bottomhalf.filled")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
}
|
||||
/// Expires
|
||||
if waypoint.expire != nil {
|
||||
.padding(.bottom, 5)
|
||||
}
|
||||
/// Distance
|
||||
if let cl = LocationsHandler.currentLocation {
|
||||
if cl.distance(from: cl) > 0.0 {
|
||||
let metersAway = waypoint.coordinate.distance(from: cl)
|
||||
Label {
|
||||
Text("Expires: \(waypoint.expire?.formatted() ?? "?")")
|
||||
Text("Distance".localized + ": \(distanceFormatter.string(fromDistance: Double(metersAway)))")
|
||||
.foregroundColor(.primary)
|
||||
} icon: {
|
||||
Image(systemName: "hourglass.bottomhalf.filled")
|
||||
Image(systemName: "lines.measurement.horizontal")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.frame(width: 35)
|
||||
}
|
||||
.padding(.bottom, 5)
|
||||
}
|
||||
/// Distance
|
||||
if let cl = LocationsHandler.currentLocation {
|
||||
if cl.distance(from: cl) > 0.0 {
|
||||
let metersAway = waypoint.coordinate.distance(from: cl)
|
||||
Label {
|
||||
Text("Distance".localized + ": \(distanceFormatter.string(fromDistance: Double(metersAway)))")
|
||||
.foregroundColor(.primary)
|
||||
} icon: {
|
||||
Image(systemName: "lines.measurement.horizontal")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.frame(width: 35)
|
||||
}
|
||||
.padding(.bottom, 5)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.top)
|
||||
#if targetEnvironment(macCatalyst)
|
||||
Spacer()
|
||||
Button {
|
||||
dismiss()
|
||||
} label: {
|
||||
Label("Close", systemImage: "xmark")
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.buttonBorderShape(.capsule)
|
||||
.controlSize(.large)
|
||||
.padding()
|
||||
#endif
|
||||
}
|
||||
.padding(.top)
|
||||
#if targetEnvironment(macCatalyst)
|
||||
Spacer()
|
||||
Button {
|
||||
dismiss()
|
||||
} label: {
|
||||
Label("Close", systemImage: "xmark")
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.buttonBorderShape(.capsule)
|
||||
.controlSize(.large)
|
||||
.padding()
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
.alert("Waypoint Failed to Send", isPresented: $waypointFailedAlert) {
|
||||
Button("OK", role: .cancel) {
|
||||
context.delete(waypoint)
|
||||
|
|
@ -421,6 +470,9 @@ struct WaypointForm: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.task {
|
||||
await fetchNodeInfo()
|
||||
}
|
||||
.onAppear {
|
||||
if waypoint.id > 0 {
|
||||
let waypoint = getWaypoint(id: Int64(waypoint.id), context: context)
|
||||
|
|
@ -453,4 +505,37 @@ struct WaypointForm: View {
|
|||
.presentationBackgroundInteraction(.enabled(upThrough: .fraction(0.85)))
|
||||
.presentationDragIndicator(.visible)
|
||||
}
|
||||
|
||||
private func fetchNodeInfo() async {
|
||||
// --- Fetch createdBy node ---
|
||||
if waypoint.createdBy != 0 {
|
||||
let createdByFetch: NSFetchRequest<NodeInfoEntity> = NodeInfoEntity.fetchRequest()
|
||||
createdByFetch.predicate = NSPredicate(format: "num == %lld", Int64(waypoint.createdBy))
|
||||
createdByFetch.fetchLimit = 1
|
||||
|
||||
do {
|
||||
let nodes = try context.fetch(createdByFetch)
|
||||
createdByNode = nodes.first
|
||||
} catch {
|
||||
Logger.services.warning("Error fetching createdBy node: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
|
||||
// --- Fetch lastUpdatedBy node (only if different from createdBy) ---
|
||||
if waypoint.lastUpdatedBy != 0,
|
||||
waypoint.lastUpdatedBy != waypoint.createdBy {
|
||||
let updatedByFetch: NSFetchRequest<NodeInfoEntity> = NodeInfoEntity.fetchRequest()
|
||||
updatedByFetch.predicate = NSPredicate(format: "num == %lld", Int64(waypoint.lastUpdatedBy))
|
||||
updatedByFetch.fetchLimit = 1
|
||||
|
||||
do {
|
||||
let nodes = try context.fetch(updatedByFetch)
|
||||
lastUpdatedByNode = nodes.first
|
||||
} catch {
|
||||
Logger.services.warning("Error fetching lastUpdatedBy node: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -120,12 +120,17 @@ struct MeshMap: View {
|
|||
}
|
||||
.sheet(item: $selectedWaypoint) { selection in
|
||||
WaypointForm(waypoint: selection)
|
||||
.presentationDetents([.large])
|
||||
.padding()
|
||||
.presentationDetents([.large]) // full screen
|
||||
.presentationDragIndicator(.visible)
|
||||
}
|
||||
.sheet(item: $editingWaypoint) { selection in
|
||||
WaypointForm(waypoint: selection, editMode: true)
|
||||
.padding()
|
||||
.presentationDetents([.large])
|
||||
.presentationDragIndicator(.visible)
|
||||
}
|
||||
|
||||
.sheet(isPresented: $editingSettings) {
|
||||
MapSettingsForm(traffic: $showTraffic, pointsOfInterest: $showPointsOfInterest, mapLayer: $selectedMapLayer, meshMap: $isMeshMap, enabledOverlayConfigs: $enabledOverlayConfigs)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue