Merge pull request #761 from meshtastic/node-detail-actions

Add node actions to the node detail screen
This commit is contained in:
Garth Vander Houwen 2024-07-08 20:27:20 -07:00 committed by GitHub
commit 483e0ec513
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 491 additions and 225 deletions

View file

@ -330,6 +330,9 @@
},
"Acknowledged by another node" : {
},
"Actions" : {
},
"Active" : {
@ -400,6 +403,9 @@
},
"Add Channels" : {
},
"Add to favorites" : {
},
"Additional help" : {
@ -8201,6 +8207,9 @@
},
"Help with App Development" : {
},
"Hide alerts" : {
},
"Hide Alerts" : {
@ -17150,6 +17159,9 @@
},
"Remove" : {
},
"Remove from favorites" : {
},
"Replace Channels" : {
@ -19618,6 +19630,9 @@
},
"Short Name: %@" : {
},
"Show alerts" : {
},
"Show Alerts" : {
@ -20860,7 +20875,7 @@
"This conversation will be deleted." : {
},
"This could take a while, response will appear in the trace route log for the node it was sent to." : {
"This could take a while. The response will appear in the trace route log for the node it was sent to." : {
},
"This determines the actual frequency you are transmitting on in the band. If set to 0 this value will be calculated automatically based on the primary channel name." : {

View file

@ -7,6 +7,12 @@
objects = {
/* Begin PBXBuildFile section */
251926852C3BA97800249DF5 /* FavoriteNodeButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 251926842C3BA97800249DF5 /* FavoriteNodeButton.swift */; };
251926872C3BAE2200249DF5 /* NodeAlertsButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 251926862C3BAE2200249DF5 /* NodeAlertsButton.swift */; };
2519268A2C3BB1B200249DF5 /* ExchangePositionsButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 251926892C3BB1B200249DF5 /* ExchangePositionsButton.swift */; };
2519268C2C3BB52000249DF5 /* TraceRouteButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2519268B2C3BB52000249DF5 /* TraceRouteButton.swift */; };
251926902C3CB44900249DF5 /* ClientHistoryButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2519268F2C3CB44900249DF5 /* ClientHistoryButton.swift */; };
251926922C3CB52300249DF5 /* DeleteNodeButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 251926912C3CB52300249DF5 /* DeleteNodeButton.swift */; };
259792252C2F114500AD1659 /* ChannelEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD58C5F12919AD3C00D5BEFB /* ChannelEntityExtension.swift */; };
259792262C2F114500AD1659 /* PositionEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5394FD276BA0EF00AD86B1 /* PositionEntityExtension.swift */; };
259792272C2F114500AD1659 /* TraceRouteEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE5B4052B227E3200FCDD05 /* TraceRouteEntityExtension.swift */; };
@ -222,6 +228,12 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
251926842C3BA97800249DF5 /* FavoriteNodeButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteNodeButton.swift; sourceTree = "<group>"; };
251926862C3BAE2200249DF5 /* NodeAlertsButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeAlertsButton.swift; sourceTree = "<group>"; };
251926892C3BB1B200249DF5 /* ExchangePositionsButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExchangePositionsButton.swift; sourceTree = "<group>"; };
2519268B2C3BB52000249DF5 /* TraceRouteButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TraceRouteButton.swift; sourceTree = "<group>"; };
2519268F2C3CB44900249DF5 /* ClientHistoryButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientHistoryButton.swift; sourceTree = "<group>"; };
251926912C3CB52300249DF5 /* DeleteNodeButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteNodeButton.swift; sourceTree = "<group>"; };
25AECD4E2C2F723200862C8E /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
6D825E612C34786C008DBEE4 /* CommonRegex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommonRegex.swift; sourceTree = "<group>"; };
6DA39D8D2A92DC52007E311C /* MeshtasticAppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshtasticAppDelegate.swift; sourceTree = "<group>"; };
@ -471,6 +483,19 @@
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
251926882C3BAF2E00249DF5 /* Actions */ = {
isa = PBXGroup;
children = (
251926842C3BA97800249DF5 /* FavoriteNodeButton.swift */,
251926892C3BB1B200249DF5 /* ExchangePositionsButton.swift */,
251926862C3BAE2200249DF5 /* NodeAlertsButton.swift */,
2519268B2C3BB52000249DF5 /* TraceRouteButton.swift */,
2519268F2C3CB44900249DF5 /* ClientHistoryButton.swift */,
251926912C3CB52300249DF5 /* DeleteNodeButton.swift */,
);
path = Actions;
sourceTree = "<group>";
};
C9483F6B2773016700998F6B /* MapKitMap */ = {
isa = PBXGroup;
children = (
@ -874,6 +899,7 @@
DDDB26452AACC0B7003AFCB7 /* NodeInfoItem.swift */,
DDDB26412AABF655003AFCB7 /* NodeListItem.swift */,
DDDCD56F2BB26F5C00BE6B60 /* NodeListFilter.swift */,
251926882C3BAF2E00249DF5 /* Actions */,
);
path = Helpers;
sourceTree = "<group>";
@ -1112,6 +1138,7 @@
DD93800E2BA74D0C008BEC06 /* ChannelForm.swift in Sources */,
DD41A61529AB0035003C5A37 /* NodeWeatherForecast.swift in Sources */,
DDB6ABD628AE742000384BA1 /* BluetoothConfig.swift in Sources */,
251926902C3CB44900249DF5 /* ClientHistoryButton.swift in Sources */,
DDD5BB102C285FB3007E03CA /* AppLogFilter.swift in Sources */,
DD4640202AFF10F4002A5ECB /* WaypointForm.swift in Sources */,
DD769E0328D18BF1001A3F05 /* DeviceMetricsLog.swift in Sources */,
@ -1168,6 +1195,7 @@
DD007BB02AA5981000F5FA12 /* NodeInfoEntityExtension.swift in Sources */,
DDDB26422AABF655003AFCB7 /* NodeListItem.swift in Sources */,
DDDB444629F8A96500EE2349 /* Character.swift in Sources */,
2519268C2C3BB52000249DF5 /* TraceRouteButton.swift in Sources */,
DD23A50F26FD1B4400D9B90C /* PeripheralModel.swift in Sources */,
DDB6ABDB28B0AC6000384BA1 /* DistanceText.swift in Sources */,
DD94B7402ACCE3BE00DCD1D1 /* MapSettingsForm.swift in Sources */,
@ -1188,6 +1216,7 @@
DDB6ABE028B13AC700384BA1 /* DeviceEnums.swift in Sources */,
DD86D40C287F401000BAEB7A /* SaveChannelQRCode.swift in Sources */,
D93068DD2B81CA820066FBC8 /* ConfigHeader.swift in Sources */,
251926872C3BAE2200249DF5 /* NodeAlertsButton.swift in Sources */,
DDA1C48E28DB49D3009933EC /* ChannelRoles.swift in Sources */,
D9BC22DB2B7DE8E2006A37D5 /* TileDownloadStatus.swift in Sources */,
DDD5BB092C285DDC007E03CA /* AppLog.swift in Sources */,
@ -1202,6 +1231,7 @@
DDC3B274283F411B00AC321C /* LastHeardText.swift in Sources */,
DDDE5A1029AFE69700490C6C /* MeshActivityAttributes.swift in Sources */,
DD1925B928CDA93900720036 /* SerialConfigEnums.swift in Sources */,
251926852C3BA97800249DF5 /* FavoriteNodeButton.swift in Sources */,
D9C983A02B79D0E800BDBE6A /* AlertButton.swift in Sources */,
DD86D4112881D16900BAEB7A /* WriteCsvFile.swift in Sources */,
DDDB445029F8AC9C00EE2349 /* UIImage.swift in Sources */,
@ -1213,9 +1243,11 @@
DDB75A212A12B954006ED576 /* LoRaSignalStrength.swift in Sources */,
DD6193752862F6E600E59241 /* ExternalNotificationConfig.swift in Sources */,
DD268D8E2BCC90E2008073AE /* RouteEnums.swift in Sources */,
251926922C3CB52300249DF5 /* DeleteNodeButton.swift in Sources */,
DDB6ABE428B13FFF00384BA1 /* DisplayEnums.swift in Sources */,
DD4975A52B147BA90026544E /* AmbientLightingConfig.swift in Sources */,
D93068D92B81509C0066FBC8 /* TapbackResponses.swift in Sources */,
2519268A2C3BB1B200249DF5 /* ExchangePositionsButton.swift in Sources */,
DD86D40A287F04F100BAEB7A /* InvalidVersion.swift in Sources */,
DDD94A502845C8F5004A87A0 /* DateTimeText.swift in Sources */,
DDB6ABE228B13FB500384BA1 /* PositionConfigEnums.swift in Sources */,

View file

@ -1,5 +1,5 @@
{
"originHash" : "74b3ad6215f078d89f4436b6ce0e318f145842efa3453bbe055ab76057de7d6b",
"originHash" : "c5be9820b6e5add3da0e3bd134c3826b3eece5f926d667cb3800a26572f9e63c",
"pins" : [
{
"identity" : "cocoamqtt",

View file

@ -0,0 +1,33 @@
import SwiftUI
struct ClientHistoryButton: View {
var bleManager: BLEManager
var connectedNode: NodeInfoEntity
var node: NodeInfoEntity
@State
private var isPresentingAlert = false
var body: some View {
Button {
isPresentingAlert = bleManager.requestStoreAndForwardClientHistory(
fromUser: connectedNode.user!,
toUser: node.user!
)
} label: {
Label(
"Client History",
systemImage: "envelope.arrow.triangle.branch"
)
}.alert(
"Client History Request Sent",
isPresented: $isPresentingAlert
) {
Button("OK") { }.keyboardShortcut(.defaultAction)
} message: {
Text("Any missed messages will be delivered again.")
}
}
}

View file

@ -0,0 +1,51 @@
import CoreData
import OSLog
import SwiftUI
struct DeleteNodeButton: View {
var bleManager: BLEManager
var context: NSManagedObjectContext
var connectedNode: NodeInfoEntity
var node: NodeInfoEntity
@State
private var isPresentingAlert = false
var body: some View {
Button(role: .destructive) {
isPresentingAlert = true
} label: {
Label {
Text("Delete Node")
} icon: {
Image(systemName: "trash")
.symbolRenderingMode(.multicolor)
}
}
.confirmationDialog(
"are.you.sure",
isPresented: $isPresentingAlert,
titleVisibility: .visible
) {
Button("Delete Node", role: .destructive) {
guard let deleteNode = getNodeInfo(
id: node.num,
context: context
) else {
Logger.data.error("Unable to find node info to delete node \(node.num)")
return
}
let success = bleManager.removeNode(
node: deleteNode,
connectedNodeNum: connectedNode.num
)
if !success {
Logger.data.error("Failed to delete node \(deleteNode.user?.longName ?? "unknown".localized)")
}
}
}
}
}

View file

@ -0,0 +1,35 @@
import CoreData
import SwiftUI
struct ExchangePositionsButton: View {
var bleManager: BLEManager
var node: NodeInfoEntity
@State
private var isPresentingPositionSentAlert: Bool = false
var body: some View {
Button {
isPresentingPositionSentAlert = bleManager.sendPosition(
channel: node.channel,
destNum: node.num,
wantResponse: true
)
} label: {
Label {
Text("Exchange Positions")
} icon: {
Image(systemName: "arrow.triangle.2.circlepath")
.symbolRenderingMode(.hierarchical)
}
}.alert(
"Position Sent",
isPresented: $isPresentingPositionSentAlert
) {
Button("OK") { }.keyboardShortcut(.defaultAction)
} message: {
Text("Your position has been sent with a request for a response with their position.")
}
}
}

View file

@ -0,0 +1,45 @@
import CoreData
import OSLog
import SwiftUI
struct FavoriteNodeButton: View {
var bleManager: BLEManager
var context: NSManagedObjectContext
@ObservedObject
var node: NodeInfoEntity
var body: some View {
Button {
guard let connectedNodeNum = bleManager.connectedPeripheral?.num else { return }
let success = if node.favorite {
bleManager.removeFavoriteNode(
node: node,
connectedNodeNum: Int64(connectedNodeNum)
)
} else {
bleManager.setFavoriteNode(
node: node,
connectedNodeNum: Int64(connectedNodeNum)
)
}
if success {
node.favorite = !node.favorite
do {
try context.save()
} catch {
context.rollback()
Logger.data.error("Save Node Favorite Error")
}
Logger.data.debug("Favorited a node")
}
} label: {
Label {
Text(node.favorite ? "Remove from favorites" : "Add to favorites")
} icon: {
Image(systemName: node.favorite ? "star.fill" : "star")
.symbolRenderingMode(.multicolor)
}
}
}
}

View file

@ -0,0 +1,33 @@
import CoreData
import OSLog
import SwiftUI
struct NodeAlertsButton: View {
var context: NSManagedObjectContext
@ObservedObject
var node: NodeInfoEntity
@ObservedObject
var user: UserEntity
var body: some View {
Button {
user.mute = !user.mute
context.refresh(node, mergeChanges: true)
do {
try context.save()
} catch {
context.rollback()
Logger.data.error("Save User Mute Error")
}
} label: {
Label {
Text(user.mute ? "Show alerts" : "Hide alerts")
} icon: {
Image(systemName: user.mute ? "bell.slash" : "bell")
.symbolRenderingMode(.hierarchical)
}
}
}
}

View file

@ -0,0 +1,33 @@
import SwiftUI
struct TraceRouteButton: View {
var bleManager: BLEManager
var node: NodeInfoEntity
@State
private var isPresentingTraceRouteSentAlert: Bool = false
var body: some View {
Button {
isPresentingTraceRouteSentAlert = bleManager.sendTraceRouteRequest(
destNum: node.user?.num ?? 0,
wantResponse: true
)
} label: {
Label {
Text("Trace Route")
} icon: {
Image(systemName: "signpost.right.and.left")
.symbolRenderingMode(.hierarchical)
}
}.alert(
"Trace Route Sent",
isPresented: $isPresentingTraceRouteSentAlert
) {
Button("OK") { }.keyboardShortcut(.defaultAction)
} message: {
Text("This could take a while. The response will appear in the trace route log for the node it was sent to.")
}
}
}

View file

@ -16,9 +16,49 @@ struct NodeDetail: View {
@State private var showingShutdownConfirm: Bool = false
@State private var showingRebootConfirm: Bool = false
@ObservedObject var node: NodeInfoEntity
// The node the device is currently connected to
var connectedNode: NodeInfoEntity?
// The node information being displayed on the detail screen
@ObservedObject
var node: NodeInfoEntity
var columnVisibility = NavigationSplitViewVisibility.all
var favoriteNodeAction: some View {
let connectedNodeNum = bleManager.connectedPeripheral?.num ?? 0
return Button {
let success = if node.favorite {
bleManager.removeFavoriteNode(
node: node,
connectedNodeNum: Int64(connectedNodeNum)
)
} else {
bleManager.setFavoriteNode(
node: node,
connectedNodeNum: Int64(connectedNodeNum)
)
}
if success {
node.favorite = !node.favorite
do {
try context.save()
} catch {
context.rollback()
Logger.data.error("Save Node Favorite Error")
}
Logger.data.debug("Favorited a node")
}
} label: {
Label {
Text(node.favorite ? "Remove from favorites" : "Add to favorites")
} icon: {
Image(systemName: node.favorite ? "star.fill" : "star")
.symbolRenderingMode(.multicolor)
}
}
}
var body: some View {
NavigationStack {
List {
@ -182,6 +222,52 @@ struct NodeDetail: View {
}
}
Section("Actions") {
FavoriteNodeButton(
bleManager: bleManager,
context: context,
node: node
)
if let user = node.user {
NodeAlertsButton(
context: context,
node: node,
user: user
)
}
if let connectedPeripheral = bleManager.connectedPeripheral,
node.num != connectedPeripheral.num {
ExchangePositionsButton(
bleManager: bleManager,
node: node
)
TraceRouteButton(
bleManager: bleManager,
node: node
)
if let connectedNode {
if node.isStoreForwardRouter {
ClientHistoryButton(
bleManager: bleManager,
connectedNode: connectedNode,
node: node
)
}
DeleteNodeButton(
bleManager: bleManager,
context: context,
connectedNode: connectedNode,
node: node
)
}
}
}
if let metadata = node.metadata,
let connectedNode,
self.bleManager.connectedPeripheral != nil {
@ -207,43 +293,47 @@ struct NodeDetail: View {
}
if metadata.canShutdown {
Label("Power Off", systemImage: "power")
.onTapGesture {
showingShutdownConfirm = true
}
.confirmationDialog(
"are.you.sure",
isPresented: $showingShutdownConfirm
) {
Button("Shutdown Node?", role: .destructive) {
if !bleManager.sendShutdown(
fromUser: connectedNode.user!,
toUser: node.user!,
adminIndex: connectedNode.myInfo!.adminIndex
) {
Logger.mesh.warning("Shutdown Failed")
}
}
}
}
Label("reboot", systemImage: "arrow.triangle.2.circlepath")
.onTapGesture {
showingRebootConfirm = true
}
.confirmationDialog(
Button {
showingShutdownConfirm = true
} label: {
Label("Power Off", systemImage: "power")
}.confirmationDialog(
"are.you.sure",
isPresented: $showingRebootConfirm
isPresented: $showingShutdownConfirm
) {
Button("reboot.node", role: .destructive) {
if !bleManager.sendReboot(
Button("Shutdown Node?", role: .destructive) {
if !bleManager.sendShutdown(
fromUser: connectedNode.user!,
toUser: node.user!,
adminIndex: connectedNode.myInfo!.adminIndex
) {
Logger.mesh.warning("Reboot Failed")
Logger.mesh.warning("Shutdown Failed")
}
}
}
}
Button {
showingRebootConfirm = true
} label: {
Label(
"reboot",
systemImage: "arrow.triangle.2.circlepath"
)
}.confirmationDialog(
"are.you.sure",
isPresented: $showingRebootConfirm
) {
Button("reboot.node", role: .destructive) {
if !bleManager.sendReboot(
fromUser: connectedNode.user!,
toUser: node.user!,
adminIndex: connectedNode.myInfo!.adminIndex
) {
Logger.mesh.warning("Reboot Failed")
}
}
}
}
}
}

View file

@ -13,11 +13,6 @@ struct NodeList: View {
@StateObject var appState = AppState.shared
@State private var columnVisibility = NavigationSplitViewVisibility.all
@State private var selectedNode: NodeInfoEntity?
@State private var isPresentingTraceRouteSentAlert = false
@State private var isPresentingClientHistorySentAlert = false
@State private var isPresentingDeleteNodeAlert = false
@State private var isPresentingPositionSentAlert = false
@State private var deleteNodeId: Int64 = 0
@State private var searchText = ""
@State private var viaLora = true
@State private var viaMqtt = true
@ -37,159 +32,72 @@ struct NodeList: View {
@EnvironmentObject var bleManager: BLEManager
@FetchRequest(
sortDescriptors: [NSSortDescriptor(key: "favorite", ascending: false),
NSSortDescriptor(key: "lastHeard", ascending: false),
NSSortDescriptor(key: "user.longName", ascending: true)],
animation: .default)
sortDescriptors: [
NSSortDescriptor(key: "favorite", ascending: false),
NSSortDescriptor(key: "lastHeard", ascending: false),
NSSortDescriptor(key: "user.longName", ascending: true),
],
animation: .default
)
var nodes: FetchedResults<NodeInfoEntity>
@ViewBuilder
func contextMenuActions(
node: NodeInfoEntity,
connectedNode: NodeInfoEntity?
) -> some View {
FavoriteNodeButton(
bleManager: bleManager,
context: context,
node: node
)
if let user = node.user {
NodeAlertsButton(
context: context,
node: node,
user: user
)
}
if let connectedNode {
DeleteNodeButton(
bleManager: bleManager,
context: context,
connectedNode: connectedNode,
node: node
)
}
}
var body: some View {
NavigationSplitView(columnVisibility: $columnVisibility) {
// HStack {
// Button("Open Node") {
// UIApplication
// .shared
// .open(URL(string: "meshtastic://nodes?nodeNum=530606484")!)
// }
// }
let connectedNodeNum = Int(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral?.num ?? 0 : 0)
let connectedNodeNum = Int(bleManager.connectedPeripheral?.num ?? 0)
let connectedNode = nodes.first(where: { $0.num == connectedNodeNum })
List(nodes, id: \.self, selection: $selectedNode) { node in
NodeListItem(node: node,
connected: bleManager.connectedPeripheral != nil && bleManager.connectedPeripheral?.num ?? -1 == node.num,
connectedNode: (bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral?.num ?? -1 : -1))
NodeListItem(
node: node,
connected: bleManager.connectedPeripheral?.num ?? -1 == node.num,
connectedNode: bleManager.connectedPeripheral?.num ?? -1
)
.contextMenu {
Button {
if !node.favorite {
let success = bleManager.setFavoriteNode(node: node, connectedNodeNum: Int64(connectedNodeNum))
if success {
node.favorite = !node.favorite
do {
try context.save()
} catch {
context.rollback()
Logger.data.error("Save Node Favorite Error")
}
Logger.data.debug("Favorited a node")
}
} else {
let success = bleManager.removeFavoriteNode(node: node, connectedNodeNum: Int64(connectedNodeNum))
if success {
node.favorite = !node.favorite
do {
try context.save()
} catch {
context.rollback()
Logger.data.error("Save Node Favorite Error")
}
Logger.data.debug("Favorited a node")
}
}
} label: {
Label(node.favorite ? "Un-Favorite" : "Favorite", systemImage: node.favorite ? "star.slash.fill" : "star.fill")
}
if node.user != nil {
Button {
node.user!.mute = !node.user!.mute
context.refresh(node, mergeChanges: true)
do {
try context.save()
} catch {
context.rollback()
Logger.data.error("Save User Mute Error")
}
} label: {
Label(node.user!.mute ? "Show Alerts" : "Hide Alerts", systemImage: node.user!.mute ? "bell" : "bell.slash")
}
if bleManager.connectedPeripheral != nil && node.num != connectedNodeNum {
Button {
let positionSent = bleManager.sendPosition(
channel: node.channel,
destNum: node.num,
wantResponse: true
)
if positionSent {
isPresentingPositionSentAlert = true
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
isPresentingPositionSentAlert = false
}
}
} label: {
Label("Exchange Positions", systemImage: "arrow.triangle.2.circlepath")
}
}
if bleManager.connectedPeripheral != nil && connectedNodeNum != node.num {
Button {
let success = bleManager.sendTraceRouteRequest(destNum: node.user?.num ?? 0, wantResponse: true)
if success {
isPresentingTraceRouteSentAlert = true
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
isPresentingTraceRouteSentAlert = false
}
}
} label: {
Label("Trace Route", systemImage: "signpost.right.and.left")
}
if node.isStoreForwardRouter {
Button {
let success = bleManager.requestStoreAndForwardClientHistory(fromUser: connectedNode!.user!, toUser: node.user!)
if success {
isPresentingClientHistorySentAlert = true
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
isPresentingClientHistorySentAlert = false
}
}
} label: {
Label("Client History", systemImage: "envelope.arrow.triangle.branch")
}
}
}
if bleManager.connectedPeripheral != nil {
Button(role: .destructive) {
deleteNodeId = node.num
isPresentingDeleteNodeAlert = true
} label: {
Label("Delete Node", systemImage: "trash")
}
}
}
}
.alert(
"Position Sent",
isPresented: $isPresentingPositionSentAlert
) {
Button("OK") { }.keyboardShortcut(.defaultAction)
} message: {
Text("Your position has been sent with a request for a response with their position.")
}
.alert(
"Trace Route Sent",
isPresented: $isPresentingTraceRouteSentAlert
) {
Button("OK") { }.keyboardShortcut(.defaultAction)
} message: {
Text("This could take a while, response will appear in the trace route log for the node it was sent to.")
}
.alert(
"Client History Request Sent",
isPresented: $isPresentingClientHistorySentAlert
) {
Button("OK") { }.keyboardShortcut(.defaultAction)
} message: {
Text("Any missed messages will be delivered again.")
contextMenuActions(
node: node,
connectedNode: connectedNode
)
}
}
.sheet(isPresented: $isEditingFilters) {
NodeListFilter(viaLora: $viaLora, viaMqtt: $viaMqtt, isOnline: $isOnline, isFavorite: $isFavorite, distanceFilter: $distanceFilter, maximumDistance: $maxDistance, hopsAway: $hopsAway, roleFilter: $roleFilter, deviceRoles: $deviceRoles)
NodeListFilter(
viaLora: $viaLora,
viaMqtt: $viaMqtt,
isOnline: $isOnline,
isFavorite: $isFavorite,
distanceFilter: $distanceFilter,
maximumDistance: $maxDistance,
hopsAway: $hopsAway,
roleFilter: $roleFilter,
deviceRoles: $deviceRoles
)
}
.safeAreaInset(edge: .bottom, alignment: .trailing) {
HStack {
@ -204,67 +112,58 @@ struct NodeList: View {
.tint(Color(UIColor.secondarySystemBackground))
.foregroundColor(.accentColor)
.buttonStyle(.borderedProminent)
}
.controlSize(.regular)
.padding(5)
}
.padding(.bottom, 5)
.searchable(text: $searchText, placement: .automatic, prompt: "Find a node")
.disableAutocorrection(true)
.scrollDismissesKeyboard(.immediately)
.disableAutocorrection(true)
.scrollDismissesKeyboard(.immediately)
.navigationTitle(String.localizedStringWithFormat("nodes %@".localized, String(nodes.count)))
.listStyle(.plain)
.confirmationDialog(
"are.you.sure",
isPresented: $isPresentingDeleteNodeAlert,
titleVisibility: .visible
) {
Button("Delete Node") {
let deleteNode = getNodeInfo(id: deleteNodeId, context: context)
if connectedNode != nil {
if deleteNode != nil {
let success = bleManager.removeNode(node: deleteNode!, connectedNodeNum: Int64(connectedNodeNum))
if !success {
Logger.data.error("Failed to delete node \(deleteNode?.user?.longName ?? "unknown".localized)")
}
}
}
}
}
.navigationSplitViewColumnWidth(min: 100, ideal: 250, max: 500)
.navigationBarItems(leading:
MeshtasticLogo(),
trailing:
ZStack {
.navigationBarItems(
leading: MeshtasticLogo(),
trailing: ZStack {
ConnectedDevice(
bluetoothOn: bleManager.isSwitchedOn,
deviceConnected: bleManager.connectedPeripheral != nil,
name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?", phoneOnly: true)
})
name: bleManager.connectedPeripheral?.shortName ?? "?",
phoneOnly: true
)
}
)
} content: {
if let node = selectedNode {
NavigationStack {
NodeDetail(node: node, columnVisibility: columnVisibility)
.edgesIgnoringSafeArea([.leading, .trailing])
.navigationBarTitle(String(node.user?.longName ?? "unknown".localized), displayMode: .inline)
.navigationBarItems(
trailing:
ZStack {
if UIDevice.current.userInterfaceIdiom != .phone {
Button {
columnVisibility = .detailOnly
} label: {
Image(systemName: "rectangle")
}
NodeDetail(
connectedNode: nodes.first(where: {
let connectedNodeNum = Int(bleManager.connectedPeripheral?.num ?? 0)
return $0.num == connectedNodeNum
}),
node: node,
columnVisibility: columnVisibility
)
.edgesIgnoringSafeArea([.leading, .trailing])
.navigationBarTitle(String(node.user?.longName ?? "unknown".localized), displayMode: .inline)
.navigationBarItems(
trailing: ZStack {
if UIDevice.current.userInterfaceIdiom != .phone {
Button {
columnVisibility = .detailOnly
} label: {
Image(systemName: "rectangle")
}
ConnectedDevice(
bluetoothOn: bleManager.isSwitchedOn,
deviceConnected: bleManager.connectedPeripheral != nil,
name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?", phoneOnly: true)
})
}
ConnectedDevice(
bluetoothOn: bleManager.isSwitchedOn,
deviceConnected: bleManager.connectedPeripheral != nil,
name: bleManager.connectedPeripheral?.shortName ?? "?",
phoneOnly: true
)
}
)
}
} else {
@ -363,7 +262,7 @@ struct NodeList: View {
}
}
private func searchNodeList() async -> Void {
private func searchNodeList() async {
/// Case Insensitive Search Text Predicates
let searchPredicates = ["user.userId", "user.numString", "user.hwModel", "user.longName", "user.shortName"].map { property in
return NSPredicate(format: "%K CONTAINS[c] %@", property, searchText)