mirror of
https://github.com/meshtastic/Meshtastic-Apple.git
synced 2026-04-20 22:13:56 +00:00
Checkpoint 2 CoreData upgrade
This commit is contained in:
parent
fd5b9eb1c3
commit
6a3e661345
15 changed files with 416 additions and 597 deletions
|
|
@ -9,11 +9,9 @@
|
|||
/* Begin PBXBuildFile section */
|
||||
DD23A50F26FD1B4400D9B90C /* PeripheralModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD23A50E26FD1B4400D9B90C /* PeripheralModel.swift */; };
|
||||
DD23A51326FEF5D500D9B90C /* MessageData.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD23A51226FEF5D500D9B90C /* MessageData.swift */; };
|
||||
DD2E652427679E4000E45FC5 /* NodeInfoEntityRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2E652327679E4000E45FC5 /* NodeInfoEntityRow.swift */; };
|
||||
DD2E65262767A01F00E45FC5 /* NodeInfoEntityDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2E65252767A01F00E45FC5 /* NodeInfoEntityDetail.swift */; };
|
||||
DD47E3CC26F0E51D00029299 /* NodeDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD47E3CB26F0E51D00029299 /* NodeDetail.swift */; };
|
||||
DD2E652427679E4000E45FC5 /* NodeRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2E652327679E4000E45FC5 /* NodeRow.swift */; };
|
||||
DD2E65262767A01F00E45FC5 /* NodeDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2E65252767A01F00E45FC5 /* NodeDetail.swift */; };
|
||||
DD47E3CE26F103C600029299 /* NodeList.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD47E3CD26F103C600029299 /* NodeList.swift */; };
|
||||
DD47E3D026F1073F00029299 /* NodeRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD47E3CF26F1073F00029299 /* NodeRow.swift */; };
|
||||
DD47E3D626F17ED900029299 /* CircleText.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD47E3D526F17ED900029299 /* CircleText.swift */; };
|
||||
DD47E3D926F3093800029299 /* MessageBubble.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD47E3D826F3093800029299 /* MessageBubble.swift */; };
|
||||
DD47E3DB26F3901B00029299 /* Channels.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD47E3DA26F3901A00029299 /* Channels.swift */; };
|
||||
|
|
@ -22,11 +20,11 @@
|
|||
DD4A911E2708C65400501B7E /* AppSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4A911D2708C65400501B7E /* AppSettings.swift */; };
|
||||
DD4A91202708C66600501B7E /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4A911F2708C66600501B7E /* Configuration.swift */; };
|
||||
DD5394FC276993AD00AD86B1 /* SwiftProtobuf in Frameworks */ = {isa = PBXBuildFile; productRef = DD5394FB276993AD00AD86B1 /* SwiftProtobuf */; };
|
||||
DD5394FE276BA0EF00AD86B1 /* PositionEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5394FD276BA0EF00AD86B1 /* PositionEntityExtension.swift */; };
|
||||
DD8169F9271F1A6100F4AB02 /* MeshLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD8169F8271F1A6100F4AB02 /* MeshLogger.swift */; };
|
||||
DD8169FB271F1F3A00F4AB02 /* MeshLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD8169FA271F1F3A00F4AB02 /* MeshLog.swift */; };
|
||||
DD8169FF272476C700F4AB02 /* LogDocument.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD8169FE272476C700F4AB02 /* LogDocument.swift */; };
|
||||
DD836AE726F6B38600ABCC23 /* Connect.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD836AE626F6B38600ABCC23 /* Connect.swift */; };
|
||||
DD836AED26F858F900ABCC23 /* MeshData.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD836AEC26F858F900ABCC23 /* MeshData.swift */; };
|
||||
DD836AEF26F85D8D00ABCC23 /* NodeInfoModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD836AEE26F85D8D00ABCC23 /* NodeInfoModel.swift */; };
|
||||
DD90860C26F684AF00DC5189 /* BatteryIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD90860B26F684AF00DC5189 /* BatteryIcon.swift */; };
|
||||
DD90860E26F69BAE00DC5189 /* NodeMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD90860D26F69BAE00DC5189 /* NodeMap.swift */; };
|
||||
|
|
@ -75,11 +73,9 @@
|
|||
/* Begin PBXFileReference section */
|
||||
DD23A50E26FD1B4400D9B90C /* PeripheralModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeripheralModel.swift; sourceTree = "<group>"; };
|
||||
DD23A51226FEF5D500D9B90C /* MessageData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageData.swift; sourceTree = "<group>"; };
|
||||
DD2E652327679E4000E45FC5 /* NodeInfoEntityRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeInfoEntityRow.swift; sourceTree = "<group>"; };
|
||||
DD2E65252767A01F00E45FC5 /* NodeInfoEntityDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeInfoEntityDetail.swift; sourceTree = "<group>"; };
|
||||
DD47E3CB26F0E51D00029299 /* NodeDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeDetail.swift; sourceTree = "<group>"; };
|
||||
DD2E652327679E4000E45FC5 /* NodeRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeRow.swift; sourceTree = "<group>"; };
|
||||
DD2E65252767A01F00E45FC5 /* NodeDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeDetail.swift; sourceTree = "<group>"; };
|
||||
DD47E3CD26F103C600029299 /* NodeList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeList.swift; sourceTree = "<group>"; };
|
||||
DD47E3CF26F1073F00029299 /* NodeRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeRow.swift; sourceTree = "<group>"; };
|
||||
DD47E3D526F17ED900029299 /* CircleText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircleText.swift; sourceTree = "<group>"; };
|
||||
DD47E3D826F3093800029299 /* MessageBubble.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageBubble.swift; sourceTree = "<group>"; };
|
||||
DD47E3DA26F3901A00029299 /* Channels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Channels.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -87,11 +83,11 @@
|
|||
DD47E3DE26F39D9F00029299 /* MyInfoModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyInfoModel.swift; sourceTree = "<group>"; };
|
||||
DD4A911D2708C65400501B7E /* AppSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettings.swift; sourceTree = "<group>"; };
|
||||
DD4A911F2708C66600501B7E /* Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = "<group>"; };
|
||||
DD5394FD276BA0EF00AD86B1 /* PositionEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PositionEntityExtension.swift; sourceTree = "<group>"; };
|
||||
DD8169F8271F1A6100F4AB02 /* MeshLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshLogger.swift; sourceTree = "<group>"; };
|
||||
DD8169FA271F1F3A00F4AB02 /* MeshLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshLog.swift; sourceTree = "<group>"; };
|
||||
DD8169FE272476C700F4AB02 /* LogDocument.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogDocument.swift; sourceTree = "<group>"; };
|
||||
DD836AE626F6B38600ABCC23 /* Connect.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Connect.swift; sourceTree = "<group>"; };
|
||||
DD836AEC26F858F900ABCC23 /* MeshData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshData.swift; sourceTree = "<group>"; };
|
||||
DD836AEE26F85D8D00ABCC23 /* NodeInfoModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeInfoModel.swift; sourceTree = "<group>"; };
|
||||
DD90860A26F645B700DC5189 /* MeshtasticClient.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = MeshtasticClient.entitlements; sourceTree = "<group>"; };
|
||||
DD90860B26F684AF00DC5189 /* BatteryIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatteryIcon.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -156,12 +152,10 @@
|
|||
DD47E3CA26F0E50300029299 /* Nodes */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DD47E3CB26F0E51D00029299 /* NodeDetail.swift */,
|
||||
DD47E3CD26F103C600029299 /* NodeList.swift */,
|
||||
DD47E3CF26F1073F00029299 /* NodeRow.swift */,
|
||||
DD90860D26F69BAE00DC5189 /* NodeMap.swift */,
|
||||
DD2E652327679E4000E45FC5 /* NodeInfoEntityRow.swift */,
|
||||
DD2E65252767A01F00E45FC5 /* NodeInfoEntityDetail.swift */,
|
||||
DD2E652327679E4000E45FC5 /* NodeRow.swift */,
|
||||
DD2E65252767A01F00E45FC5 /* NodeDetail.swift */,
|
||||
);
|
||||
path = Nodes;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -292,7 +286,6 @@
|
|||
children = (
|
||||
DD47E3DE26F39D9F00029299 /* MyInfoModel.swift */,
|
||||
DDF924C526FA2375009FE055 /* MessageModel.swift */,
|
||||
DD836AEC26F858F900ABCC23 /* MeshData.swift */,
|
||||
DD836AEE26F85D8D00ABCC23 /* NodeInfoModel.swift */,
|
||||
DD23A50E26FD1B4400D9B90C /* PeripheralModel.swift */,
|
||||
DD23A51226FEF5D500D9B90C /* MessageData.swift */,
|
||||
|
|
@ -344,6 +337,7 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
DDC4D567275499A500A4208E /* Persistence.swift */,
|
||||
DD5394FD276BA0EF00AD86B1 /* PositionEntityExtension.swift */,
|
||||
);
|
||||
path = Persistence;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -506,20 +500,19 @@
|
|||
files = (
|
||||
DD836AE726F6B38600ABCC23 /* Connect.swift in Sources */,
|
||||
DDAF8C6E26ED19040058C060 /* Extensions.swift in Sources */,
|
||||
DD47E3CC26F0E51D00029299 /* NodeDetail.swift in Sources */,
|
||||
DD836AEF26F85D8D00ABCC23 /* NodeInfoModel.swift in Sources */,
|
||||
DDC2E1A726CEB3400042C5E4 /* LocationHelper.swift in Sources */,
|
||||
DDAF8C5F26ED09B50058C060 /* radioconfig.pb.swift in Sources */,
|
||||
DD5394FE276BA0EF00AD86B1 /* PositionEntityExtension.swift in Sources */,
|
||||
DD913639270DFF4C00D7ACF3 /* LocalNotificationManager.swift in Sources */,
|
||||
DDAF8C5326EB1DF10058C060 /* BLEManager.swift in Sources */,
|
||||
DDC4D568275499A500A4208E /* Persistence.swift in Sources */,
|
||||
DD2E652427679E4000E45FC5 /* NodeInfoEntityRow.swift in Sources */,
|
||||
DD2E652427679E4000E45FC5 /* NodeRow.swift in Sources */,
|
||||
DD90860E26F69BAE00DC5189 /* NodeMap.swift in Sources */,
|
||||
DD47E3DB26F3901B00029299 /* Channels.swift in Sources */,
|
||||
DD8169FF272476C700F4AB02 /* LogDocument.swift in Sources */,
|
||||
DDAF8C6926ED0D070058C060 /* deviceonly.pb.swift in Sources */,
|
||||
DD23A51326FEF5D500D9B90C /* MessageData.swift in Sources */,
|
||||
DD836AED26F858F900ABCC23 /* MeshData.swift in Sources */,
|
||||
DDAF8C6B26ED0DD80058C060 /* environmental_measurement.pb.swift in Sources */,
|
||||
DD90860C26F684AF00DC5189 /* BatteryIcon.swift in Sources */,
|
||||
DD4A91202708C66600501B7E /* Configuration.swift in Sources */,
|
||||
|
|
@ -531,14 +524,13 @@
|
|||
DD47E3DF26F39D9F00029299 /* MyInfoModel.swift in Sources */,
|
||||
DD23A50F26FD1B4400D9B90C /* PeripheralModel.swift in Sources */,
|
||||
DD47E3CE26F103C600029299 /* NodeList.swift in Sources */,
|
||||
DD47E3D026F1073F00029299 /* NodeRow.swift in Sources */,
|
||||
DD47E3D626F17ED900029299 /* CircleText.swift in Sources */,
|
||||
DDF924C626FA2375009FE055 /* MessageModel.swift in Sources */,
|
||||
DDC2E18F26CE25FE0042C5E4 /* ContentView.swift in Sources */,
|
||||
DDAF8C6326ED0A230058C060 /* admin.pb.swift in Sources */,
|
||||
DDAF8C5826ED07FD0058C060 /* mesh.pb.swift in Sources */,
|
||||
DD8169FB271F1F3A00F4AB02 /* MeshLog.swift in Sources */,
|
||||
DD2E65262767A01F00E45FC5 /* NodeInfoEntityDetail.swift in Sources */,
|
||||
DD2E65262767A01F00E45FC5 /* NodeDetail.swift in Sources */,
|
||||
DD47E3D926F3093800029299 /* MessageBubble.swift in Sources */,
|
||||
DD8169F9271F1A6100F4AB02 /* MeshLogger.swift in Sources */,
|
||||
DDAF8C6726ED0C8C0058C060 /* remote_hardware.pb.swift in Sources */,
|
||||
|
|
|
|||
|
|
@ -72,8 +72,8 @@
|
|||
filePath = "MeshtasticClient/Helpers/BLEManager.swift"
|
||||
startingColumnNumber = "9223372036854775807"
|
||||
endingColumnNumber = "9223372036854775807"
|
||||
startingLineNumber = "839"
|
||||
endingLineNumber = "839"
|
||||
startingLineNumber = "824"
|
||||
endingLineNumber = "824"
|
||||
landmarkName = "sendMessage(message:)"
|
||||
landmarkType = "7">
|
||||
</BreakpointContent>
|
||||
|
|
@ -88,8 +88,8 @@
|
|||
filePath = "MeshtasticClient/Helpers/BLEManager.swift"
|
||||
startingColumnNumber = "9223372036854775807"
|
||||
endingColumnNumber = "9223372036854775807"
|
||||
startingLineNumber = "862"
|
||||
endingLineNumber = "862"
|
||||
startingLineNumber = "847"
|
||||
endingLineNumber = "847"
|
||||
landmarkName = "sendMessage(message:)"
|
||||
landmarkType = "7">
|
||||
</BreakpointContent>
|
||||
|
|
@ -113,33 +113,17 @@
|
|||
<BreakpointProxy
|
||||
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
|
||||
<BreakpointContent
|
||||
uuid = "03628B8D-EBF0-453E-884C-1161447647CD"
|
||||
shouldBeEnabled = "No"
|
||||
ignoreCount = "0"
|
||||
continueAfterRunningActions = "No"
|
||||
filePath = "MeshtasticClient/Views/Messages/Messages.swift"
|
||||
startingColumnNumber = "9223372036854775807"
|
||||
endingColumnNumber = "9223372036854775807"
|
||||
startingLineNumber = "54"
|
||||
endingLineNumber = "54"
|
||||
landmarkName = "body"
|
||||
landmarkType = "24">
|
||||
</BreakpointContent>
|
||||
</BreakpointProxy>
|
||||
<BreakpointProxy
|
||||
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
|
||||
<BreakpointContent
|
||||
uuid = "2F7DD194-C7B0-472F-8346-9128AE44D6AD"
|
||||
uuid = "64E92D44-3827-42C3-AFC0-3EE687F00068"
|
||||
shouldBeEnabled = "Yes"
|
||||
ignoreCount = "0"
|
||||
continueAfterRunningActions = "No"
|
||||
filePath = "MeshtasticClient/Views/Messages/Messages.swift"
|
||||
filePath = "MeshtasticClient/Helpers/BLEManager.swift"
|
||||
startingColumnNumber = "9223372036854775807"
|
||||
endingColumnNumber = "9223372036854775807"
|
||||
startingLineNumber = "56"
|
||||
endingLineNumber = "56"
|
||||
landmarkName = "body"
|
||||
landmarkType = "24">
|
||||
startingLineNumber = "447"
|
||||
endingLineNumber = "447"
|
||||
landmarkName = "peripheral(_:didUpdateValueFor:error:)"
|
||||
landmarkType = "7">
|
||||
</BreakpointContent>
|
||||
</BreakpointProxy>
|
||||
</Breakpoints>
|
||||
|
|
|
|||
|
|
@ -109,7 +109,7 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph
|
|||
|
||||
self.timeoutTimerCount += 1
|
||||
|
||||
if timeoutTimerCount == 6 {
|
||||
if timeoutTimerCount == 10 {
|
||||
|
||||
if connectedPeripheral != nil {
|
||||
|
||||
|
|
@ -167,16 +167,16 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph
|
|||
peripheralName = name
|
||||
}
|
||||
|
||||
let newPeripheral = Peripheral(id: peripheral.identifier.uuidString, name: peripheralName, firmwareVersion: "Unknown", rssi: RSSI.intValue, subscribed: false, peripheral: peripheral, myInfo: nil)
|
||||
let newPeripheral = Peripheral(id: peripheral.identifier.uuidString, name: peripheralName, shortName: "", longName: "", firmwareVersion: "Unknown", rssi: RSSI.intValue, subscribed: false, peripheral: peripheral)
|
||||
let peripheralIndex = peripherals.firstIndex(where: { $0.id == newPeripheral.id })
|
||||
|
||||
if peripheralIndex != nil && newPeripheral.peripheral.state != CBPeripheralState.connected {
|
||||
|
||||
//newPeripheral.myInfo = peripherals.first(where: { $0.id == newPeripheral.id })?.myInfo
|
||||
peripherals[peripheralIndex!] = newPeripheral
|
||||
peripherals.remove(at: peripheralIndex!)
|
||||
peripherals.append(newPeripheral)
|
||||
print("Updating peripheral: \(peripheralName)")
|
||||
|
||||
} else {
|
||||
|
||||
if newPeripheral.peripheral.state != CBPeripheralState.connected {
|
||||
|
|
@ -190,7 +190,6 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph
|
|||
// Called when a peripheral is connected
|
||||
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
|
||||
|
||||
// guard let connectedPeripheral = connectedPeripheral else { return }
|
||||
self.isConnected = true
|
||||
|
||||
// Invalidate and reset connection timer count, remove any connection errors
|
||||
|
|
@ -202,15 +201,16 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph
|
|||
connectedPeripheral = peripherals.filter({ $0.peripheral.identifier == peripheral.identifier }).first
|
||||
connectedPeripheral.peripheral.delegate = self
|
||||
|
||||
let fetchNodeRequest:NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "NodeInfoEntity")
|
||||
fetchNodeRequest.predicate = NSPredicate(format: "bleName MATCHES %@", String(peripheral.name ?? "???"))
|
||||
let fetchConnectedPeripheralRequest:NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "NodeInfoEntity")
|
||||
fetchConnectedPeripheralRequest.predicate = NSPredicate(format: "bleName MATCHES %@", String(peripheral.name ?? "???"))
|
||||
|
||||
do {
|
||||
let fetchedNode = try context?.fetch(fetchNodeRequest) as! [NodeInfoEntity]
|
||||
let fetchedNode = try context?.fetch(fetchConnectedPeripheralRequest) as! [NodeInfoEntity]
|
||||
|
||||
if fetchedNode.count == 1 {
|
||||
|
||||
connectedPeripheral.name = fetchedNode[0].user!.longName!
|
||||
connectedPeripheral.shortName = fetchedNode[0].user!.shortName!
|
||||
connectedPeripheral.longName = fetchedNode[0].user!.longName!
|
||||
connectedPeripheral.firmwareVersion = (fetchedNode[0].myInfo?.firmwareVersion ?? "Unknown")
|
||||
}
|
||||
|
||||
|
|
@ -395,7 +395,7 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph
|
|||
print("Save a CoreData MyInfoEntity")
|
||||
|
||||
let fetchMyInfoRequest:NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "MyInfoEntity")
|
||||
fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %i", Int64(decodedInfo.myInfo.myNodeNum))
|
||||
fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(decodedInfo.myInfo.myNodeNum))
|
||||
|
||||
do {
|
||||
let fetchedMyInfo = try context?.fetch(fetchMyInfoRequest) as! [MyInfoEntity]
|
||||
|
|
@ -493,7 +493,7 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph
|
|||
|
||||
// Look for a MyInfo
|
||||
let fetchMyInfoRequest:NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "MyInfoEntity")
|
||||
fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %i", Int64(decodedInfo.nodeInfo.num))
|
||||
fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(decodedInfo.nodeInfo.num))
|
||||
|
||||
do {
|
||||
|
||||
|
|
@ -661,15 +661,12 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph
|
|||
}
|
||||
} else if decodedInfo.packet.decoded.portnum == PortNum.nodeinfoApp {
|
||||
|
||||
//let fetchNodeInfoAppRequest:NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "NodeInfoEntity")
|
||||
//fetchNodeInfoAppRequest.predicate = NSPredicate(format: "num == %i", Int64(decodedInfo.packet.from))
|
||||
|
||||
let fetchNodeRequest:NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "NodeInfoEntity")
|
||||
fetchNodeRequest.predicate = NSPredicate(format: "num == %lld", Int64(decodedInfo.packet.from))
|
||||
let fetchNodeInfoAppRequest:NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "NodeInfoEntity")
|
||||
fetchNodeInfoAppRequest.predicate = NSPredicate(format: "num == %lld", Int64(decodedInfo.packet.from))
|
||||
|
||||
do {
|
||||
|
||||
let fetchedNode = try context?.fetch(fetchNodeRequest) as! [NodeInfoEntity]
|
||||
let fetchedNode = try context?.fetch(fetchNodeInfoAppRequest) as! [NodeInfoEntity]
|
||||
|
||||
if fetchedNode.count == 1 {
|
||||
fetchedNode[0].id = Int64(decodedInfo.nodeInfo.num)
|
||||
|
|
@ -686,7 +683,7 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph
|
|||
try context!.save()
|
||||
|
||||
if meshLoggingEnabled {
|
||||
MeshLogger.log("MESH PACKET Updated NodeInfo SNR and Time from Node Info App Packet For: \(fetchedNode[0].num)")
|
||||
MeshLogger.log("MESH PACKET Updated NodeInfo SNR and Time from Node Info App Packet For: \(Int64(decodedInfo.nodeInfo.num))")
|
||||
}
|
||||
print("Updated NodeInfo SNR and Time from Packet For: \(fetchedNode[0].num)")
|
||||
|
||||
|
|
@ -696,17 +693,15 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph
|
|||
|
||||
let nsError = error as NSError
|
||||
print("Error Saving NodeInfoEntity from NODEINFO_APP \(nsError)")
|
||||
|
||||
}
|
||||
} catch {
|
||||
|
||||
print("Error Fetching NodeInfoEntity for NODEINFO_APP")
|
||||
}
|
||||
|
||||
|
||||
|
||||
print(decodedInfo.packet.decoded.payload)
|
||||
|
||||
|
||||
|
||||
} else if decodedInfo.packet.decoded.portnum == PortNum.positionApp {
|
||||
|
||||
let fetchNodePositionRequest:NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "NodeInfoEntity")
|
||||
|
|
@ -746,16 +741,6 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph
|
|||
|
||||
print("Error Fetching NodeInfoEntity for NODEINFO_APP")
|
||||
}
|
||||
|
||||
|
||||
print(decodedInfo.packet.decoded.payload)
|
||||
// if meshLoggingEnabled {
|
||||
// MeshLogger.log("MESH PACKET Updated NodeInfo SNR and Time from Position App Packet For: \(updatedNode.num)")
|
||||
// }
|
||||
// print("Updated NodeInfo SNR and Time from Packet For: \(updatedNode.num)")
|
||||
//
|
||||
// print("Postion Payload")
|
||||
// print(try decodedInfo.packet.jsonString())
|
||||
|
||||
} else if decodedInfo.packet.decoded.portnum == PortNum.adminApp {
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import Foundation
|
||||
|
||||
class MeshLogger {
|
||||
|
||||
|
||||
static var logFile: URL? {
|
||||
guard let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return nil }
|
||||
let fileName = "mesh.log"
|
||||
|
|
|
|||
|
|
@ -1,60 +0,0 @@
|
|||
import Foundation
|
||||
|
||||
class MeshData: ObservableObject {
|
||||
private static var documentsFolder: URL {
|
||||
do {
|
||||
return try FileManager.default.url(
|
||||
for: .documentDirectory,
|
||||
in: .userDomainMask,
|
||||
appropriateFor: nil,
|
||||
create: true)
|
||||
} catch {
|
||||
fatalError("Can't find documents directory.")
|
||||
}
|
||||
}
|
||||
|
||||
private static var fileURL: URL {
|
||||
return documentsFolder.appendingPathComponent("nodeinfo.data")
|
||||
}
|
||||
|
||||
@Published var nodes: [NodeInfoModel] = []
|
||||
|
||||
func load() {
|
||||
DispatchQueue.global(qos: .background).async { [weak self] in
|
||||
guard let data = try? Data(contentsOf: Self.fileURL) else {
|
||||
#if DEBUG
|
||||
DispatchQueue.main.async {
|
||||
self?.nodes = NodeInfoModel.data
|
||||
}
|
||||
#endif
|
||||
return
|
||||
}
|
||||
guard let nodeList = try? JSONDecoder().decode([NodeInfoModel].self, from: data) else {
|
||||
do {
|
||||
// If the file is borked delete it so we stop crashing
|
||||
try FileManager.default.removeItem(at: Self.fileURL)
|
||||
} catch {
|
||||
|
||||
fatalError("Can't delete saved node data.")
|
||||
}
|
||||
|
||||
fatalError("Can't decode saved node data.")
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
self?.nodes = nodeList
|
||||
}
|
||||
}
|
||||
}
|
||||
func save() {
|
||||
DispatchQueue.global(qos: .background).async { [weak self] in
|
||||
guard let scrums = self?.nodes else { fatalError("Self out of scope") }
|
||||
guard let data = try? JSONEncoder().encode(scrums) else { fatalError("Error encoding data") }
|
||||
do {
|
||||
let outfile = Self.fileURL
|
||||
try data.write(to: outfile)
|
||||
} catch {
|
||||
fatalError("Can't write to file")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,20 +4,24 @@ import CoreBluetooth
|
|||
struct Peripheral: Identifiable {
|
||||
var id: String
|
||||
var name: String
|
||||
var shortName: String
|
||||
var longName: String
|
||||
var firmwareVersion: String
|
||||
var rssi: Int
|
||||
var subscribed: Bool
|
||||
var peripheral: CBPeripheral
|
||||
|
||||
var myInfo: MyInfoModel?
|
||||
//var myInfo: MyInfoModel?
|
||||
|
||||
init(id: String, name: String, firmwareVersion: String, rssi: Int, subscribed: Bool, peripheral: CBPeripheral, myInfo: MyInfoModel?) {
|
||||
init(id: String, name: String, shortName: String, longName: String, firmwareVersion: String, rssi: Int, subscribed: Bool, peripheral: CBPeripheral) {//, myInfo: MyInfoModel?) {
|
||||
self.id = id
|
||||
self.name = name
|
||||
self.shortName = shortName
|
||||
self.longName = longName
|
||||
self.firmwareVersion = firmwareVersion
|
||||
self.rssi = rssi
|
||||
self.subscribed = subscribed
|
||||
self.peripheral = peripheral
|
||||
self.myInfo = myInfo
|
||||
//self.myInfo = myInfo
|
||||
}
|
||||
}
|
||||
|
|
|
|||
45
MeshtasticClient/Persistence/PositionEntityExtension.swift
Normal file
45
MeshtasticClient/Persistence/PositionEntityExtension.swift
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import CoreData
|
||||
import CoreLocation
|
||||
import MapKit
|
||||
import SwiftUI
|
||||
|
||||
extension PositionEntity {
|
||||
|
||||
var latitude: Double? {
|
||||
|
||||
let d = Double(latitudeI)
|
||||
if d == 0 {
|
||||
return nil
|
||||
}
|
||||
return d / 1e7
|
||||
}
|
||||
|
||||
var longitude: Double? {
|
||||
|
||||
let d = Double(longitudeI)
|
||||
if d == 0 {
|
||||
return nil
|
||||
}
|
||||
return d / 1e7
|
||||
}
|
||||
|
||||
var coordinate: CLLocationCoordinate2D? {
|
||||
if latitude != nil && longitude != nil {
|
||||
let coord = CLLocationCoordinate2D(latitude: latitude!, longitude: longitude!)
|
||||
|
||||
return coord
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var annotaton: MKPointAnnotation {
|
||||
let pointAnn = MKPointAnnotation()
|
||||
if coordinate != nil {
|
||||
|
||||
pointAnn.coordinate = coordinate!
|
||||
}
|
||||
return pointAnn
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -52,17 +52,17 @@ struct Connect: View {
|
|||
|
||||
if bleManager.connectedPeripheral != nil {
|
||||
|
||||
Text(bleManager.connectedPeripheral.name).font(.title2)
|
||||
Text(bleManager.connectedPeripheral.longName).font(.title2)
|
||||
|
||||
} else {
|
||||
|
||||
Text(String(bleManager.connectedPeripheral.peripheral.name ?? "Unknown")).font(.title2)
|
||||
}
|
||||
Text("BLE Name: ").font(.caption)+Text(bleManager.connectedPeripheral.name)
|
||||
.font(.caption).foregroundColor(Color.gray)
|
||||
if bleManager.connectedPeripheral != nil {
|
||||
//Text("Model: ").font(.caption)+Text(bleManager.connectedNode?.user!.hwModel ?? "(null)").font(.caption).foregroundColor(Color.gray)
|
||||
}
|
||||
Text("BLE Name: ").font(.caption)+Text(bleManager.connectedPeripheral.name).font(.caption).foregroundColor(Color.gray)
|
||||
if bleManager.connectedPeripheral != nil {
|
||||
//Text("FW Version: ").font(.caption)+Text(bleManager.connectedPeripheral.myInfo?.firmwareVersion ?? "(null)").font(.caption).foregroundColor(Color.gray)
|
||||
Text("FW Version: ").font(.caption)+Text(bleManager.connectedPeripheral.firmwareVersion)
|
||||
.font(.caption).foregroundColor(Color.gray)
|
||||
}
|
||||
if bleManager.connectedPeripheral.subscribed {
|
||||
Text("Properly Subscribed").font(.caption)
|
||||
|
|
@ -200,8 +200,12 @@ struct Connect: View {
|
|||
.navigationBarItems(trailing:
|
||||
|
||||
ZStack {
|
||||
|
||||
//ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedNode != nil) ? bleManager.connectedNode.user.shortName : ((bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.name : "Unknown") )
|
||||
|
||||
ConnectedDevice(
|
||||
bluetoothOn: bleManager.isSwitchedOn,
|
||||
deviceConnected: bleManager.connectedPeripheral != nil,
|
||||
name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName :
|
||||
"???")
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ struct Messages: View {
|
|||
print("I want to delete message: \(message.messageId)")
|
||||
self.showDeleteMessageAlert = true
|
||||
self.deleteMessageId = message.messageId
|
||||
|
||||
print(deleteMessageId)
|
||||
})
|
||||
|
||||
|
|
@ -89,13 +90,19 @@ struct Messages: View {
|
|||
print("OK button tapped")
|
||||
if deleteMessageId > 0 {
|
||||
|
||||
//let message = messages.first.where: { $0.messageId == deleteMessageId })
|
||||
//context.delete(object: message)
|
||||
//bleManager.messageData.messages.remove(at: messageIndex!)
|
||||
//bleManager.messageData.save()
|
||||
//print("Deleted message: \(message.messageId)")
|
||||
//showDeleteMessageAlert = false
|
||||
deleteMessageId = 0
|
||||
let message = messages.first(where: { $0.messageId == deleteMessageId })
|
||||
|
||||
context.delete(message!)
|
||||
do {
|
||||
|
||||
try context.save()
|
||||
deleteMessageId = 0
|
||||
messageCount = messages.count
|
||||
|
||||
} catch {
|
||||
print("Failed to delete message \(deleteMessageId)")
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
secondaryButton: .cancel()
|
||||
|
|
@ -194,12 +201,16 @@ struct Messages: View {
|
|||
}
|
||||
}
|
||||
.navigationTitle("Channel - Primary")
|
||||
//.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationBarItems(trailing:
|
||||
|
||||
ZStack {
|
||||
//ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedNode != nil) ? bleManager.connectedNode.user.shortName : ((bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.name : "Unknown") )
|
||||
|
||||
ConnectedDevice(
|
||||
bluetoothOn: bleManager.isSwitchedOn,
|
||||
deviceConnected: bleManager.connectedPeripheral != nil,
|
||||
name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName :
|
||||
"???")
|
||||
})
|
||||
.onAppear(perform: {
|
||||
|
||||
|
|
|
|||
|
|
@ -1,176 +1,187 @@
|
|||
///*
|
||||
//Abstract:
|
||||
//A view showing the details for a node.
|
||||
//*/
|
||||
/*
|
||||
Abstract:
|
||||
A view showing the details for a node.
|
||||
*/
|
||||
|
||||
import SwiftUI
|
||||
import MapKit
|
||||
import CoreLocation
|
||||
|
||||
struct NodeDetail: View {
|
||||
|
||||
// CoreData
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@EnvironmentObject var bleManager: BLEManager
|
||||
|
||||
var node: NodeInfoEntity
|
||||
|
||||
struct MapLocation: Identifiable {
|
||||
let id = UUID()
|
||||
let name: String
|
||||
let coordinate: CLLocationCoordinate2D
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
|
||||
GeometryReader { bounds in
|
||||
|
||||
VStack {
|
||||
|
||||
if node.positions != nil && node.positions!.count > 0 {
|
||||
|
||||
// let nodeCoordinatePosition = CLLocationCoordinate2D(latitude: node.position.latitude!, longitude: node.position.longitude!)
|
||||
//
|
||||
//import SwiftUI
|
||||
//import MapKit
|
||||
//import CoreLocation
|
||||
// let regionBinding = Binding<MKCoordinateRegion>(
|
||||
// get: {
|
||||
// MKCoordinateRegion(center: nodeCoordinatePosition, span: MKCoordinateSpan(latitudeDelta: 0.005, longitudeDelta: 0.005))
|
||||
// },
|
||||
// set: { _ in }
|
||||
// )
|
||||
// let annotations = [MapLocation(name: node.user.shortName, coordinate: node.position.coordinate!)]
|
||||
//
|
||||
//struct NodeDetail: View {
|
||||
//
|
||||
// @EnvironmentObject var bleManager: BLEManager
|
||||
//
|
||||
// var node: NodeInfoModel
|
||||
//
|
||||
// struct MapLocation: Identifiable {
|
||||
// let id = UUID()
|
||||
// let name: String
|
||||
// let coordinate: CLLocationCoordinate2D
|
||||
// }
|
||||
//
|
||||
// var body: some View {
|
||||
//
|
||||
// GeometryReader { bounds in
|
||||
//
|
||||
// VStack {
|
||||
//
|
||||
// if node.position.coordinate != nil {
|
||||
//
|
||||
// let nodeCoordinatePosition = CLLocationCoordinate2D(latitude: node.position.latitude!, longitude: node.position.longitude!)
|
||||
//
|
||||
// let regionBinding = Binding<MKCoordinateRegion>(
|
||||
// get: {
|
||||
// MKCoordinateRegion(center: nodeCoordinatePosition, span: MKCoordinateSpan(latitudeDelta: 0.005, longitudeDelta: 0.005))
|
||||
// },
|
||||
// set: { _ in }
|
||||
// )
|
||||
// let annotations = [MapLocation(name: node.user.shortName, coordinate: node.position.coordinate!)]
|
||||
//
|
||||
// Map(coordinateRegion: regionBinding, showsUserLocation: true, userTrackingMode: .none, annotationItems: annotations) { location in
|
||||
// MapAnnotation(
|
||||
// coordinate: location.coordinate,
|
||||
// content: {
|
||||
// Map(coordinateRegion: regionBinding, showsUserLocation: true, userTrackingMode: .none, annotationItems: annotations) { location in
|
||||
// MapAnnotation(
|
||||
// coordinate: location.coordinate,
|
||||
// content: {
|
||||
// CircleText(text: node.user.shortName, color: .accentColor)
|
||||
// }
|
||||
// )
|
||||
// }.frame(idealWidth: bounds.size.width, minHeight: bounds.size.height / 2)
|
||||
// } else {
|
||||
// Image(node.user.hwModel)
|
||||
// .resizable()
|
||||
// .aspectRatio(contentMode: .fit)
|
||||
// .frame(width: bounds.size.width, height: bounds.size.height / 2)
|
||||
// }
|
||||
// ScrollView {
|
||||
//
|
||||
// HStack {
|
||||
//
|
||||
// VStack(alignment: .center) {
|
||||
// Text("AKA").font(.title2).fixedSize()
|
||||
// CircleText(text: node.user.shortName, color: .accentColor)
|
||||
// .offset(y: 10)
|
||||
// }
|
||||
// .padding([.leading, .trailing, .bottom])
|
||||
// Divider()
|
||||
// if node.snr != nil && node.snr! > 0 {
|
||||
// VStack(alignment: .center) {
|
||||
//
|
||||
// Image(systemName: "waveform.path")
|
||||
// .font(.title)
|
||||
// .foregroundColor(.accentColor)
|
||||
// .symbolRenderingMode(.hierarchical)
|
||||
// Text("SNR").font(.title2).fixedSize()
|
||||
// Text(String(node.snr ?? 0))
|
||||
// .font(.title2)
|
||||
// .foregroundColor(.gray)
|
||||
// }
|
||||
// Divider()
|
||||
// }
|
||||
// VStack(alignment: .center) {
|
||||
// }
|
||||
// )
|
||||
// }.frame(idealWidth: bounds.size.width, minHeight: bounds.size.height / 2)
|
||||
} else {
|
||||
Image(node.user?.hwModel ?? "UNSET")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: bounds.size.width, height: bounds.size.height / 2)
|
||||
}
|
||||
ScrollView {
|
||||
|
||||
HStack {
|
||||
|
||||
VStack(alignment: .center) {
|
||||
Text("AKA").font(.title2).fixedSize()
|
||||
CircleText(text: node.user?.shortName ?? "???", color: .accentColor)
|
||||
.offset(y: 10)
|
||||
}
|
||||
.padding([.leading, .trailing, .bottom])
|
||||
Divider()
|
||||
if node.snr > 0 {
|
||||
VStack(alignment: .center) {
|
||||
|
||||
Image(systemName: "waveform.path")
|
||||
.font(.title)
|
||||
.foregroundColor(.accentColor)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
Text("SNR").font(.title2).fixedSize()
|
||||
Text(String(node.snr))
|
||||
.font(.title2)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
Divider()
|
||||
}
|
||||
// VStack(alignment: .center) {
|
||||
// BatteryIcon(batteryLevel: node.position.batteryLevel, font: .title, color: .accentColor)
|
||||
// if node.position.batteryLevel != nil && node.position.batteryLevel! > 0 {
|
||||
// Text("Battery").font(.title2).fixedSize()
|
||||
// Text(String(node.position.batteryLevel!) + "%")
|
||||
// .font(.title2)
|
||||
// .foregroundColor(.gray)
|
||||
// .symbolRenderingMode(.hierarchical)
|
||||
// } else {
|
||||
// Text("Powered").font(.title2)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// }.padding(4)
|
||||
// Divider()
|
||||
// HStack {
|
||||
//
|
||||
// Image(node.user.hwModel)
|
||||
// .resizable()
|
||||
// .frame(width: 60, height: 60)
|
||||
// .cornerRadius(5)
|
||||
//
|
||||
// Text("Model: " + String(node.user.hwModel))
|
||||
// .font(.title3)
|
||||
// }
|
||||
// .padding()
|
||||
// Divider()
|
||||
//
|
||||
// if node.lastHeard > 0 {
|
||||
//
|
||||
// HStack {
|
||||
//
|
||||
// Image(systemName: "clock").font(.title2).foregroundColor(.accentColor)
|
||||
// let lastHeard = Date(timeIntervalSince1970: TimeInterval(node.lastHeard))
|
||||
// Text("Last Heard: \(lastHeard, style: .relative) ago").font(.title3)
|
||||
// }.padding()
|
||||
// Divider()
|
||||
// }
|
||||
//
|
||||
// if node.position.coordinate != nil {
|
||||
// HStack(alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/, spacing: 14) {
|
||||
// if node.position.batteryLevel != nil && node.position.batteryLevel! > 0 {
|
||||
// Text("Battery").font(.title2).fixedSize()
|
||||
// Text(String(node.position.batteryLevel!) + "%")
|
||||
// .font(.title2)
|
||||
// .foregroundColor(.gray)
|
||||
// .symbolRenderingMode(.hierarchical)
|
||||
// } else {
|
||||
// Text("Powered").font(.title2)
|
||||
// }
|
||||
// }
|
||||
|
||||
}.padding(4)
|
||||
Divider()
|
||||
HStack {
|
||||
|
||||
Image(node.user!.hwModel ?? "UNSET")
|
||||
.resizable()
|
||||
.frame(width: 60, height: 60)
|
||||
.cornerRadius(5)
|
||||
|
||||
Text("Model: " + String(node.user!.hwModel ?? "UNSET"))
|
||||
.font(.title3)
|
||||
}
|
||||
.padding()
|
||||
Divider()
|
||||
|
||||
if node.lastHeard > 0 {
|
||||
|
||||
HStack {
|
||||
|
||||
Image(systemName: "clock").font(.title2).foregroundColor(.accentColor)
|
||||
let lastHeard = Date(timeIntervalSince1970: TimeInterval(node.lastHeard))
|
||||
Text("Last Heard: \(lastHeard, style: .relative) ago").font(.title3)
|
||||
}.padding()
|
||||
Divider()
|
||||
}
|
||||
|
||||
// if node.position.coordinate != nil {
|
||||
// HStack(alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/, spacing: 14) {
|
||||
// Image(systemName: "mappin").font(.title).foregroundColor(.accentColor)
|
||||
// VStack(alignment: .leading) {
|
||||
// Text("Latitude").font(.headline)
|
||||
// Text(String(node.position.latitude ?? 0)).font(.caption).foregroundColor(.gray)
|
||||
// }
|
||||
// Divider()
|
||||
// VStack(alignment: .leading) {
|
||||
// Text("Longitude").font(.headline)
|
||||
// Text(String(node.position.longitude ?? 0)).font(.caption).foregroundColor(.gray)
|
||||
// }
|
||||
// Divider()
|
||||
// VStack(alignment: .leading) {
|
||||
// Text("Altitude").font(.headline)
|
||||
// Text(String(node.position.altitude ?? 0) + " m").font(.caption).foregroundColor(.gray)
|
||||
// }
|
||||
// }.padding()
|
||||
// Divider()
|
||||
// }
|
||||
// HStack(alignment: .center) {
|
||||
// VStack {
|
||||
// HStack {
|
||||
// Image(systemName: "person").font(.title3).foregroundColor(.accentColor)
|
||||
// Text("Unique Id:").font(.title3)
|
||||
// }
|
||||
// Text(node.user.id).font(.headline).foregroundColor(.gray)
|
||||
// }
|
||||
// Divider()
|
||||
// VStack {
|
||||
// HStack {
|
||||
// Image(systemName: "number").font(.title3).foregroundColor(.accentColor)
|
||||
// Text("Node Number:").font(.title3)
|
||||
// }
|
||||
// Text(String(node.num)).font(.headline).foregroundColor(.gray)
|
||||
// }
|
||||
// }.padding()
|
||||
// }
|
||||
// }.navigationTitle(node.user.longName)
|
||||
// .navigationBarTitleDisplayMode(.inline)
|
||||
// .navigationBarItems(trailing:
|
||||
//
|
||||
// ZStack {
|
||||
// // ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedNode != nil) ? bleManager.connectedNode.user.shortName : ((bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.name : "Unknown") )
|
||||
// })
|
||||
// }.ignoresSafeArea(.all, edges: [.leading, .trailing])
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//struct NodeDetail_Previews: PreviewProvider {
|
||||
// static let bleManager = BLEManager()
|
||||
//
|
||||
// static var previews: some View {
|
||||
// Group {
|
||||
// NodeDetail(node: bleManager.meshData.nodes[0])
|
||||
// NodeDetail(node: bleManager.meshData.nodes[1])
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
// VStack(alignment: .leading) {
|
||||
// Text("Latitude").font(.headline)
|
||||
// Text(String(node.position.latitude ?? 0)).font(.caption).foregroundColor(.gray)
|
||||
// }
|
||||
// Divider()
|
||||
// VStack(alignment: .leading) {
|
||||
// Text("Longitude").font(.headline)
|
||||
// Text(String(node.position.longitude ?? 0)).font(.caption).foregroundColor(.gray)
|
||||
// }
|
||||
// Divider()
|
||||
// VStack(alignment: .leading) {
|
||||
// Text("Altitude").font(.headline)
|
||||
// Text(String(node.position.altitude ?? 0) + " m").font(.caption).foregroundColor(.gray)
|
||||
// }
|
||||
// }.padding()
|
||||
// Divider()
|
||||
// }
|
||||
HStack(alignment: .center) {
|
||||
VStack {
|
||||
HStack {
|
||||
Image(systemName: "person").font(.title3).foregroundColor(.accentColor)
|
||||
Text("Unique Id:").font(.title3)
|
||||
}
|
||||
Text(node.user?.userId ?? "??????").font(.headline).foregroundColor(.gray)
|
||||
}
|
||||
Divider()
|
||||
VStack {
|
||||
HStack {
|
||||
Image(systemName: "number").font(.title3).foregroundColor(.accentColor)
|
||||
Text("Node Number:").font(.title3)
|
||||
}
|
||||
Text(String(node.num)).font(.headline).foregroundColor(.gray)
|
||||
}
|
||||
}.padding()
|
||||
}
|
||||
}.navigationTitle(node.user!.longName ?? "Unknown")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationBarItems(trailing:
|
||||
|
||||
ZStack {
|
||||
|
||||
ConnectedDevice(
|
||||
bluetoothOn: bleManager.isSwitchedOn,
|
||||
deviceConnected: bleManager.connectedPeripheral != nil,
|
||||
name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName :
|
||||
"???")
|
||||
})
|
||||
.onAppear(perform: {
|
||||
|
||||
self.bleManager.context = context
|
||||
|
||||
})
|
||||
}.ignoresSafeArea(.all, edges: [.leading, .trailing])
|
||||
}
|
||||
}
|
||||
|
||||
struct NodeInfoEntityDetail_Previews: PreviewProvider {
|
||||
static let bleManager = BLEManager()
|
||||
|
||||
static var previews: some View {
|
||||
Group {
|
||||
//NodeInfoEntityDetail(node: node)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,175 +0,0 @@
|
|||
/*
|
||||
Abstract:
|
||||
A view showing the details for a node.
|
||||
*/
|
||||
|
||||
import SwiftUI
|
||||
import MapKit
|
||||
import CoreLocation
|
||||
|
||||
struct NodeInfoEntityDetail: View {
|
||||
|
||||
@EnvironmentObject var bleManager: BLEManager
|
||||
|
||||
var node: NodeInfoEntity
|
||||
|
||||
struct MapLocation: Identifiable {
|
||||
let id = UUID()
|
||||
let name: String
|
||||
let coordinate: CLLocationCoordinate2D
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
|
||||
GeometryReader { bounds in
|
||||
|
||||
VStack {
|
||||
|
||||
if node.positions!.count > 0 {
|
||||
|
||||
// let nodeCoordinatePosition = CLLocationCoordinate2D(latitude: node.position.latitude!, longitude: node.position.longitude!)
|
||||
//
|
||||
// let regionBinding = Binding<MKCoordinateRegion>(
|
||||
// get: {
|
||||
// MKCoordinateRegion(center: nodeCoordinatePosition, span: MKCoordinateSpan(latitudeDelta: 0.005, longitudeDelta: 0.005))
|
||||
// },
|
||||
// set: { _ in }
|
||||
// )
|
||||
// let annotations = [MapLocation(name: node.user.shortName, coordinate: node.position.coordinate!)]
|
||||
//
|
||||
// Map(coordinateRegion: regionBinding, showsUserLocation: true, userTrackingMode: .none, annotationItems: annotations) { location in
|
||||
// MapAnnotation(
|
||||
// coordinate: location.coordinate,
|
||||
// content: {
|
||||
// CircleText(text: node.user.shortName, color: .accentColor)
|
||||
// }
|
||||
// )
|
||||
// }.frame(idealWidth: bounds.size.width, minHeight: bounds.size.height / 2)
|
||||
} else {
|
||||
Image(node.user?.hwModel ?? "UNSET")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: bounds.size.width, height: bounds.size.height / 2)
|
||||
}
|
||||
ScrollView {
|
||||
|
||||
HStack {
|
||||
|
||||
VStack(alignment: .center) {
|
||||
Text("AKA").font(.title2).fixedSize()
|
||||
CircleText(text: node.user?.shortName ?? "???", color: .accentColor)
|
||||
.offset(y: 10)
|
||||
}
|
||||
.padding([.leading, .trailing, .bottom])
|
||||
Divider()
|
||||
if node.snr > 0 {
|
||||
VStack(alignment: .center) {
|
||||
|
||||
Image(systemName: "waveform.path")
|
||||
.font(.title)
|
||||
.foregroundColor(.accentColor)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
Text("SNR").font(.title2).fixedSize()
|
||||
Text(String(node.snr))
|
||||
.font(.title2)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
Divider()
|
||||
}
|
||||
// VStack(alignment: .center) {
|
||||
// BatteryIcon(batteryLevel: node.position.batteryLevel, font: .title, color: .accentColor)
|
||||
// if node.position.batteryLevel != nil && node.position.batteryLevel! > 0 {
|
||||
// Text("Battery").font(.title2).fixedSize()
|
||||
// Text(String(node.position.batteryLevel!) + "%")
|
||||
// .font(.title2)
|
||||
// .foregroundColor(.gray)
|
||||
// .symbolRenderingMode(.hierarchical)
|
||||
// } else {
|
||||
// Text("Powered").font(.title2)
|
||||
// }
|
||||
// }
|
||||
|
||||
}.padding(4)
|
||||
Divider()
|
||||
HStack {
|
||||
|
||||
Image(node.user!.hwModel ?? "UNSET")
|
||||
.resizable()
|
||||
.frame(width: 60, height: 60)
|
||||
.cornerRadius(5)
|
||||
|
||||
Text("Model: " + String(node.user!.hwModel ?? "UNSET"))
|
||||
.font(.title3)
|
||||
}
|
||||
.padding()
|
||||
Divider()
|
||||
|
||||
if node.lastHeard > 0 {
|
||||
|
||||
HStack {
|
||||
|
||||
Image(systemName: "clock").font(.title2).foregroundColor(.accentColor)
|
||||
let lastHeard = Date(timeIntervalSince1970: TimeInterval(node.lastHeard))
|
||||
Text("Last Heard: \(lastHeard, style: .relative) ago").font(.title3)
|
||||
}.padding()
|
||||
Divider()
|
||||
}
|
||||
|
||||
// if node.position.coordinate != nil {
|
||||
// HStack(alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/, spacing: 14) {
|
||||
// Image(systemName: "mappin").font(.title).foregroundColor(.accentColor)
|
||||
// VStack(alignment: .leading) {
|
||||
// Text("Latitude").font(.headline)
|
||||
// Text(String(node.position.latitude ?? 0)).font(.caption).foregroundColor(.gray)
|
||||
// }
|
||||
// Divider()
|
||||
// VStack(alignment: .leading) {
|
||||
// Text("Longitude").font(.headline)
|
||||
// Text(String(node.position.longitude ?? 0)).font(.caption).foregroundColor(.gray)
|
||||
// }
|
||||
// Divider()
|
||||
// VStack(alignment: .leading) {
|
||||
// Text("Altitude").font(.headline)
|
||||
// Text(String(node.position.altitude ?? 0) + " m").font(.caption).foregroundColor(.gray)
|
||||
// }
|
||||
// }.padding()
|
||||
// Divider()
|
||||
// }
|
||||
HStack(alignment: .center) {
|
||||
VStack {
|
||||
HStack {
|
||||
Image(systemName: "person").font(.title3).foregroundColor(.accentColor)
|
||||
Text("Unique Id:").font(.title3)
|
||||
}
|
||||
Text(node.user?.userId ?? "??????").font(.headline).foregroundColor(.gray)
|
||||
}
|
||||
Divider()
|
||||
VStack {
|
||||
HStack {
|
||||
Image(systemName: "number").font(.title3).foregroundColor(.accentColor)
|
||||
Text("Node Number:").font(.title3)
|
||||
}
|
||||
Text(String(node.num)).font(.headline).foregroundColor(.gray)
|
||||
}
|
||||
}.padding()
|
||||
}
|
||||
}.navigationTitle(node.user!.longName ?? "Unknown")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationBarItems(trailing:
|
||||
|
||||
ZStack {
|
||||
// ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedNode != nil) ? bleManager.connectedNode.user.shortName : ((bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.name : "Unknown") )
|
||||
})
|
||||
}.ignoresSafeArea(.all, edges: [.leading, .trailing])
|
||||
}
|
||||
}
|
||||
|
||||
struct NodeInfoEntityDetail_Previews: PreviewProvider {
|
||||
static let bleManager = BLEManager()
|
||||
|
||||
static var previews: some View {
|
||||
Group {
|
||||
//NodeInfoEntityDetail(node: node)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -42,17 +42,28 @@ struct NodeList: View {
|
|||
} else {
|
||||
ForEach( nodes ) { node in
|
||||
let index = nodes.firstIndex(where: { $0.id == node.id })
|
||||
NavigationLink(destination: NodeInfoEntityDetail(node: node), tag: String(index!), selection: $selection) {
|
||||
NavigationLink(destination: NodeDetail(node: node), tag: String(index!), selection: $selection) {
|
||||
|
||||
if bleManager.connectedPeripheral != nil && bleManager.connectedPeripheral.myInfo != nil {
|
||||
if bleManager.connectedPeripheral != nil {
|
||||
|
||||
let connected: Bool = (bleManager.connectedPeripheral.myInfo!.myNodeNum == node.id)
|
||||
NodeInfoEntityRow(node: node, connected: connected)
|
||||
let connected: Bool = (bleManager.connectedPeripheral.name == node.bleName)
|
||||
NodeRow(node: node, connected: connected)
|
||||
} else {
|
||||
NodeInfoEntityRow(node: node, connected: false)
|
||||
NodeRow(node: node, connected: false)
|
||||
}
|
||||
|
||||
}
|
||||
.swipeActions {
|
||||
Button {
|
||||
|
||||
context.delete(node)
|
||||
|
||||
} label: {
|
||||
|
||||
Label("Delete from app", systemImage: "trash")
|
||||
}
|
||||
.tint(.red)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,13 +21,6 @@ struct NodeMap: View {
|
|||
animation: .default)
|
||||
|
||||
private var locationNodes: FetchedResults<NodeInfoEntity>
|
||||
|
||||
//var locationNodes: [NodeInfoModel]// {
|
||||
//bleManager.meshData.nodes.filter { node in
|
||||
// (node.position.coordinate != nil)
|
||||
// }
|
||||
//}
|
||||
|
||||
|
||||
struct MapLocation: Identifiable {
|
||||
let id = UUID()
|
||||
|
|
@ -38,6 +31,7 @@ struct NodeMap: View {
|
|||
var body: some View {
|
||||
let location = LocationHelper.currentLocation
|
||||
|
||||
|
||||
let currentCoordinatePosition = CLLocationCoordinate2D(latitude: location.latitude, longitude: location.longitude)
|
||||
let regionBinding = Binding<MKCoordinateRegion>(
|
||||
get: {
|
||||
|
|
@ -49,20 +43,32 @@ struct NodeMap: View {
|
|||
NavigationView {
|
||||
|
||||
ZStack {
|
||||
// Map(coordinateRegion: regionBinding,
|
||||
// interactionModes: [.all],
|
||||
// showsUserLocation: true,
|
||||
// userTrackingMode: .constant(.follow), annotationItems: locationNodes) { location in
|
||||
//
|
||||
// MapAnnotation(
|
||||
// coordinate: location.position.coordinate!,
|
||||
// content: {
|
||||
// CircleText(text: location.user.shortName, color: .accentColor)
|
||||
// }
|
||||
// )
|
||||
// }
|
||||
// .frame(maxHeight: .infinity)
|
||||
// .ignoresSafeArea(.all, edges: [.leading, .trailing])
|
||||
|
||||
Map(coordinateRegion: regionBinding, showsUserLocation: true, userTrackingMode: .none)
|
||||
.frame(maxHeight: .infinity)
|
||||
//, annotationItems: locationNodes[0].positions?) { location in
|
||||
// MapAnnotation(
|
||||
// coordinate: location.coordinate,
|
||||
// content: {
|
||||
// CircleText(text: location.latitude, color: .accentColor)
|
||||
// }
|
||||
// )
|
||||
// }.frame(maxHeight: .infinity)
|
||||
|
||||
//Map(coordinateRegion: regionBinding,
|
||||
// interactionModes: [.all],
|
||||
// showsUserLocation: true,
|
||||
// userTrackingMode: .constant(.follow), annotationItems: locationNodes) { node in
|
||||
|
||||
//MapAnnotation(
|
||||
//coordinate: node.positions[0].coordinate,
|
||||
//content: {
|
||||
// CircleText(text: node.user!.shortName, color: .accentColor)
|
||||
//}
|
||||
// )
|
||||
//}
|
||||
//.frame(maxHeight: .infinity)
|
||||
//.ignoresSafeArea(.all, edges: [.leading, .trailing])
|
||||
}
|
||||
.navigationTitle("Mesh Map")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
|
|
|
|||
|
|
@ -1,64 +1,64 @@
|
|||
//import SwiftUI
|
||||
//
|
||||
//struct NodeRow: View {
|
||||
// var node: NodeInfoModel
|
||||
// var connected: Bool
|
||||
//
|
||||
// var body: some View {
|
||||
// VStack(alignment: .leading) {
|
||||
//
|
||||
// HStack {
|
||||
//
|
||||
// CircleText(text: node.user.shortName, color: Color.accentColor).offset(y: 1).padding(.trailing, 5)
|
||||
// .offset(x: -15)
|
||||
//
|
||||
// if UIDevice.current.userInterfaceIdiom == .pad {
|
||||
// Text(node.user.longName).font(.headline)
|
||||
// .offset(x: -15)
|
||||
// } else {
|
||||
// Text(node.user.longName).font(.title)
|
||||
// .offset(x: -15)
|
||||
// }
|
||||
// }
|
||||
// .padding(.bottom, 10)
|
||||
//
|
||||
// HStack(alignment: .bottom) {
|
||||
//
|
||||
// Image(systemName: "clock.badge.checkmark.fill").font(.headline).foregroundColor(.accentColor).symbolRenderingMode(.hierarchical)
|
||||
//
|
||||
// if UIDevice.current.userInterfaceIdiom == .pad {
|
||||
//
|
||||
// if connected {
|
||||
// Text("Currently Connected").font(.caption).foregroundColor(Color.accentColor)
|
||||
// } else if node.lastHeard > 0 {
|
||||
// let lastHeard = Date(timeIntervalSince1970: TimeInterval(node.lastHeard))
|
||||
// Text("Last Heard: \(lastHeard, style: .relative) ago").font(.caption).foregroundColor(.gray)
|
||||
// } else {
|
||||
// Text("Last Heard: Unknown").font(.caption).foregroundColor(.gray)
|
||||
// }
|
||||
//
|
||||
// } else {
|
||||
// if connected {
|
||||
// Text("Currently Connected").font(.subheadline).foregroundColor(Color.accentColor)
|
||||
// } else if node.lastHeard > 0 {
|
||||
// let lastHeard = Date(timeIntervalSince1970: TimeInterval(node.lastHeard))
|
||||
// Text("Last Heard: \(lastHeard, style: .relative) ago").font(.subheadline).foregroundColor(.gray)
|
||||
// } else {
|
||||
// Text("Last Heard: Unknown").font(.subheadline).foregroundColor(.gray)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }.padding([.leading, .top, .bottom])
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//struct NodeRow_Previews: PreviewProvider {
|
||||
// static var nodes = BLEManager().meshData.nodes
|
||||
//
|
||||
// static var previews: some View {
|
||||
// Group {
|
||||
// NodeRow(node: nodes[0], connected: true)
|
||||
// }
|
||||
// .previewLayout(.fixed(width: 300, height: 70))
|
||||
// }
|
||||
//}
|
||||
import SwiftUI
|
||||
|
||||
struct NodeRow: View {
|
||||
var node: NodeInfoEntity
|
||||
var connected: Bool
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
|
||||
HStack {
|
||||
|
||||
CircleText(text: node.user?.shortName ?? "???", color: Color.accentColor).offset(y: 1).padding(.trailing, 5)
|
||||
.offset(x: -15)
|
||||
|
||||
if UIDevice.current.userInterfaceIdiom == .pad {
|
||||
Text(node.user?.longName ?? "Unknown").font(.headline)
|
||||
.offset(x: -15)
|
||||
} else {
|
||||
Text(node.user?.longName ?? "Unknown").font(.title)
|
||||
.offset(x: -15)
|
||||
}
|
||||
}
|
||||
.padding(.bottom, 10)
|
||||
|
||||
HStack(alignment: .bottom) {
|
||||
|
||||
Image(systemName: "clock.badge.checkmark.fill").font(.headline).foregroundColor(.accentColor).symbolRenderingMode(.hierarchical)
|
||||
|
||||
if UIDevice.current.userInterfaceIdiom == .pad {
|
||||
|
||||
if connected {
|
||||
Text("Currently Connected").font(.caption).foregroundColor(Color.accentColor)
|
||||
} else if node.lastHeard > 0 {
|
||||
let lastHeard = Date(timeIntervalSince1970: TimeInterval(node.lastHeard))
|
||||
Text("Last Heard: \(lastHeard, style: .relative) ago").font(.caption).foregroundColor(.gray)
|
||||
} else {
|
||||
Text("Last Heard: Unknown").font(.caption).foregroundColor(.gray)
|
||||
}
|
||||
|
||||
} else {
|
||||
if connected {
|
||||
Text("Currently Connected").font(.subheadline).foregroundColor(Color.accentColor)
|
||||
} else if node.lastHeard > 0 {
|
||||
let lastHeard = Date(timeIntervalSince1970: TimeInterval(node.lastHeard))
|
||||
Text("Last Heard: \(lastHeard, style: .relative) ago").font(.subheadline).foregroundColor(.gray)
|
||||
} else {
|
||||
Text("Last Heard: Unknown").font(.subheadline).foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
}
|
||||
}.padding([.leading, .top, .bottom])
|
||||
}
|
||||
}
|
||||
|
||||
struct NodeRow_Previews: PreviewProvider {
|
||||
//static var nodes = BLEManager().meshData.nodes
|
||||
|
||||
static var previews: some View {
|
||||
Group {
|
||||
//NodeInfoEntityRow(node: nodes[0], connected: true)
|
||||
}
|
||||
.previewLayout(.fixed(width: 300, height: 70))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -144,7 +144,9 @@ struct AppSettings: View {
|
|||
.navigationBarItems(trailing:
|
||||
|
||||
ZStack {
|
||||
// ConnectedDevice(bluetoothOn: self.bleManager.isSwitchedOn, deviceConnected: self.bleManager.connectedPeripheral != nil, name: (self.bleManager.connectedNode != nil) ? self.bleManager.connectedNode.user.shortName : ((self.bleManager.connectedPeripheral != nil) ? self.bleManager.connectedPeripheral.name : "Unknown") )
|
||||
|
||||
|
||||
//ConnectedDevice(bluetoothOn: self.bleManager.isSwitchedOn, deviceConnected: self.bleManager.connectedPeripheral != nil, name: (self.bleManager.connectedNode != nil) ? self.bleManager.connectedNode.user.shortName : ((self.bleManager.connectedPeripheral != nil) ? self.bleManager.connectedPeripheral.name : "Unknown") )
|
||||
})
|
||||
}
|
||||
.navigationViewStyle(StackNavigationViewStyle())
|
||||
|
|
@ -152,8 +154,7 @@ struct AppSettings: View {
|
|||
}
|
||||
|
||||
struct AppSettings_Previews: PreviewProvider {
|
||||
static let meshData = MeshData()
|
||||
|
||||
|
||||
static var previews: some View {
|
||||
Group {
|
||||
AppSettings()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue