mirror of
https://github.com/meshtastic/Meshtastic-Apple.git
synced 2026-04-20 22:13:56 +00:00
Factor out client history & delete node buttons, and tidy up the available actions in the list & detail screen
This commit is contained in:
parent
7a7a225b8e
commit
4c448e08b5
5 changed files with 204 additions and 121 deletions
|
|
@ -11,6 +11,8 @@
|
|||
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 */; };
|
||||
|
|
@ -230,6 +232,8 @@
|
|||
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>"; };
|
||||
|
|
@ -486,6 +490,8 @@
|
|||
251926892C3BB1B200249DF5 /* ExchangePositionsButton.swift */,
|
||||
251926862C3BAE2200249DF5 /* NodeAlertsButton.swift */,
|
||||
2519268B2C3BB52000249DF5 /* TraceRouteButton.swift */,
|
||||
2519268F2C3CB44900249DF5 /* ClientHistoryButton.swift */,
|
||||
251926912C3CB52300249DF5 /* DeleteNodeButton.swift */,
|
||||
);
|
||||
path = Actions;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -1132,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 */,
|
||||
|
|
@ -1236,6 +1243,7 @@
|
|||
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 */,
|
||||
|
|
|
|||
|
|
@ -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.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -16,7 +16,13 @@ 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 {
|
||||
|
|
@ -242,6 +248,23 @@ struct NodeDetail: View {
|
|||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,9 +13,6 @@ struct NodeList: View {
|
|||
@StateObject var appState = AppState.shared
|
||||
@State private var columnVisibility = NavigationSplitViewVisibility.all
|
||||
@State private var selectedNode: NodeInfoEntity?
|
||||
@State private var isPresentingClientHistorySentAlert = false
|
||||
@State private var isPresentingDeleteNodeAlert = false
|
||||
@State private var deleteNodeId: Int64 = 0
|
||||
@State private var searchText = ""
|
||||
@State private var viaLora = true
|
||||
@State private var viaMqtt = true
|
||||
|
|
@ -35,92 +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?.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)
|
||||
connected: bleManager.connectedPeripheral?.num ?? -1 == node.num,
|
||||
connectedNode: bleManager.connectedPeripheral?.num ?? -1
|
||||
)
|
||||
.contextMenu {
|
||||
FavoriteNodeButton(
|
||||
bleManager: bleManager,
|
||||
context: context,
|
||||
node: node
|
||||
contextMenuActions(
|
||||
node: node,
|
||||
connectedNode: connectedNode
|
||||
)
|
||||
|
||||
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 node.isStoreForwardRouter {
|
||||
Button {
|
||||
let success = bleManager.requestStoreAndForwardClientHistory(fromUser: connectedNode!.user!, toUser: node.user!)
|
||||
if success {
|
||||
isPresentingClientHistorySentAlert = true
|
||||
}
|
||||
} 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(
|
||||
"Client History Request Sent",
|
||||
isPresented: $isPresentingClientHistorySentAlert
|
||||
) {
|
||||
Button("OK") { }.keyboardShortcut(.defaultAction)
|
||||
} message: {
|
||||
Text("Any missed messages will be delivered again.")
|
||||
}
|
||||
}
|
||||
.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 {
|
||||
|
|
@ -135,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 {
|
||||
|
|
@ -294,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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue