mirror of
https://github.com/meshtastic/Meshtastic-Apple.git
synced 2026-04-20 22:13:56 +00:00
Merge branch 'main'
This commit is contained in:
commit
29d7f594e6
33 changed files with 1250 additions and 1323 deletions
File diff suppressed because it is too large
Load diff
|
|
@ -21,15 +21,15 @@ enum MeshMapTypes: Int, CaseIterable, Identifiable {
|
|||
case .standard:
|
||||
return "Standard".localized
|
||||
case .mutedStandard:
|
||||
return "standard.muted".localized
|
||||
return "Standard Muted".localized
|
||||
case .hybrid:
|
||||
return "hybrid".localized
|
||||
return "Hybrid".localized
|
||||
case .hybridFlyover:
|
||||
return "hybrid.flyover".localized
|
||||
return "Hybrid Flyover".localized
|
||||
case .satellite:
|
||||
return "satellite".localized
|
||||
return "Satellite".localized
|
||||
case .satelliteFlyover:
|
||||
return "satellite.flyover".localized
|
||||
return "Satellite Flyover".localized
|
||||
}
|
||||
}
|
||||
func MKMapTypeValue() -> MKMapType {
|
||||
|
|
|
|||
|
|
@ -168,17 +168,17 @@ enum RebroadcastModes: Int, CaseIterable, Identifiable {
|
|||
var description: String {
|
||||
switch self {
|
||||
case .all:
|
||||
return "Rebroadcast any observed message, if it was on our private channel or from another mesh with the same lora params."
|
||||
return "Rebroadcast any observed message, if it was on our private channel or from another mesh with the same lora params.".localized
|
||||
case .allSkipDecoding:
|
||||
return "Same as behavior as ALL but skips packet decoding and simply rebroadcasts them. Only available in Repeater role. Setting this on any other roles will result in ALL behavior."
|
||||
return "Same as behavior as ALL but skips packet decoding and simply rebroadcasts them. Only available in Repeater role. Setting this on any other roles will result in ALL behavior.".localized
|
||||
case .localOnly:
|
||||
return "Ignores observed messages from foreign meshes that are open or those which it cannot decrypt. Only rebroadcasts message on the nodes local primary / secondary channels."
|
||||
return "Ignores observed messages from foreign meshes that are open or those which it cannot decrypt. Only rebroadcasts message on the nodes local primary / secondary channels.".localized
|
||||
case .knownOnly:
|
||||
return "Ignores observed messages from foreign meshes like Local Only, but takes it step further by also ignoring messages from nodes not already in the node's known list."
|
||||
return "Ignores observed messages from foreign meshes like Local Only, but takes it step further by also ignoring messages from nodes not already in the node's known list.".localized
|
||||
case .none:
|
||||
return "Only permitted for SENSOR, TRACKER and TAK_TRACKER roles, this will inhibit all rebroadcasts, not unlike CLIENT_MUTE role."
|
||||
return "Only permitted for SENSOR, TRACKER and TAK_TRACKER roles, this will inhibit all rebroadcasts, not unlike CLIENT_MUTE role.".localized
|
||||
case .corePortnums:
|
||||
return "Only rebroadcasts packets from the core portnums: NodeInfo, Text, Position, Telemetry, and Routing."
|
||||
return "Only rebroadcasts packets from the core portnums: NodeInfo, Text, Position, Telemetry, and Routing.".localized
|
||||
}
|
||||
}
|
||||
func protoEnumValue() -> Config.DeviceConfig.RebroadcastMode {
|
||||
|
|
|
|||
|
|
@ -32,31 +32,31 @@ enum RoutingError: Int, CaseIterable, Identifiable {
|
|||
switch self {
|
||||
|
||||
case .none:
|
||||
return "routing.acknowledged".localized
|
||||
return "Acknowledged".localized
|
||||
case .noRoute:
|
||||
return "routing.noroute".localized
|
||||
return "No Route".localized
|
||||
case .gotNak:
|
||||
return "routing.gotnak".localized
|
||||
return "Received a negative acknowledgment".localized
|
||||
case .timeout:
|
||||
return "routing.timeout".localized
|
||||
return "Timeout".localized
|
||||
case .noInterface:
|
||||
return "routing.nointerface".localized
|
||||
return "No Interface".localized
|
||||
case .maxRetransmit:
|
||||
return "routing.maxretransmit".localized
|
||||
return "Max Retransmission Reached".localized
|
||||
case .noChannel:
|
||||
return "routing.nochannel".localized
|
||||
return "No Channel".localized
|
||||
case .tooLarge:
|
||||
return "routing.toolarge".localized
|
||||
return "The packet is too large".localized
|
||||
case .noResponse:
|
||||
return "routing.noresponse".localized
|
||||
return "No Response".localized
|
||||
case .dutyCycleLimit:
|
||||
return "routing.dutycyclelimit".localized
|
||||
return "Regional Duty Cycle Limit Reached".localized
|
||||
case .badRequest:
|
||||
return "routing.badRequest".localized
|
||||
return "Bad Request".localized
|
||||
case .notAuthorized:
|
||||
return "routing.notauthorized".localized
|
||||
return "Not Authorized".localized
|
||||
case .pkiFailed:
|
||||
return "routing.pkifailed".localized
|
||||
return "Encrypted Send Failed".localized
|
||||
case .pkiUnknownPubkey:
|
||||
return "Unknown public key".localized
|
||||
case .adminBadSessionKey:
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ extension UserDefaults {
|
|||
@UserDefault(.meshMapDistance, defaultValue: 800000)
|
||||
static var meshMapDistance: Double
|
||||
|
||||
@UserDefault(.enableMapWaypoints, defaultValue: false)
|
||||
@UserDefault(.enableMapWaypoints, defaultValue: true)
|
||||
static var enableMapWaypoints: Bool
|
||||
|
||||
@UserDefault(.enableMapRecentering, defaultValue: false)
|
||||
|
|
|
|||
|
|
@ -227,6 +227,7 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext)
|
|||
let myInfoEntity = MyInfoEntity(context: context)
|
||||
myInfoEntity.myNodeNum = Int64(packet.from)
|
||||
myInfoEntity.rebootCount = 0
|
||||
newNode.myInfo = myInfoEntity
|
||||
do {
|
||||
try context.save()
|
||||
Logger.data.info("💾 [NodeInfo] Saved a NodeInfo for node number: \(packet.from.toHex(), privacy: .public)")
|
||||
|
|
@ -236,7 +237,6 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext)
|
|||
let nsError = error as NSError
|
||||
Logger.data.error("💥 [MyInfoEntity] Error Inserting New Core Data: \(nsError, privacy: .public)")
|
||||
}
|
||||
newNode.myInfo = myInfoEntity
|
||||
|
||||
} else {
|
||||
// Update an existing node
|
||||
|
|
@ -1101,7 +1101,7 @@ func upsertExternalNotificationModuleConfigPacket(config: ModuleConfig.ExternalN
|
|||
|
||||
func upsertPaxCounterModuleConfigPacket(config: ModuleConfig.PaxcounterConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) {
|
||||
|
||||
let logString = String.localizedStringWithFormat("mesh.log.paxcounter.config %@".localized, String(nodeNum))
|
||||
let logString = String.localizedStringWithFormat("PAX Counter config received: %@".localized, String(nodeNum))
|
||||
Logger.data.info("🧑🤝🧑 \(logString, privacy: .public)")
|
||||
|
||||
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
|
||||
|
|
|
|||
|
|
@ -144,7 +144,7 @@ struct ChannelList: View {
|
|||
context.refresh(myInfo, mergeChanges: true)
|
||||
channelSelection = nil
|
||||
} label: {
|
||||
Text("delete")
|
||||
Text("Delete")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@ struct ChannelMessageList: View {
|
|||
@State private var hasReachedBottom = false
|
||||
@State private var gotFirstUnreadMessage: Bool = false
|
||||
|
||||
@State private var messageToHighlight: Int64 = 0
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
ScrollViewReader { scrollView in
|
||||
|
|
@ -36,16 +38,33 @@ struct ChannelMessageList: View {
|
|||
if message.replyID > 0 {
|
||||
let messageReply = channel.allPrivateMessages.first(where: { $0.messageId == message.replyID })
|
||||
HStack {
|
||||
Text(messageReply?.messagePayload ?? "EMPTY MESSAGE").foregroundColor(.accentColor).font(.caption2)
|
||||
.padding(10)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 18)
|
||||
.stroke(Color.blue, lineWidth: 0.5)
|
||||
)
|
||||
Image(systemName: "arrowshape.turn.up.left.fill")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.imageScale(.large).foregroundColor(.accentColor)
|
||||
.padding(.trailing)
|
||||
Button {
|
||||
if let messageNum = messageReply?.messageId {
|
||||
withAnimation(.easeInOut(duration: 0.5)) {
|
||||
messageToHighlight = messageNum
|
||||
}
|
||||
scrollView.scrollTo(messageNum, anchor: .center)
|
||||
|
||||
// Reset highlight after delay
|
||||
Task {
|
||||
try? await Task.sleep(nanoseconds: 1_000_000_000) // 1 second
|
||||
withAnimation(.easeInOut(duration: 0.5)) {
|
||||
messageToHighlight = -1
|
||||
}
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Text(messageReply?.messagePayload ?? "EMPTY MESSAGE").foregroundColor(.accentColor).font(.caption2)
|
||||
.padding(10)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 18)
|
||||
.stroke(Color.blue, lineWidth: 0.5)
|
||||
)
|
||||
Image(systemName: "arrowshape.turn.up.left.fill")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.imageScale(.large).foregroundColor(.accentColor)
|
||||
.padding(.trailing)
|
||||
}
|
||||
}
|
||||
}
|
||||
HStack(alignment: .bottom) {
|
||||
|
|
@ -111,6 +130,12 @@ struct ChannelMessageList: View {
|
|||
Spacer(minLength: 50)
|
||||
}
|
||||
}
|
||||
|
||||
.overlay {
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.stroke(.blue, lineWidth: 2)
|
||||
.opacity(((messageToHighlight == message.messageId) || (replyMessageId == message.messageId)) ? 1 : 0)
|
||||
}
|
||||
.padding([.leading, .trailing])
|
||||
.frame(maxWidth: .infinity)
|
||||
.id(message.messageId)
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ struct MessageContextMenuItems: View {
|
|||
Button(role: .destructive) {
|
||||
isShowingDeleteConfirmation = true
|
||||
} label: {
|
||||
Text("delete")
|
||||
Text("Delete")
|
||||
Image(systemName: "trash")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ struct Messages: View {
|
|||
|
||||
TipView(MessagesTip(), arrowEdge: .top)
|
||||
}
|
||||
.navigationTitle("messages")
|
||||
.navigationTitle("Messages")
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
.navigationBarItems(leading: MeshtasticLogo())
|
||||
} content: {
|
||||
|
|
|
|||
|
|
@ -15,78 +15,93 @@ struct TextMessageField: View {
|
|||
@State private var sendPositionWithMessage = false
|
||||
|
||||
var body: some View {
|
||||
#if targetEnvironment(macCatalyst)
|
||||
HStack {
|
||||
if destination.showAlertButton {
|
||||
VStack {
|
||||
#if targetEnvironment(macCatalyst)
|
||||
HStack {
|
||||
if destination.showAlertButton {
|
||||
Spacer()
|
||||
AlertButton { typingMessage += "🔔 Alert Bell! \u{7}" }
|
||||
}
|
||||
Spacer()
|
||||
AlertButton { typingMessage += "🔔 Alert Bell! \u{7}" }
|
||||
RequestPositionButton(action: requestPosition)
|
||||
TextMessageSize(maxbytes: Self.maxbytes, totalBytes: totalBytes).padding(.trailing)
|
||||
}
|
||||
Spacer()
|
||||
RequestPositionButton(action: requestPosition)
|
||||
TextMessageSize(maxbytes: Self.maxbytes, totalBytes: totalBytes).padding(.trailing)
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
HStack(alignment: .top) {
|
||||
ZStack {
|
||||
TextField("Message", text: $typingMessage, axis: .vertical)
|
||||
.onChange(of: typingMessage) { _, value in
|
||||
totalBytes = value.utf8.count
|
||||
// Only mess with the value if it is too big
|
||||
while totalBytes > Self.maxbytes {
|
||||
typingMessage = String(typingMessage.dropLast())
|
||||
totalBytes = typingMessage.utf8.count
|
||||
}
|
||||
}
|
||||
.keyboardType(.default)
|
||||
.toolbar {
|
||||
ToolbarItemGroup(placement: .keyboard) {
|
||||
Button("Dismiss") {
|
||||
isFocused = false
|
||||
HStack(alignment: .top) {
|
||||
if replyMessageId != 0 {
|
||||
HStack {
|
||||
Button {
|
||||
withAnimation(.easeInOut(duration: 0.2)) {
|
||||
replyMessageId = 0
|
||||
}
|
||||
.font(.subheadline)
|
||||
isFocused = false
|
||||
} label: {
|
||||
Image(systemName: "x.circle.fill")
|
||||
}
|
||||
Text("Replying to a message")
|
||||
}
|
||||
}
|
||||
|
||||
ZStack {
|
||||
TextField("Message", text: $typingMessage, axis: .vertical)
|
||||
.onChange(of: typingMessage) { _, value in
|
||||
totalBytes = value.utf8.count
|
||||
while totalBytes > Self.maxbytes {
|
||||
typingMessage = String(typingMessage.dropLast())
|
||||
totalBytes = typingMessage.utf8.count
|
||||
}
|
||||
}
|
||||
.keyboardType(.default)
|
||||
.toolbar {
|
||||
ToolbarItemGroup(placement: .keyboard) {
|
||||
Button("Dismiss") {
|
||||
isFocused = false
|
||||
}
|
||||
.font(.subheadline)
|
||||
|
||||
if destination.showAlertButton {
|
||||
Spacer()
|
||||
AlertButton { typingMessage += "🔔 Alert Bell Character! \u{7}" }
|
||||
}
|
||||
|
||||
if destination.showAlertButton {
|
||||
Spacer()
|
||||
AlertButton { typingMessage += "🔔 Alert Bell Character! \u{7}" }
|
||||
RequestPositionButton(action: requestPosition)
|
||||
TextMessageSize(maxbytes: Self.maxbytes, totalBytes: totalBytes)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
RequestPositionButton(action: requestPosition)
|
||||
TextMessageSize(maxbytes: Self.maxbytes, totalBytes: totalBytes)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 8)
|
||||
.focused($isFocused)
|
||||
.multilineTextAlignment(.leading)
|
||||
.frame(minHeight: 50)
|
||||
.keyboardShortcut(.defaultAction)
|
||||
.onSubmit {
|
||||
#if targetEnvironment(macCatalyst)
|
||||
sendMessage()
|
||||
#endif
|
||||
}
|
||||
.padding(.horizontal, 8)
|
||||
.focused($isFocused)
|
||||
.multilineTextAlignment(.leading)
|
||||
.frame(minHeight: 50)
|
||||
.keyboardShortcut(.defaultAction)
|
||||
.onSubmit {
|
||||
#if targetEnvironment(macCatalyst)
|
||||
sendMessage()
|
||||
#endif
|
||||
}
|
||||
|
||||
Text(typingMessage)
|
||||
.opacity(0)
|
||||
.padding(.all, 0)
|
||||
}
|
||||
.overlay(RoundedRectangle(cornerRadius: 20).stroke(.tertiary, lineWidth: 1))
|
||||
.padding(.bottom, 15)
|
||||
Text(typingMessage)
|
||||
.opacity(0)
|
||||
.padding(.all, 0)
|
||||
}
|
||||
.overlay(RoundedRectangle(cornerRadius: 20).stroke(.tertiary, lineWidth: 1))
|
||||
.padding(.bottom, 15)
|
||||
|
||||
Button(action: sendMessage) {
|
||||
Image(systemName: "arrow.up.circle.fill")
|
||||
.font(.largeTitle)
|
||||
.foregroundColor(.accentColor)
|
||||
Button(action: sendMessage) {
|
||||
Image(systemName: "arrow.up.circle.fill")
|
||||
.font(.largeTitle)
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
}
|
||||
.padding(.all, 15)
|
||||
}
|
||||
.padding(.all, 15)
|
||||
}
|
||||
|
||||
private func requestPosition() {
|
||||
let userLongName = bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.longName : "Unknown"
|
||||
sendPositionWithMessage = true
|
||||
typingMessage = "📍 " + userLongName + " \(destination.positionShareMessage)."
|
||||
typingMessage = "📍 " + userLongName + " \(destination.positionShareMessage)."
|
||||
}
|
||||
|
||||
private func sendMessage() {
|
||||
|
|
|
|||
|
|
@ -187,7 +187,7 @@ struct UserList: View {
|
|||
deleteUserMessages(user: userSelection!, context: context)
|
||||
context.refresh(node!.user!, mergeChanges: true)
|
||||
} label: {
|
||||
Text("delete")
|
||||
Text("Delete")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@ struct UserMessageList: View {
|
|||
@State private var hasReachedBottom = false
|
||||
@State private var gotFirstUnreadMessage: Bool = false
|
||||
|
||||
@State private var messageToHighlight: Int64 = 0
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
ScrollViewReader { scrollView in
|
||||
|
|
@ -38,16 +40,33 @@ struct UserMessageList: View {
|
|||
if message.replyID > 0 {
|
||||
let messageReply = user.messageList.first(where: { $0.messageId == message.replyID })
|
||||
HStack {
|
||||
Text(messageReply?.messagePayload ?? "EMPTY MESSAGE").foregroundColor(.accentColor).font(.caption2)
|
||||
.padding(10)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 18)
|
||||
.stroke(Color.blue, lineWidth: 0.5)
|
||||
)
|
||||
Image(systemName: "arrowshape.turn.up.left.fill")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.imageScale(.large).foregroundColor(.accentColor)
|
||||
.padding(.trailing)
|
||||
Button {
|
||||
if let messageNum = messageReply?.messageId {
|
||||
withAnimation(.easeInOut(duration: 0.5)) {
|
||||
messageToHighlight = messageNum
|
||||
}
|
||||
scrollView.scrollTo(messageNum, anchor: .center)
|
||||
|
||||
// Reset highlight after delay
|
||||
Task {
|
||||
try? await Task.sleep(nanoseconds: 1_000_000_000) // 1 second
|
||||
withAnimation(.easeInOut(duration: 0.5)) {
|
||||
messageToHighlight = -1
|
||||
}
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Text(messageReply?.messagePayload ?? "EMPTY MESSAGE").foregroundColor(.accentColor).font(.caption2)
|
||||
.padding(10)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 18)
|
||||
.stroke(Color.blue, lineWidth: 0.5)
|
||||
)
|
||||
Image(systemName: "arrowshape.turn.up.left.fill")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.imageScale(.large).foregroundColor(.accentColor)
|
||||
.padding(.trailing)
|
||||
}
|
||||
}
|
||||
}
|
||||
HStack(alignment: .top) {
|
||||
|
|
@ -100,6 +119,11 @@ struct UserMessageList: View {
|
|||
Spacer(minLength: 50)
|
||||
}
|
||||
}
|
||||
.overlay {
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.stroke(.blue, lineWidth: 2)
|
||||
.opacity(((messageToHighlight == message.messageId) || (replyMessageId == message.messageId)) ? 1 : 0)
|
||||
}
|
||||
.padding([.leading, .trailing])
|
||||
.frame(maxWidth: .infinity)
|
||||
.id(message.messageId)
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ struct DetectionSensorLog: View {
|
|||
exportString = detectionsToCsv(detections: chartData)
|
||||
isExporting = true
|
||||
} label: {
|
||||
Label("save", systemImage: "square.and.arrow.down")
|
||||
Label("Save", systemImage: "square.and.arrow.down")
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.buttonBorderShape(.capsule)
|
||||
|
|
|
|||
|
|
@ -222,7 +222,7 @@ struct DeviceMetricsLog: View {
|
|||
exportString = telemetryToCsvFile(telemetry: deviceMetrics, metricsType: 0)
|
||||
isExporting = true
|
||||
} label: {
|
||||
Label("save", systemImage: "square.and.arrow.down")
|
||||
Label("Save", systemImage: "square.and.arrow.down")
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.buttonBorderShape(.capsule)
|
||||
|
|
|
|||
|
|
@ -145,7 +145,7 @@ struct EnvironmentMetricsLog: View {
|
|||
exportString = telemetryToCsvFile(telemetry: environmentMetrics, metricsType: 1)
|
||||
isExporting = true
|
||||
} label: {
|
||||
Label("save", systemImage: "square.and.arrow.down")
|
||||
Label("Save", systemImage: "square.and.arrow.down")
|
||||
.imageScale(imageScale)
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ struct MeshMapContent: MapContent {
|
|||
@Binding var selectedMapLayer: MapLayer
|
||||
// Map Configuration
|
||||
@Binding var selectedPosition: PositionEntity?
|
||||
@AppStorage("enableMapWaypoints") private var showWaypoints = false
|
||||
@AppStorage("enableMapWaypoints") private var showWaypoints = true
|
||||
@Binding var selectedWaypoint: WaypointEntity?
|
||||
|
||||
@FetchRequest(fetchRequest: PositionEntity.allPositionsFetchRequest(), animation: .easeIn)
|
||||
|
|
@ -74,9 +74,9 @@ struct MeshMapContent: MapContent {
|
|||
}
|
||||
}
|
||||
}
|
||||
.onTapGesture { _ in
|
||||
.highPriorityGesture(TapGesture().onEnded { _ in
|
||||
selectedPosition = (selectedPosition == position ? nil : position)
|
||||
}
|
||||
})
|
||||
}
|
||||
/// Node History and Route Lines for favorites
|
||||
if let nodePosition = position.nodePosition,
|
||||
|
|
@ -186,7 +186,7 @@ struct MeshMapContent: MapContent {
|
|||
LazyVStack {
|
||||
ZStack {
|
||||
CircleText(text: String(UnicodeScalar(Int(waypoint.icon)) ?? "📍"), color: Color.orange, circleSize: 40)
|
||||
.onTapGesture(perform: { _ in
|
||||
.highPriorityGesture(TapGesture().onEnded { _ in
|
||||
selectedWaypoint = (selectedWaypoint == waypoint ? nil : waypoint)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ struct NodeMapContent: MapContent {
|
|||
/// Map State User Defaults
|
||||
@AppStorage("meshMapShowNodeHistory") private var showNodeHistory = false
|
||||
@AppStorage("meshMapShowRouteLines") private var showRouteLines = false
|
||||
@AppStorage("enableMapWaypoints") private var showWaypoints = false
|
||||
@AppStorage("enableMapWaypoints") private var showWaypoints = true
|
||||
@AppStorage("enableMapConvexHull") private var showConvexHull = false
|
||||
@AppStorage("enableMapTraffic") private var showTraffic: Bool = false
|
||||
@AppStorage("enableMapPointsOfInterest") private var showPointsOfInterest: Bool = false
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ struct PositionPopover: View {
|
|||
/// Time
|
||||
Label {
|
||||
if idiom != .phone {
|
||||
Text("heard".localized + ":")
|
||||
Text("Heard".localized + ":")
|
||||
}
|
||||
Text(position.time?.lastHeard ?? "unknown")
|
||||
.foregroundColor(.primary)
|
||||
|
|
|
|||
|
|
@ -134,40 +134,44 @@ struct WaypointForm: View {
|
|||
.scrollDismissesKeyboard(.immediately)
|
||||
HStack {
|
||||
Button {
|
||||
/// 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)
|
||||
}
|
||||
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(bleManager.connectedPeripheral!.num)
|
||||
if bleManager.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.lockedTo = UInt32(lockedTo)
|
||||
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(bleManager.connectedPeripheral!.num)
|
||||
} else {
|
||||
newWaypoint.lockedTo = UInt32(lockedTo)
|
||||
}
|
||||
}
|
||||
if expires {
|
||||
newWaypoint.expire = UInt32(expire.timeIntervalSince1970)
|
||||
} else {
|
||||
newWaypoint.expire = 0
|
||||
}
|
||||
if bleManager.sendWaypoint(waypoint: newWaypoint) {
|
||||
dismiss()
|
||||
} else {
|
||||
dismiss()
|
||||
Logger.mesh.warning("Send waypoint failed")
|
||||
}
|
||||
}
|
||||
if expires {
|
||||
newWaypoint.expire = UInt32(expire.timeIntervalSince1970)
|
||||
} else {
|
||||
newWaypoint.expire = 0
|
||||
}
|
||||
if bleManager.sendWaypoint(waypoint: newWaypoint) {
|
||||
dismiss()
|
||||
} else {
|
||||
dismiss()
|
||||
Logger.mesh.warning("Send waypoint failed")
|
||||
Logger.mesh.warning("Send waypoint failed, node not connected")
|
||||
}
|
||||
} label: {
|
||||
Label("Send", systemImage: "arrow.up")
|
||||
|
|
@ -235,7 +239,7 @@ struct WaypointForm: View {
|
|||
})
|
||||
}
|
||||
label: {
|
||||
Label("delete", systemImage: "trash")
|
||||
Label("Delete", systemImage: "trash")
|
||||
.foregroundColor(.red)
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
|
|
@ -364,6 +368,18 @@ struct WaypointForm: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
if waypoint.id == 0 {
|
||||
// New, unsent waypoint created by the user: delete it
|
||||
bleManager.context.delete(waypoint)
|
||||
do {
|
||||
try bleManager.context.save()
|
||||
} catch {
|
||||
bleManager.context.rollback()
|
||||
Logger.mesh.error("Failed to save context on waypoint deletion: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
if waypoint.id > 0 {
|
||||
let waypoint = getWaypoint(id: Int64(waypoint.id), context: bleManager.context)
|
||||
|
|
|
|||
|
|
@ -187,7 +187,7 @@ struct PaxCounterLog: View {
|
|||
exportString = paxToCsvFile(pax: pax)
|
||||
isExporting = true
|
||||
} label: {
|
||||
Label("save", systemImage: "square.and.arrow.down")
|
||||
Label("Save", systemImage: "square.and.arrow.down")
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.buttonBorderShape(.capsule)
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ struct TraceRouteLog: View {
|
|||
Logger.data.error("\(error.localizedDescription, privacy: .public)")
|
||||
}
|
||||
} label: {
|
||||
Label("delete", systemImage: "trash")
|
||||
Label("Delete", systemImage: "trash")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ struct AppData: View {
|
|||
Logger.services.error("🗑️ Delete file error: \(error, privacy: .public)")
|
||||
}
|
||||
} label: {
|
||||
Label("delete", systemImage: "trash")
|
||||
Label("Delete", systemImage: "trash")
|
||||
}
|
||||
}
|
||||
} icon: {
|
||||
|
|
@ -61,7 +61,7 @@ struct AppData: View {
|
|||
Logger.services.error("🗑️ Delete file error: \(error, privacy: .public)")
|
||||
}
|
||||
} label: {
|
||||
Label("delete", systemImage: "trash")
|
||||
Label("Delete", systemImage: "trash")
|
||||
}
|
||||
}
|
||||
} icon: {
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ struct AppLog: View {
|
|||
.foregroundStyle(value.level.color)
|
||||
}
|
||||
.width(min: 85, max: 110)
|
||||
TableColumn("log.category", value: \.category)
|
||||
TableColumn("Category", value: \.category)
|
||||
.width(min: 80, max: 130)
|
||||
TableColumn("Message", value: \.composedMessage) { value in
|
||||
Text(value.composedMessage)
|
||||
|
|
|
|||
|
|
@ -219,7 +219,7 @@ struct Channels: View {
|
|||
hasChanges = false
|
||||
}
|
||||
} label: {
|
||||
Label("save", systemImage: "square.and.arrow.down")
|
||||
Label("Save", systemImage: "square.and.arrow.down")
|
||||
}
|
||||
.disabled(bleManager.connectedPeripheral == nil)// || !hasChanges)// !hasValidKey)
|
||||
.buttonStyle(.bordered)
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ struct PaxCounterConfig: View {
|
|||
|
||||
var body: some View {
|
||||
Form {
|
||||
ConfigHeader(title: "config.module.paxcounter.title", config: \.powerConfig, node: node, onAppear: setPaxValues)
|
||||
ConfigHeader(title: "PAX Counter Config", config: \.powerConfig, node: node, onAppear: setPaxValues)
|
||||
|
||||
Section {
|
||||
Toggle(isOn: $enabled) {
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ struct RangeTestConfig: View {
|
|||
.font(.callout)
|
||||
|
||||
Toggle(isOn: $save) {
|
||||
Label("save", systemImage: "square.and.arrow.down.fill")
|
||||
Label("Save", systemImage: "square.and.arrow.down.fill")
|
||||
Text("Saves a CSV with the range test message details, currently only available on ESP32 devices with a web server.")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ struct SaveConfigButton: View {
|
|||
Button {
|
||||
isPresentingSaveConfirm = true
|
||||
} label: {
|
||||
Label("save", systemImage: "square.and.arrow.down")
|
||||
Label("Save", systemImage: "square.and.arrow.down")
|
||||
}
|
||||
.disabled(bleManager.connectedPeripheral == nil || !hasChanges)
|
||||
.buttonStyle(.bordered)
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ struct LogDetail: View {
|
|||
/// Category
|
||||
Label {
|
||||
HStack {
|
||||
Text("log.category".localized + ":")
|
||||
Text("Category".localized + ":")
|
||||
.font(idiom == .phone ? .caption : .title)
|
||||
.frame(width: idiom == .phone ? 115 : 190, alignment: .trailing)
|
||||
Text(log.category)
|
||||
|
|
|
|||
|
|
@ -154,7 +154,7 @@ struct Routes: View {
|
|||
Logger.data.error("\(error.localizedDescription, privacy: .public)")
|
||||
}
|
||||
} label: {
|
||||
Label("delete", systemImage: "trash")
|
||||
Label("Delete", systemImage: "trash")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -215,7 +215,7 @@ struct Routes: View {
|
|||
.buttonBorderShape(.capsule)
|
||||
.controlSize(.large)
|
||||
|
||||
Button("save") {
|
||||
Button("Save") {
|
||||
selectedRoute?.name = name
|
||||
selectedRoute?.notes = notes
|
||||
selectedRoute?.enabled = enabled
|
||||
|
|
@ -279,7 +279,7 @@ struct Routes: View {
|
|||
exportString = routeToCsvFile(locations: selectedRoute!.locations!.array as? [LocationEntity] ?? [])
|
||||
isExporting = true
|
||||
} label: {
|
||||
Label("export", systemImage: "square.and.arrow.down")
|
||||
Label("Export", systemImage: "square.and.arrow.down")
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.buttonBorderShape(.capsule)
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ struct SaveChannelQRCode: View {
|
|||
showError = true
|
||||
}
|
||||
} label: {
|
||||
Label("save", systemImage: "square.and.arrow.down")
|
||||
Label("Save", systemImage: "square.and.arrow.down")
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.buttonBorderShape(.capsule)
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ struct Settings: View {
|
|||
}
|
||||
|
||||
var deviceConfigurationSection: some View {
|
||||
Section("device.configuration") {
|
||||
Section("Device Configuration") {
|
||||
NavigationLink(value: SettingsNavigationState.user) {
|
||||
Label {
|
||||
Text("User")
|
||||
|
|
|
|||
|
|
@ -141,7 +141,7 @@ struct UserConfig: View {
|
|||
Button {
|
||||
isPresentingSaveConfirm = true
|
||||
} label: {
|
||||
Label("save", systemImage: "square.and.arrow.down")
|
||||
Label("Save", systemImage: "square.and.arrow.down")
|
||||
}
|
||||
.disabled(bleManager.connectedPeripheral == nil || !hasChanges)
|
||||
.buttonStyle(.bordered)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue