mirror of
https://github.com/meshtastic/Meshtastic-Apple.git
synced 2026-04-20 22:13:56 +00:00
initial swift data conversion
This commit is contained in:
parent
183924d4dc
commit
b2c72ae166
130 changed files with 2939 additions and 2269 deletions
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import SwiftUI
|
||||
import MapKit
|
||||
import CoreData
|
||||
import SwiftData
|
||||
import CoreLocation
|
||||
import CoreBluetooth
|
||||
import OSLog
|
||||
|
|
@ -18,7 +18,7 @@ import ActivityKit
|
|||
|
||||
struct Connect: View {
|
||||
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@Environment(\.modelContext) private var context
|
||||
@EnvironmentObject var accessoryManager: AccessoryManager
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
@State var router: Router
|
||||
|
|
@ -351,8 +351,10 @@ struct Connect: View {
|
|||
|
||||
if let deviceNum = accessoryManager.activeDeviceNum, UserDefaults.preferredPeripheralId.count > 0 && state == .subscribed {
|
||||
|
||||
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
|
||||
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", deviceNum)
|
||||
var fetchNodeInfoRequest = FetchDescriptor<NodeInfoEntity>(
|
||||
predicate: #Predicate<NodeInfoEntity> { $0.num == deviceNum }
|
||||
)
|
||||
fetchNodeInfoRequest.fetchLimit = 1
|
||||
|
||||
do {
|
||||
node = try context.fetch(fetchNodeInfoRequest).first
|
||||
|
|
@ -373,8 +375,8 @@ struct Connect: View {
|
|||
liveActivityStarted = true
|
||||
// 15 Minutes Local Stats Interval
|
||||
let timerSeconds = 900
|
||||
let localStats = node?.telemetries?.filtered(using: NSPredicate(format: "metricsType == 4"))
|
||||
let mostRecent = localStats?.lastObject as? TelemetryEntity
|
||||
let localStats = node?.telemetries.filter { $0.metricsType == 4 }
|
||||
let mostRecent = localStats?.last
|
||||
|
||||
let activityAttributes = MeshActivityAttributes(nodeNum: Int(node?.num ?? 0), name: node?.user?.longName?.addingVariationSelectors ?? "unknown")
|
||||
|
||||
|
|
@ -445,7 +447,7 @@ struct TransportIcon: View {
|
|||
struct ManualConnectionMenu: View {
|
||||
|
||||
@EnvironmentObject var accessoryManager: AccessoryManager
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@Environment(\.modelContext) private var context
|
||||
|
||||
private struct IterableTransport: Identifiable {
|
||||
let id: UUID
|
||||
|
|
@ -518,7 +520,7 @@ struct ManualConnectionMenu: View {
|
|||
if accessoryManager.allowDisconnect {
|
||||
try await accessoryManager.disconnect()
|
||||
}
|
||||
await MeshPackets.shared.clearCoreDataDatabase(includeRoutes: false)
|
||||
await PersistenceController.shared.clearDatabase(includeRoutes: false)
|
||||
clearNotifications()
|
||||
try await selectedTransport?.transport.manuallyConnect(toDevice: device)
|
||||
|
||||
|
|
@ -532,7 +534,7 @@ struct ManualConnectionMenu: View {
|
|||
}
|
||||
|
||||
struct DeviceConnectRow: View {
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@Environment(\.modelContext) private var context
|
||||
@EnvironmentObject var accessoryManager: AccessoryManager
|
||||
@State var presentingSwitchPreferredPeripheral = false
|
||||
let device: Device
|
||||
|
|
@ -599,7 +601,7 @@ struct DeviceConnectRow: View {
|
|||
if accessoryManager.allowDisconnect {
|
||||
try await accessoryManager.disconnect()
|
||||
}
|
||||
await MeshPackets.shared.clearCoreDataDatabase(includeRoutes: false)
|
||||
await PersistenceController.shared.clearDatabase(includeRoutes: false)
|
||||
clearNotifications()
|
||||
|
||||
try await accessoryManager.connect(to: device)
|
||||
|
|
|
|||
|
|
@ -10,14 +10,14 @@ import Charts
|
|||
|
||||
struct BatteryGauge: View {
|
||||
|
||||
@ObservedObject var node: NodeInfoEntity
|
||||
@Bindable var node: NodeInfoEntity
|
||||
private let minValue = 0.0
|
||||
private let maxValue = 100.00
|
||||
|
||||
var body: some View {
|
||||
|
||||
let deviceMetrics = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0"))
|
||||
let mostRecent = deviceMetrics?.lastObject as? TelemetryEntity
|
||||
let deviceMetrics = node.telemetries.filter { $0.metricsType == 0 }
|
||||
let mostRecent = deviceMetrics.last
|
||||
// For VoiceOver purposes, detect when device is plugged in (battery > 100%)
|
||||
let isPluggedIn = (mostRecent?.batteryLevel ?? 0) > 100
|
||||
// Use a capped battery level for UI display
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import SwiftUI
|
|||
|
||||
struct ChannelLock: View {
|
||||
|
||||
@ObservedObject var channel: ChannelEntity
|
||||
@Bindable var channel: ChannelEntity
|
||||
var body: some View {
|
||||
/// Unencrypted - using no key at all or a known 1 byte key
|
||||
if channel.psk?.hexDescription.count ?? 0 < 3 {
|
||||
|
|
@ -33,14 +33,16 @@ struct ChannelLock: View {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: Fix preview for SwiftData
|
||||
/*
|
||||
#Preview {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
let encryptedChannel = ChannelEntity(context: context)
|
||||
let encryptedChannel = ChannelEntity()
|
||||
encryptedChannel.psk = Data([0x01, 0x02, 0x03, 0x04])
|
||||
let unencryptedChannel = ChannelEntity(context: context)
|
||||
let unencryptedChannel = ChannelEntity()
|
||||
unencryptedChannel.psk = Data()
|
||||
return HStack(spacing: 16) {
|
||||
HStack(spacing: 16) {
|
||||
ChannelLock(channel: encryptedChannel)
|
||||
ChannelLock(channel: unencryptedChannel)
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ A view draws a circle in the background of the shortName text
|
|||
*/
|
||||
|
||||
import SwiftUI
|
||||
import CoreData
|
||||
import SwiftData
|
||||
|
||||
struct CircleText: View {
|
||||
var text: String
|
||||
|
|
|
|||
|
|
@ -38,13 +38,12 @@ struct MessageTemplate: View {
|
|||
}
|
||||
|
||||
#Preview {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
let user = UserEntity(context: context)
|
||||
let user = UserEntity()
|
||||
user.longName = "Test User"
|
||||
user.shortName = "TU"
|
||||
let message = MessageEntity(context: context)
|
||||
let message = MessageEntity()
|
||||
message.messagePayload = "Hello, World!"
|
||||
message.messageTimestamp = Int32(Date().timeIntervalSince1970)
|
||||
message.replyID = 0
|
||||
return MessageTemplate(user: user, message: message)
|
||||
MessageTemplate(user: user, message: message)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,12 +6,12 @@
|
|||
//
|
||||
|
||||
import SwiftUI
|
||||
import CoreData
|
||||
import SwiftData
|
||||
import OSLog
|
||||
|
||||
struct ChannelList: View {
|
||||
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@Environment(\.modelContext) private var context
|
||||
@EnvironmentObject var accessoryManager: AccessoryManager
|
||||
@Binding var node: NodeInfoEntity?
|
||||
@Binding var channelSelection: ChannelEntity?
|
||||
|
|
@ -22,11 +22,8 @@ struct ChannelList: View {
|
|||
|
||||
var restrictedChannels = ["gpio", "mqtt", "serial", "admin"]
|
||||
|
||||
@FetchRequest(
|
||||
sortDescriptors: [NSSortDescriptor(keyPath: \ChannelEntity.index, ascending: true)],
|
||||
predicate: nil,
|
||||
animation: .default
|
||||
) private var channels: FetchedResults<ChannelEntity>
|
||||
@Query(sort: \ChannelEntity.index)
|
||||
private var channels: [ChannelEntity]
|
||||
|
||||
@ViewBuilder
|
||||
private func makeChannelRow(
|
||||
|
|
@ -108,72 +105,74 @@ struct ChannelList: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
@ViewBuilder
|
||||
private func makeChannelListItem(
|
||||
node: NodeInfoEntity,
|
||||
myInfo: MyInfoEntity,
|
||||
channel: ChannelEntity
|
||||
) -> some View {
|
||||
let hasMessages = channel.mostRecentPrivateMessage != nil
|
||||
makeChannelRow(myInfo: myInfo, channel: channel)
|
||||
.alignmentGuide(.listRowSeparatorLeading) {
|
||||
$0[.leading]
|
||||
}
|
||||
.frame(height: 62)
|
||||
.contextMenu {
|
||||
if hasMessages {
|
||||
Button(role: .destructive) {
|
||||
isPresentingDeleteChannelMessagesConfirm = true
|
||||
channelToDeleteMessages = channel
|
||||
} label: {
|
||||
Label("Delete Messages", systemImage: "trash")
|
||||
}
|
||||
}
|
||||
Button {
|
||||
channel.mute.toggle()
|
||||
Task {
|
||||
do {
|
||||
_ = try await accessoryManager.saveChannel(channel: channel.protoBuf, fromUser: node.user!, toUser: node.user!)
|
||||
Task { @MainActor in
|
||||
do {
|
||||
try context.save()
|
||||
} catch {
|
||||
context.rollback()
|
||||
Logger.data.error("💥 Save Channel Mute Error")
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
Logger.mesh.error("Unable to save channel")
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Label(channel.mute ? "Show Alerts" : "Hide Alerts", systemImage: channel.mute ? "bell" : "bell.slash")
|
||||
}
|
||||
}
|
||||
.confirmationDialog(
|
||||
"This conversation will be deleted.",
|
||||
isPresented: $isPresentingDeleteChannelMessagesConfirm,
|
||||
titleVisibility: .visible
|
||||
) {
|
||||
Button(role: .destructive) {
|
||||
Task {
|
||||
await MeshPackets.shared.deleteChannelMessages(channel: channelToDeleteMessages!)
|
||||
await MainActor.run {
|
||||
channelToDeleteMessages = nil
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Text("Delete")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
// Display Contacts for the rest of the non admin channels
|
||||
if let node, let myInfo = node.myInfo {
|
||||
List(selection: $channelSelection) {
|
||||
ForEach(channels) { (channel: ChannelEntity) in
|
||||
let hasMessages = channel.mostRecentPrivateMessage != nil
|
||||
if !restrictedChannels.contains(channel.name?.lowercased() ?? "") {
|
||||
makeChannelRow(myInfo: myInfo, channel: channel)
|
||||
.alignmentGuide(.listRowSeparatorLeading) {
|
||||
$0[.leading]
|
||||
}
|
||||
.frame(height: 62)
|
||||
.contextMenu {
|
||||
if hasMessages {
|
||||
Button(role: .destructive) {
|
||||
isPresentingDeleteChannelMessagesConfirm = true
|
||||
channelToDeleteMessages = channel
|
||||
} label: {
|
||||
Label("Delete Messages", systemImage: "trash")
|
||||
}
|
||||
}
|
||||
Button {
|
||||
channel.mute.toggle()
|
||||
do {
|
||||
Task {
|
||||
do {
|
||||
_ = try await accessoryManager.saveChannel(channel: channel.protoBuf, fromUser: node.user!, toUser: node.user!)
|
||||
Task { @MainActor in
|
||||
do {
|
||||
context.refresh(channel, mergeChanges: true)
|
||||
try context.save()
|
||||
} catch {
|
||||
context.rollback()
|
||||
Logger.data.error("💥 Save Channel Mute Error")
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
Logger.mesh.error("Unable to save channel")
|
||||
}
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Label(channel.mute ? "Show Alerts" : "Hide Alerts", systemImage: channel.mute ? "bell" : "bell.slash")
|
||||
}
|
||||
}
|
||||
.confirmationDialog(
|
||||
"This conversation will be deleted.",
|
||||
isPresented: $isPresentingDeleteChannelMessagesConfirm,
|
||||
titleVisibility: .visible
|
||||
) {
|
||||
Button(role: .destructive) {
|
||||
Task {
|
||||
await MeshPackets.shared.deleteChannelMessages(channel: channelToDeleteMessages!)
|
||||
await MainActor.run {
|
||||
context.refresh(channel, mergeChanges: true)
|
||||
context.refresh(myInfo, mergeChanges: true)
|
||||
|
||||
// Reset state
|
||||
channelToDeleteMessages = nil
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Text("Delete")
|
||||
}
|
||||
}
|
||||
makeChannelListItem(node: node, myInfo: myInfo, channel: channel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
// Created by Garth Vander Houwen on 12/24/21.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
import SwiftData
|
||||
import MeshtasticProtobufs
|
||||
import OSLog
|
||||
import SwiftUI
|
||||
|
|
@ -13,31 +13,28 @@ import SwiftUI
|
|||
struct ChannelMessageList: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
@Environment(\.scenePhase) var scenePhase
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@Environment(\.modelContext) private var context
|
||||
@EnvironmentObject var accessoryManager: AccessoryManager
|
||||
@FocusState var messageFieldFocused: Bool
|
||||
@ObservedObject var myInfo: MyInfoEntity
|
||||
@ObservedObject var channel: ChannelEntity
|
||||
@Bindable var myInfo: MyInfoEntity
|
||||
@Bindable var channel: ChannelEntity
|
||||
@State private var replyMessageId: Int64 = 0
|
||||
@State private var redrawTapbacksTrigger = UUID()
|
||||
@AppStorage("preferredPeripheralNum") private var preferredPeripheralNum = -1
|
||||
@State private var messageToHighlight: Int64 = 0
|
||||
@FetchRequest private var allPrivateMessages: FetchedResults<MessageEntity>
|
||||
@Query private var allPrivateMessages: [MessageEntity]
|
||||
|
||||
init(myInfo: MyInfoEntity, channel: ChannelEntity) {
|
||||
self.myInfo = myInfo
|
||||
self.channel = channel
|
||||
|
||||
// Configure fetch request here
|
||||
let request: NSFetchRequest<MessageEntity> = MessageEntity.fetchRequest()
|
||||
request.sortDescriptors = [
|
||||
NSSortDescriptor(keyPath: \MessageEntity.messageTimestamp, ascending: true)
|
||||
]
|
||||
request.predicate = NSPredicate(
|
||||
format: "channel == %ld AND toUser == nil AND isEmoji == false",
|
||||
channel.index
|
||||
let channelIndex = channel.index
|
||||
_allPrivateMessages = Query(
|
||||
filter: #Predicate<MessageEntity> {
|
||||
$0.channel == channelIndex && $0.toUser == nil && $0.isEmoji == false
|
||||
},
|
||||
sort: \MessageEntity.messageTimestamp
|
||||
)
|
||||
_allPrivateMessages = FetchRequest(fetchRequest: request)
|
||||
}
|
||||
|
||||
func handleInteractionComplete() {
|
||||
|
|
@ -53,7 +50,6 @@ struct ChannelMessageList: View {
|
|||
try context.save()
|
||||
Logger.data.info("📖 [App] All unread messages marked as read.")
|
||||
appState.unreadChannelMessages = myInfo.unreadMessages
|
||||
context.refresh(myInfo, mergeChanges: true)
|
||||
} catch {
|
||||
Logger.data.error("Failed to read messages: \(error.localizedDescription, privacy: .public)")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import CoreData
|
||||
import SwiftData
|
||||
import MeshtasticProtobufs
|
||||
import SwiftUI
|
||||
|
||||
|
|
@ -6,9 +6,9 @@ struct ChannelMessageRow: View {
|
|||
@EnvironmentObject var appState: AppState
|
||||
|
||||
// Core Data object observed for changes (like Tapbacks being received)
|
||||
@ObservedObject var message: MessageEntity
|
||||
@Bindable var message: MessageEntity
|
||||
|
||||
let allMessages: FetchedResults<MessageEntity> // The full list for reply lookup
|
||||
let allMessages: [MessageEntity] // The full list for reply lookup
|
||||
let previousMessage: MessageEntity?
|
||||
let preferredPeripheralNum: Int
|
||||
let channel: ChannelEntity
|
||||
|
|
@ -24,7 +24,7 @@ struct ChannelMessageRow: View {
|
|||
}
|
||||
|
||||
init(message: MessageEntity,
|
||||
allMessages: FetchedResults<MessageEntity>,
|
||||
allMessages: [MessageEntity],
|
||||
previousMessage: MessageEntity?,
|
||||
preferredPeripheralNum: Int,
|
||||
channel: ChannelEntity,
|
||||
|
|
@ -34,7 +34,7 @@ struct ChannelMessageRow: View {
|
|||
scrollView: ScrollViewProxy,
|
||||
onInteractionComplete: @escaping () -> Void) {
|
||||
// Initialize ObservedObject with the concrete instance
|
||||
self._message = ObservedObject(initialValue: message)
|
||||
self.message = message
|
||||
self.allMessages = allMessages
|
||||
self.previousMessage = previousMessage
|
||||
self.preferredPeripheralNum = preferredPeripheralNum
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import SwiftUI
|
||||
import CoreData
|
||||
import SwiftData
|
||||
import OSLog
|
||||
|
||||
struct MessageContextMenuItems: View {
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@Environment(\.modelContext) private var context
|
||||
@EnvironmentObject var accessoryManager: AccessoryManager
|
||||
|
||||
let message: MessageEntity
|
||||
|
|
@ -148,7 +148,7 @@ struct MessageContextMenuItems: View {
|
|||
}
|
||||
|
||||
private extension MessageDestination {
|
||||
var managedObject: NSManagedObject {
|
||||
var persistentModel: any PersistentModel {
|
||||
switch self {
|
||||
case let .user(user): return user
|
||||
case let .channel(channel): return channel
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ struct MessageText: View {
|
|||
)
|
||||
static let dateFormatString = (localeDateFormat ?? "MM/dd/YY j:mm:ss:a")
|
||||
static let timeFormatString = (localeTimeFormat ?? "j:mm:ss:a")
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@Environment(\.modelContext) private var context
|
||||
@EnvironmentObject var accessoryManager: AccessoryManager
|
||||
|
||||
let message: MessageEntity
|
||||
|
|
@ -284,10 +284,10 @@ struct MessageText: View {
|
|||
)
|
||||
await MainActor.run {
|
||||
switch tapBackDestination {
|
||||
case let .channel(channel):
|
||||
context.refresh(channel, mergeChanges: true)
|
||||
case let .user(user):
|
||||
context.refresh(user, mergeChanges: true)
|
||||
case .channel:
|
||||
break
|
||||
case .user:
|
||||
break
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
|
|
|
|||
|
|
@ -6,13 +6,13 @@
|
|||
//
|
||||
|
||||
import SwiftUI
|
||||
import CoreData
|
||||
import SwiftData
|
||||
import OSLog
|
||||
import TipKit
|
||||
|
||||
struct Messages: View {
|
||||
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@Environment(\.modelContext) private var context
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
@ObservedObject var router: Router
|
||||
@Binding var unreadChannelMessages: Int
|
||||
|
|
@ -116,10 +116,7 @@ struct Messages: View {
|
|||
switch state {
|
||||
case .channels(channelId: let channelId, messageId: _):
|
||||
if let channelId {
|
||||
channelSelection = node?.myInfo?.channels?.first(where: { channel in
|
||||
guard let channel = channel as? ChannelEntity else { return false }
|
||||
return channel.id == channelId
|
||||
}) as? ChannelEntity
|
||||
channelSelection = node?.myInfo?.channels.first { $0.id == channelId }
|
||||
} else {
|
||||
channelSelection = nil
|
||||
userSelection = nil
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import SwiftUI
|
|||
import OSLog
|
||||
|
||||
struct RetryButton: View {
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@Environment(\.modelContext) private var context
|
||||
@EnvironmentObject var accessoryManager: AccessoryManager
|
||||
|
||||
let message: MessageEntity
|
||||
|
|
@ -48,7 +48,6 @@ struct RetryButton: View {
|
|||
// to messages is via a weak fetched property which is not updated by
|
||||
// `bleManager.sendMessage` unlike the user entity.
|
||||
Task { @MainActor in
|
||||
context.refresh(channel, mergeChanges: true)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import SwiftUI
|
|||
import OSLog
|
||||
|
||||
struct TapbackResponses: View {
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@Environment(\.modelContext) private var context
|
||||
|
||||
let message: MessageEntity
|
||||
let onRead: () -> Void
|
||||
|
|
|
|||
|
|
@ -6,13 +6,13 @@
|
|||
//
|
||||
|
||||
import SwiftUI
|
||||
import CoreData
|
||||
import SwiftData
|
||||
import OSLog
|
||||
import TipKit
|
||||
|
||||
struct UserList: View {
|
||||
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@Environment(\.modelContext) private var context
|
||||
@EnvironmentObject var accessoryManager: AccessoryManager
|
||||
@State private var editingFilters = false
|
||||
@State private var showingHelp = false
|
||||
|
|
@ -69,30 +69,28 @@ struct UserList: View {
|
|||
|
||||
fileprivate struct FilteredUserList: View {
|
||||
@EnvironmentObject var accessoryManager: AccessoryManager
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@Environment(\.modelContext) private var context
|
||||
|
||||
@FetchRequest private var users: FetchedResults<UserEntity>
|
||||
@Query(sort: [SortDescriptor(\UserEntity.lastMessage, order: .reverse),
|
||||
SortDescriptor(\UserEntity.longName)])
|
||||
private var allUsers: [UserEntity]
|
||||
@Binding var userSelection: UserEntity?
|
||||
@Binding var node: NodeInfoEntity?
|
||||
|
||||
@State private var isPresentingDeleteUserMessagesConfirm: Bool = false
|
||||
@State private var userToDeleteMessages: UserEntity?
|
||||
private var filters: NodeFilterParameters
|
||||
|
||||
init(withFilters: NodeFilterParameters, node: Binding<NodeInfoEntity?>, userSelection: Binding<UserEntity?>) {
|
||||
let request: NSFetchRequest<UserEntity> = UserEntity.fetchRequest()
|
||||
request.sortDescriptors = [
|
||||
NSSortDescriptor(key: "lastMessage", ascending: false),
|
||||
NSSortDescriptor(key: "userNode.favorite", ascending: false),
|
||||
NSSortDescriptor(key: "pkiEncrypted", ascending: false),
|
||||
NSSortDescriptor(key: "userNode.lastHeard", ascending: false),
|
||||
NSSortDescriptor(key: "longName", ascending: true)
|
||||
]
|
||||
request.predicate = withFilters.buildPredicate()
|
||||
self._users = FetchRequest(fetchRequest: request)
|
||||
self.filters = withFilters
|
||||
self._node = node
|
||||
self._userSelection = userSelection
|
||||
}
|
||||
|
||||
private var users: [UserEntity] {
|
||||
allUsers.filter { filters.matches(user: $0) }
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
let localeDateFormat = DateFormatter.dateFormat(fromTemplate: "yyMMdd", options: 0, locale: Locale.current)
|
||||
let dateFormatString = (localeDateFormat ?? "MM/dd/YY")
|
||||
|
|
@ -188,7 +186,6 @@ fileprivate struct FilteredUserList: View {
|
|||
Logger.data.info("Unfavorited a node")
|
||||
}
|
||||
}
|
||||
context.refresh(user, mergeChanges: true)
|
||||
do {
|
||||
try context.save()
|
||||
} catch {
|
||||
|
|
@ -226,7 +223,6 @@ fileprivate struct FilteredUserList: View {
|
|||
Button(role: .destructive) {
|
||||
Task {
|
||||
await MeshPackets.shared.deleteUserMessages(user: userToDeleteMessages!)
|
||||
context.refresh(node!.user!, mergeChanges: true)
|
||||
}
|
||||
} label: {
|
||||
Text("Delete")
|
||||
|
|
@ -239,6 +235,84 @@ fileprivate struct FilteredUserList: View {
|
|||
}
|
||||
}
|
||||
fileprivate extension NodeFilterParameters {
|
||||
func matches(user: UserEntity) -> Bool {
|
||||
// Search text
|
||||
if !searchText.isEmpty {
|
||||
let text = searchText.lowercased()
|
||||
let matchesSearch = [user.userId, user.numString, user.hwModel, user.hwDisplayName, user.longName, user.shortName]
|
||||
.compactMap { $0?.lowercased() }
|
||||
.contains { $0.contains(text) }
|
||||
if !matchesSearch { return false }
|
||||
}
|
||||
// Mqtt and lora
|
||||
if !(viaLora && viaMqtt) {
|
||||
if viaLora {
|
||||
if user.userNode?.viaMqtt == true { return false }
|
||||
} else {
|
||||
if user.userNode?.viaMqtt != true { return false }
|
||||
}
|
||||
}
|
||||
// Roles
|
||||
if roleFilter && !deviceRoles.isEmpty {
|
||||
let userRole = Int(user.role)
|
||||
if !deviceRoles.contains(userRole) { return false }
|
||||
}
|
||||
// Hops Away
|
||||
if hopsAway == 0 {
|
||||
if user.userNode?.hopsAway != 0 { return false }
|
||||
} else if hopsAway > -1 {
|
||||
let nodeHops = user.userNode?.hopsAway ?? 0
|
||||
if nodeHops <= 0 || nodeHops > Int32(hopsAway) { return false }
|
||||
}
|
||||
// Online
|
||||
if isOnline {
|
||||
let twoHoursAgo = Calendar.current.date(byAdding: .minute, value: -120, to: Date()) ?? Date.distantPast
|
||||
if let lastHeard = user.userNode?.lastHeard, lastHeard < twoHoursAgo { return false }
|
||||
if user.userNode?.lastHeard == nil { return false }
|
||||
}
|
||||
// Encrypted
|
||||
if isPkiEncrypted {
|
||||
if !user.pkiEncrypted { return false }
|
||||
}
|
||||
// Favorites
|
||||
if isFavorite {
|
||||
if user.userNode?.favorite != true { return false }
|
||||
}
|
||||
// Distance
|
||||
if distanceFilter {
|
||||
if let poi = LocationsHandler.currentLocation,
|
||||
poi.latitude != LocationsHandler.DefaultLocation.latitude,
|
||||
poi.longitude != LocationsHandler.DefaultLocation.longitude {
|
||||
let d = maxDistance * 1.1
|
||||
let r: Double = 6371009
|
||||
let meanLat = poi.latitude * .pi / 180
|
||||
let deltaLat = d / r * 180 / .pi
|
||||
let deltaLon = d / (r * cos(meanLat)) * 180 / .pi
|
||||
let minLat = poi.latitude - deltaLat
|
||||
let maxLat = poi.latitude + deltaLat
|
||||
let minLon = poi.longitude - deltaLon
|
||||
let maxLon = poi.longitude + deltaLon
|
||||
let hasNearbyPosition = (user.userNode?.positions ?? []).contains { pos in
|
||||
guard pos.latest else { return false }
|
||||
let lon = Double(pos.longitudeI) / 1e7
|
||||
let lat = Double(pos.latitudeI) / 1e7
|
||||
return lon >= minLon && lon <= maxLon && lat >= minLat && lat <= maxLat
|
||||
}
|
||||
if !hasNearbyPosition { return false }
|
||||
}
|
||||
}
|
||||
// Unmessagable filter
|
||||
if user.unmessagable {
|
||||
let hasMessages = !(user.receivedMessages ?? []).isEmpty || !(user.sentMessages ?? []).isEmpty
|
||||
if !hasMessages { return false }
|
||||
}
|
||||
// Ignored
|
||||
if user.userNode?.ignored == true { return false }
|
||||
// Connected node
|
||||
if user.numString == String(UserDefaults.preferredPeripheralNum) { return false }
|
||||
return true
|
||||
}
|
||||
|
||||
func buildPredicate() -> NSPredicate? {
|
||||
var predicates: [NSPredicate] = []
|
||||
// Search text predicates
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
//
|
||||
|
||||
import SwiftUI
|
||||
import CoreData
|
||||
import SwiftData
|
||||
import OSLog
|
||||
import MeshtasticProtobufs // Added to ensure RoutingError is accessible if needed
|
||||
|
||||
|
|
@ -14,21 +14,19 @@ struct UserMessageList: View {
|
|||
@EnvironmentObject var appState: AppState
|
||||
@EnvironmentObject var accessoryManager: AccessoryManager
|
||||
@Environment(\.scenePhase) var scenePhase
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@Environment(\.modelContext) private var context
|
||||
@FocusState var messageFieldFocused: Bool
|
||||
@ObservedObject var user: UserEntity
|
||||
@Bindable var user: UserEntity
|
||||
@State private var replyMessageId: Int64 = 0
|
||||
@State private var messageToHighlight: Int64 = 0
|
||||
@State private var redrawTapbacksTrigger = UUID()
|
||||
@AppStorage("preferredPeripheralNum") private var preferredPeripheralNum = -1
|
||||
@FetchRequest private var allPrivateMessages: FetchedResults<MessageEntity>
|
||||
|
||||
init(user: UserEntity) {
|
||||
self.user = user
|
||||
|
||||
// Configure fetch request here
|
||||
let request: NSFetchRequest<MessageEntity> = user.messageFetchRequest
|
||||
_allPrivateMessages = FetchRequest(fetchRequest: request)
|
||||
private var allPrivateMessages: [MessageEntity] {
|
||||
let sent = user.sentMessages ?? []
|
||||
let received = user.receivedMessages ?? []
|
||||
return (sent + received)
|
||||
.filter { !$0.isEmoji }
|
||||
.sorted { $0.messageTimestamp < $1.messageTimestamp }
|
||||
}
|
||||
|
||||
func handleInteractionComplete() {
|
||||
|
|
@ -49,8 +47,6 @@ struct UserMessageList: View {
|
|||
let connectedUser = connectedNode.user {
|
||||
appState.unreadDirectMessages = connectedUser.unreadMessages(context: context, skipLastMessageCheck: true) // skipLastMessageCheck=true because we don't update lastMessage on our own connected node
|
||||
}
|
||||
|
||||
context.refresh(user, mergeChanges: true)
|
||||
} catch {
|
||||
Logger.data.error("Failed to read direct messages: \(error.localizedDescription, privacy: .public)")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,14 +5,14 @@
|
|||
// Copyright(c) Garth Vander Houwen 10/1/2025
|
||||
//
|
||||
|
||||
import CoreData
|
||||
import SwiftData
|
||||
import MeshtasticProtobufs
|
||||
import SwiftUI
|
||||
|
||||
struct UserMessageRow: View {
|
||||
|
||||
@EnvironmentObject var appState: AppState
|
||||
@ObservedObject var message: MessageEntity
|
||||
@Bindable var message: MessageEntity
|
||||
let allMessages: [MessageEntity]
|
||||
let previousMessage: MessageEntity?
|
||||
let preferredPeripheralNum: Int
|
||||
|
|
@ -38,7 +38,7 @@ struct UserMessageRow: View {
|
|||
scrollView: ScrollViewProxy,
|
||||
onInteractionComplete: @escaping () -> Void) {
|
||||
// Initialize ObservedObject with the concrete instance
|
||||
self._message = ObservedObject(initialValue: message)
|
||||
self.message = message
|
||||
self.allMessages = allMessages
|
||||
self.previousMessage = previousMessage
|
||||
self.preferredPeripheralNum = preferredPeripheralNum
|
||||
|
|
|
|||
|
|
@ -6,20 +6,21 @@
|
|||
//
|
||||
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
import Charts
|
||||
import MeshtasticProtobufs
|
||||
import OSLog
|
||||
|
||||
struct DetectionSensorLog: View {
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@Environment(\.modelContext) private var context
|
||||
@EnvironmentObject var accessoryManager: AccessoryManager
|
||||
@State private var isPresentingClearLogConfirm: Bool = false
|
||||
@State var isExporting = false
|
||||
@State var exportString = ""
|
||||
@ObservedObject var node: NodeInfoEntity
|
||||
@FetchRequest(sortDescriptors: [NSSortDescriptor(key: "messageTimestamp", ascending: false)],
|
||||
predicate: NSPredicate(format: "portNum == %d", Int32(PortNum.detectionSensorApp.rawValue)), animation: .none)
|
||||
private var detections: FetchedResults<MessageEntity>
|
||||
@Bindable var node: NodeInfoEntity
|
||||
@Query(filter: #Predicate<MessageEntity> { $0.portNum == 10 },
|
||||
sort: \MessageEntity.messageTimestamp, order: .reverse)
|
||||
private var detections: [MessageEntity]
|
||||
|
||||
var body: some View {
|
||||
let oneDayAgo = Calendar.current.date(byAdding: .day, value: -1, to: Date())
|
||||
|
|
@ -142,15 +143,17 @@ struct DetectionSensorLog: View {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: Fix preview for SwiftData
|
||||
/*
|
||||
#Preview {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
let node = NodeInfoEntity(context: context)
|
||||
let node = NodeInfoEntity()
|
||||
node.num = 123456789
|
||||
let user = UserEntity(context: context)
|
||||
let user = UserEntity()
|
||||
user.longName = "Test Node"
|
||||
user.shortName = "TN"
|
||||
node.user = user
|
||||
return DetectionSensorLog(node: node)
|
||||
DetectionSensorLog(node: node)
|
||||
.environmentObject(AccessoryManager.shared)
|
||||
.environment(\.managedObjectContext, context)
|
||||
.modelContainer(PersistenceController.preview.container)
|
||||
}
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import OSLog
|
|||
|
||||
struct DeviceMetricsLog: View {
|
||||
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@Environment(\.modelContext) private var context
|
||||
@EnvironmentObject var accessoryManager: AccessoryManager
|
||||
private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom }
|
||||
|
||||
|
|
@ -21,7 +21,7 @@ struct DeviceMetricsLog: View {
|
|||
@State private var batteryChartColor: Color = .blue
|
||||
@State private var airtimeChartColor: Color = .yellow
|
||||
@State private var channelUtilizationChartColor: Color = .green
|
||||
@ObservedObject var node: NodeInfoEntity
|
||||
@Bindable var node: NodeInfoEntity
|
||||
@State private var sortOrder = [KeyPathComparator(\TelemetryEntity.time, order: .reverse)]
|
||||
@State private var selection: TelemetryEntity.ID?
|
||||
@State private var chartSelection: Date?
|
||||
|
|
@ -30,7 +30,7 @@ struct DeviceMetricsLog: View {
|
|||
VStack {
|
||||
if node.hasDeviceMetrics {
|
||||
let oneWeekAgo = Calendar.current.date(byAdding: .day, value: -7, to: Date())
|
||||
let deviceMetrics = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0")).reversed() as? [TelemetryEntity] ?? []
|
||||
let deviceMetrics = node.telemetries.filter { $0.metricsType == 0 }.reversed()
|
||||
let chartData = deviceMetrics
|
||||
.filter { $0.time != nil && $0.time! >= oneWeekAgo! }
|
||||
.sorted { $0.time! < $1.time! }
|
||||
|
|
@ -255,15 +255,17 @@ struct DeviceMetricsLog: View {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: Fix preview for SwiftData
|
||||
/*
|
||||
#Preview {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
let node = NodeInfoEntity(context: context)
|
||||
let node = NodeInfoEntity()
|
||||
node.num = 123456789
|
||||
let user = UserEntity(context: context)
|
||||
let user = UserEntity()
|
||||
user.longName = "Test Node"
|
||||
user.shortName = "TN"
|
||||
node.user = user
|
||||
return DeviceMetricsLog(node: node)
|
||||
DeviceMetricsLog(node: node)
|
||||
.environmentObject(AccessoryManager.shared)
|
||||
.environment(\.managedObjectContext, context)
|
||||
.modelContainer(PersistenceController.preview.container)
|
||||
}
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -7,33 +7,27 @@
|
|||
import SwiftUI
|
||||
import Charts
|
||||
import OSLog
|
||||
import CoreData
|
||||
import SwiftData
|
||||
|
||||
struct EnvironmentMetricsLog: View {
|
||||
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@Environment(\.modelContext) private var context
|
||||
@EnvironmentObject var accessoryManager: AccessoryManager
|
||||
@State private var isPresentingClearLogConfirm: Bool = false
|
||||
@State var isExporting = false
|
||||
@State var exportString = ""
|
||||
@ObservedObject var node: NodeInfoEntity
|
||||
@Bindable var node: NodeInfoEntity
|
||||
|
||||
@StateObject var columnList = MetricsColumnList.environmentDefaultColumns
|
||||
@StateObject var seriesList = MetricsSeriesList.environmentDefaultChartSeries
|
||||
|
||||
@State var isEditingColumnConfiguration = false
|
||||
|
||||
@FetchRequest private var chartData: FetchedResults<TelemetryEntity>
|
||||
|
||||
init(node: NodeInfoEntity) {
|
||||
self.node = node
|
||||
|
||||
// Build fetch request:
|
||||
let request: NSFetchRequest<TelemetryEntity> = TelemetryEntity.fetchRequest()
|
||||
private var chartData: [TelemetryEntity] {
|
||||
let oneWeekAgo = Calendar.current.date(byAdding: .day, value: -7, to: Date()) ?? Date.distantPast
|
||||
request.predicate = NSPredicate(format: "nodeTelemetry == %@ AND metricsType == 1 AND time >= %@", node, oneWeekAgo as NSDate)
|
||||
request.sortDescriptors = [NSSortDescriptor(key: "time", ascending: false)]
|
||||
_chartData = FetchRequest(fetchRequest: request)
|
||||
return (node.telemetries ?? [])
|
||||
.filter { $0.metricsType == 1 && ($0.time ?? Date.distantPast) >= oneWeekAgo }
|
||||
.sorted { ($0.time ?? .distantPast) > ($1.time ?? .distantPast) }
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
|
|
@ -187,15 +181,17 @@ struct EnvironmentMetricsLog: View {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: Fix preview for SwiftData
|
||||
/*
|
||||
#Preview {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
let node = NodeInfoEntity(context: context)
|
||||
let node = NodeInfoEntity()
|
||||
node.num = 123456789
|
||||
let user = UserEntity(context: context)
|
||||
let user = UserEntity()
|
||||
user.longName = "Test Node"
|
||||
user.shortName = "TN"
|
||||
node.user = user
|
||||
return EnvironmentMetricsLog(node: node)
|
||||
EnvironmentMetricsLog(node: node)
|
||||
.environmentObject(AccessoryManager.shared)
|
||||
.environment(\.managedObjectContext, context)
|
||||
.modelContainer(PersistenceController.preview.container)
|
||||
}
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -41,12 +41,14 @@ struct ClientHistoryButton: View {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: Fix preview for SwiftData
|
||||
/*
|
||||
#Preview {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
let node = NodeInfoEntity(context: context)
|
||||
let node = NodeInfoEntity()
|
||||
node.num = 123456789
|
||||
let connectedNode = NodeInfoEntity(context: context)
|
||||
let connectedNode = NodeInfoEntity()
|
||||
connectedNode.num = 987654321
|
||||
return ClientHistoryButton(connectedNode: connectedNode, node: node)
|
||||
ClientHistoryButton(connectedNode: connectedNode, node: node)
|
||||
.environmentObject(AccessoryManager.shared)
|
||||
}
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import CoreData
|
||||
import SwiftData
|
||||
import OSLog
|
||||
import SwiftUI
|
||||
|
||||
struct DeleteNodeButton: View {
|
||||
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@Environment(\.modelContext) private var context
|
||||
@EnvironmentObject var accessoryManager: AccessoryManager
|
||||
|
||||
var connectedNode: NodeInfoEntity
|
||||
|
|
@ -65,13 +65,15 @@ struct DeleteNodeButton: View {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: Fix preview for SwiftData
|
||||
/*
|
||||
#Preview {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
let connectedNode = NodeInfoEntity(context: context)
|
||||
let connectedNode = NodeInfoEntity()
|
||||
connectedNode.num = 987654321
|
||||
let node = NodeInfoEntity(context: context)
|
||||
let node = NodeInfoEntity()
|
||||
node.num = 123456789
|
||||
return DeleteNodeButton(connectedNode: connectedNode, node: node)
|
||||
DeleteNodeButton(connectedNode: connectedNode, node: node)
|
||||
.environmentObject(AccessoryManager.shared)
|
||||
.environment(\.managedObjectContext, context)
|
||||
.modelContainer(PersistenceController.preview.container)
|
||||
}
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import CoreData
|
||||
import SwiftData
|
||||
import SwiftUI
|
||||
|
||||
struct ExchangePositionsButton: View {
|
||||
|
|
@ -62,12 +62,14 @@ struct ExchangePositionsButton: View {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: Fix preview for SwiftData
|
||||
/*
|
||||
#Preview {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
let node = NodeInfoEntity(context: context)
|
||||
let node = NodeInfoEntity()
|
||||
node.num = 123456789
|
||||
let connectedNode = NodeInfoEntity(context: context)
|
||||
let connectedNode = NodeInfoEntity()
|
||||
connectedNode.num = 987654321
|
||||
return ExchangePositionsButton(node: node, connectedNode: connectedNode)
|
||||
ExchangePositionsButton(node: node, connectedNode: connectedNode)
|
||||
.environmentObject(AccessoryManager.shared)
|
||||
}
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import CoreData
|
||||
import SwiftData
|
||||
import SwiftUI
|
||||
import OSLog
|
||||
|
||||
|
|
@ -60,12 +60,14 @@ struct ExchangeUserInfoButton: View {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: Fix preview for SwiftData
|
||||
/*
|
||||
#Preview {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
let node = NodeInfoEntity(context: context)
|
||||
let node = NodeInfoEntity()
|
||||
node.num = 123456789
|
||||
let connectedNode = NodeInfoEntity(context: context)
|
||||
let connectedNode = NodeInfoEntity()
|
||||
connectedNode.num = 987654321
|
||||
return ExchangeUserInfoButton(node: node, connectedNode: connectedNode)
|
||||
ExchangeUserInfoButton(node: node, connectedNode: connectedNode)
|
||||
.environmentObject(AccessoryManager.shared)
|
||||
}
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
import CoreData
|
||||
import SwiftData
|
||||
import OSLog
|
||||
import SwiftUI
|
||||
|
||||
struct FavoriteNodeButton: View {
|
||||
|
||||
@EnvironmentObject var accessoryManager: AccessoryManager
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@Environment(\.modelContext) private var context
|
||||
|
||||
@ObservedObject var node: NodeInfoEntity
|
||||
@Bindable var node: NodeInfoEntity
|
||||
@State var isShowingClientBaseConfirmation = false
|
||||
|
||||
var body: some View {
|
||||
|
|
@ -80,15 +80,17 @@ struct FavoriteNodeButton: View {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: Fix preview for SwiftData
|
||||
/*
|
||||
#Preview {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
let node = NodeInfoEntity(context: context)
|
||||
let node = NodeInfoEntity()
|
||||
node.num = 123456789
|
||||
let user = UserEntity(context: context)
|
||||
let user = UserEntity()
|
||||
user.longName = "Test Node"
|
||||
user.shortName = "TN"
|
||||
node.user = user
|
||||
return FavoriteNodeButton(node: node)
|
||||
FavoriteNodeButton(node: node)
|
||||
.environmentObject(AccessoryManager.shared)
|
||||
.environment(\.managedObjectContext, context)
|
||||
.modelContainer(PersistenceController.preview.container)
|
||||
}
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import CoreData
|
||||
import SwiftData
|
||||
import OSLog
|
||||
import SwiftUI
|
||||
|
||||
struct IgnoreNodeButton: View {
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@Environment(\.modelContext) private var context
|
||||
@EnvironmentObject var accessoryManager: AccessoryManager
|
||||
|
||||
@ObservedObject
|
||||
@Bindable
|
||||
var node: NodeInfoEntity
|
||||
|
||||
var body: some View {
|
||||
|
|
@ -52,11 +52,13 @@ struct IgnoreNodeButton: View {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: Fix preview for SwiftData
|
||||
/*
|
||||
#Preview {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
let node = NodeInfoEntity(context: context)
|
||||
let node = NodeInfoEntity()
|
||||
node.num = 123456789
|
||||
return IgnoreNodeButton(node: node)
|
||||
IgnoreNodeButton(node: node)
|
||||
.environmentObject(AccessoryManager.shared)
|
||||
.environment(\.managedObjectContext, context)
|
||||
.modelContainer(PersistenceController.preview.container)
|
||||
}
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import SwiftUI
|
||||
import CoreLocation
|
||||
import CoreData
|
||||
import SwiftData
|
||||
import OSLog
|
||||
|
||||
struct NavigateToButton: View {
|
||||
|
|
@ -21,11 +21,13 @@ struct NavigateToButton: View {
|
|||
}
|
||||
Logger.services.info("Fetching NodeInfoEntity for userNum: \(userNum, privacy: .public)")
|
||||
|
||||
let fetchRequest: NSFetchRequest<NodeInfoEntity> = NSFetchRequest(entityName: "NodeInfoEntity")
|
||||
fetchRequest.predicate = NSPredicate(format: "num == %lld", Int64(userNum))
|
||||
var descriptor = FetchDescriptor<NodeInfoEntity>(
|
||||
predicate: #Predicate<NodeInfoEntity> { $0.num == userNum }
|
||||
)
|
||||
descriptor.fetchLimit = 1
|
||||
|
||||
do {
|
||||
let fetchedNodes = try PersistenceController.shared.container.viewContext.fetch(fetchRequest)
|
||||
let fetchedNodes = try PersistenceController.shared.context.fetch(descriptor)
|
||||
guard let nodeInfo = fetchedNodes.first else {
|
||||
Logger.services.error("NavigateToAction: Node with userNum \(userNum, privacy: .public) not found in Core Data")
|
||||
return
|
||||
|
|
@ -55,14 +57,16 @@ struct NavigateToButton: View {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: Fix preview for SwiftData
|
||||
/*
|
||||
#Preview {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
let node = NodeInfoEntity(context: context)
|
||||
let node = NodeInfoEntity()
|
||||
node.num = 123456789
|
||||
let user = UserEntity(context: context)
|
||||
let user = UserEntity()
|
||||
user.longName = "Test Node"
|
||||
user.shortName = "TN"
|
||||
user.num = 123456789
|
||||
node.user = user
|
||||
return NavigateToButton(node: node)
|
||||
NavigateToButton(node: node)
|
||||
}
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1,20 +1,19 @@
|
|||
import CoreData
|
||||
import SwiftData
|
||||
import OSLog
|
||||
import SwiftUI
|
||||
|
||||
struct NodeAlertsButton: View {
|
||||
var context: NSManagedObjectContext
|
||||
var context: ModelContext
|
||||
|
||||
@ObservedObject
|
||||
@Bindable
|
||||
var node: NodeInfoEntity
|
||||
|
||||
@ObservedObject
|
||||
@Bindable
|
||||
var user: UserEntity
|
||||
|
||||
var body: some View {
|
||||
Button {
|
||||
user.mute = !user.mute
|
||||
context.refresh(node, mergeChanges: true)
|
||||
do {
|
||||
try context.save()
|
||||
} catch {
|
||||
|
|
@ -32,13 +31,15 @@ struct NodeAlertsButton: View {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: Fix preview for SwiftData
|
||||
/*
|
||||
#Preview {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
let node = NodeInfoEntity(context: context)
|
||||
let node = NodeInfoEntity()
|
||||
node.num = 123456789
|
||||
let user = UserEntity(context: context)
|
||||
let user = UserEntity()
|
||||
user.longName = "Test Node"
|
||||
user.shortName = "TN"
|
||||
node.user = user
|
||||
return NodeAlertsButton(context: context, node: node, user: user)
|
||||
NodeAlertsButton(context: context, node: node, user: user)
|
||||
}
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -44,14 +44,16 @@ struct TraceRouteButton: View {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: Fix preview for SwiftData
|
||||
/*
|
||||
#Preview {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
let node = NodeInfoEntity(context: context)
|
||||
let node = NodeInfoEntity()
|
||||
node.num = 123456789
|
||||
let user = UserEntity(context: context)
|
||||
let user = UserEntity()
|
||||
user.longName = "Test Node"
|
||||
user.shortName = "TN"
|
||||
node.user = user
|
||||
return TraceRouteButton(node: node)
|
||||
TraceRouteButton(node: node)
|
||||
.environmentObject(AccessoryManager.shared)
|
||||
}
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
//
|
||||
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
import MapKit
|
||||
import CoreLocation
|
||||
import OSLog
|
||||
|
|
@ -40,15 +41,16 @@ struct MeshMapContent: MapContent {
|
|||
@AppStorage("mapOverlaysEnabled") private var showMapOverlays = false
|
||||
@Binding var enabledOverlayConfigs: Set<UUID>
|
||||
|
||||
@FetchRequest(fetchRequest: PositionEntity.allPositionsFetchRequest(), animation: .easeIn)
|
||||
var positions: FetchedResults<PositionEntity>
|
||||
|
||||
@FetchRequest(fetchRequest: WaypointEntity.allWaypointssFetchRequest(), animation: .none)
|
||||
var waypoints: FetchedResults<WaypointEntity>
|
||||
|
||||
@FetchRequest(sortDescriptors: [NSSortDescriptor(key: "name", ascending: true)],
|
||||
predicate: NSPredicate(format: "enabled == true", ""), animation: .none)
|
||||
private var routes: FetchedResults<RouteEntity>
|
||||
@Query(filter: #Predicate<PositionEntity> { $0.nodePosition != nil && $0.latest == true },
|
||||
sort: \PositionEntity.time, order: .reverse)
|
||||
var positions: [PositionEntity]
|
||||
|
||||
@Query(sort: \WaypointEntity.name, order: .reverse)
|
||||
var waypoints: [WaypointEntity]
|
||||
|
||||
@Query(filter: #Predicate<RouteEntity> { $0.enabled == true },
|
||||
sort: \RouteEntity.name)
|
||||
private var routes: [RouteEntity]
|
||||
|
||||
@MapContentBuilder
|
||||
var positionAnnotations: some MapContent {
|
||||
|
|
@ -57,10 +59,10 @@ struct MeshMapContent: MapContent {
|
|||
if (!showFavorites || (position.nodePosition?.favorite == true)) && !(position.nodePosition?.ignored == true) {
|
||||
let coordinateForNodePin: CLLocationCoordinate2D = if position.isPreciseLocation {
|
||||
// Precise location: place node pin at actual location.
|
||||
position.coordinate
|
||||
position.nodeCoordinate ?? LocationsHandler.DefaultLocation
|
||||
} else {
|
||||
// Imprecise location: fuzz slightly so overlapping nodes are visible and clickable at highest zoom levels.
|
||||
position.fuzzedCoordinate
|
||||
position.fuzzedNodeCoordinate ?? LocationsHandler.DefaultLocation
|
||||
}
|
||||
if 12...15 ~= position.precisionBits || position.precisionBits == 32 {
|
||||
|
||||
|
|
@ -131,7 +133,8 @@ struct MeshMapContent: MapContent {
|
|||
@MapContentBuilder
|
||||
var routeAnnotations: some MapContent {
|
||||
ForEach(routes) { route in
|
||||
if let routeLocations = route.locations, let locations = Array(routeLocations) as? [LocationEntity] {
|
||||
if !route.locations.isEmpty {
|
||||
let locations = route.locations
|
||||
let routeCoords = locations.compactMap {(loc) -> CLLocationCoordinate2D in
|
||||
return loc.locationCoordinate ?? LocationsHandler.DefaultLocation
|
||||
}
|
||||
|
|
@ -167,7 +170,7 @@ struct MeshMapContent: MapContent {
|
|||
var waypointAnnotations: some MapContent {
|
||||
if waypoints.count > 0, showWaypoints, let waypoints = Array(waypoints) as? [WaypointEntity] {
|
||||
ForEach(waypoints, id: \.self) { waypoint in
|
||||
Annotation(waypoint.name ?? "?", coordinate: waypoint.coordinate) {
|
||||
Annotation(waypoint.name ?? "?", coordinate: waypoint.mapCoordinate) {
|
||||
LazyVStack {
|
||||
ZStack {
|
||||
CircleText(text: String(UnicodeScalar(Int(waypoint.icon)) ?? "📍"), color: Color.orange, circleSize: 40)
|
||||
|
|
|
|||
|
|
@ -6,11 +6,11 @@
|
|||
//
|
||||
import SwiftUI
|
||||
import MapKit
|
||||
import CoreData
|
||||
import SwiftData
|
||||
|
||||
struct NodeMapContent: MapContent {
|
||||
|
||||
@ObservedObject var node: NodeInfoEntity
|
||||
@Bindable var node: NodeInfoEntity
|
||||
/// Map State User Defaults
|
||||
@AppStorage("meshMapShowNodeHistory") private var showNodeHistory = false
|
||||
@AppStorage("meshMapShowRouteLines") private var showRouteLines = false
|
||||
|
|
@ -22,7 +22,7 @@ struct NodeMapContent: MapContent {
|
|||
|
||||
@MapContentBuilder
|
||||
var nodeMap: some MapContent {
|
||||
let positionArray = node.positions?.array as? [PositionEntity] ?? []
|
||||
let positionArray = node.positions
|
||||
|
||||
/// Node Color from node.num
|
||||
let nodeColor = UIColor(hex: UInt32(node.num))
|
||||
|
|
@ -43,7 +43,7 @@ struct NodeMapContent: MapContent {
|
|||
let pp = PositionPrecision(rawValue: Int(position.precisionBits))
|
||||
let radius: CLLocationDistance = pp?.precisionMeters ?? 0
|
||||
if radius > 0.0 {
|
||||
MapCircle(center: position.coordinate, radius: radius)
|
||||
MapCircle(center: position.nodeCoordinate ?? LocationsHandler.DefaultLocation, radius: radius)
|
||||
.foregroundStyle(Color(nodeColor).opacity(0.25))
|
||||
.stroke(.white, lineWidth: 2)
|
||||
}
|
||||
|
|
@ -51,7 +51,7 @@ struct NodeMapContent: MapContent {
|
|||
/// Lastest Position Pin
|
||||
if position.latest {
|
||||
/// Node Annotations
|
||||
Annotation(position.latest ? node.user?.shortName ?? "?": "", coordinate: position.coordinate) {
|
||||
Annotation(position.latest ? node.user?.shortName ?? "?": "", coordinate: position.nodeCoordinate ?? LocationsHandler.DefaultLocation) {
|
||||
LazyVStack {
|
||||
ZStack {
|
||||
if pf.contains(.Heading) {
|
||||
|
|
@ -100,7 +100,7 @@ struct NodeMapContent: MapContent {
|
|||
// Having showNodeHistory enabled can be quite slow if there are thousands of history points.
|
||||
if position.latest == false && node.favorite {
|
||||
let headingDegrees = Angle.degrees(Double(position.heading))
|
||||
Annotation("", coordinate: position.coordinate) {
|
||||
Annotation("", coordinate: position.nodeCoordinate ?? LocationsHandler.DefaultLocation) {
|
||||
if pf.contains(.Heading) {
|
||||
Image(uiImage: prerenderedHistoryPointArrowImage)
|
||||
.renderingMode(.original)
|
||||
|
|
@ -154,7 +154,7 @@ struct NodeMapContent: MapContent {
|
|||
|
||||
@MapContentBuilder
|
||||
var body: some MapContent {
|
||||
if node.positions?.count ?? 0 > 0 {
|
||||
if node.positions.count > 0 {
|
||||
nodeMap
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import UniformTypeIdentifiers
|
|||
import OSLog
|
||||
|
||||
struct MapDataFiles: View {
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@Environment(\.modelContext) private var context
|
||||
@EnvironmentObject var accessoryManager: AccessoryManager
|
||||
@ObservedObject private var mapDataManager = MapDataManager.shared
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
//
|
||||
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
import CoreLocation
|
||||
import MapKit
|
||||
|
||||
|
|
@ -30,10 +31,10 @@ private struct NodeMapContentEquatableWrapper<Content: View>: View, Equatable {
|
|||
}
|
||||
|
||||
struct NodeMapSwiftUI: View {
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@Environment(\.modelContext) private var context
|
||||
@EnvironmentObject var accessoryManager: AccessoryManager
|
||||
/// Parameters
|
||||
@ObservedObject var node: NodeInfoEntity
|
||||
@Bindable var node: NodeInfoEntity
|
||||
@State var showUserLocation: Bool = false
|
||||
@State var positions: [PositionEntity] = []
|
||||
/// Map State User Defaults
|
||||
|
|
@ -58,11 +59,8 @@ struct NodeMapSwiftUI: View {
|
|||
|
||||
@State private var mapRegion = MKCoordinateRegion.init()
|
||||
|
||||
@FetchRequest(sortDescriptors: [NSSortDescriptor(key: "name", ascending: false)],
|
||||
predicate: NSPredicate(
|
||||
format: "expire == nil || expire >= %@", Date() as NSDate
|
||||
), animation: .none)
|
||||
private var waypoints: FetchedResults<WaypointEntity>
|
||||
@Query(sort: \WaypointEntity.name, order: .reverse)
|
||||
private var waypoints: [WaypointEntity]
|
||||
|
||||
var body: some View {
|
||||
if node.hasPositions {
|
||||
|
|
@ -78,7 +76,7 @@ struct NodeMapSwiftUI: View {
|
|||
configuredMap
|
||||
}
|
||||
}
|
||||
.navigationBarTitle(String((node.user?.shortName ?? "Unknown".localized) + (" \(node.positions?.count ?? 0) points")), displayMode: .inline)
|
||||
.navigationBarTitle(String((node.user?.shortName ?? "Unknown".localized) + (" \(node.positions.count) points")), displayMode: .inline)
|
||||
.navigationBarItems(trailing:
|
||||
ZStack {
|
||||
ConnectedDevice(
|
||||
|
|
@ -123,8 +121,8 @@ struct NodeMapSwiftUI: View {
|
|||
}
|
||||
|
||||
private var mapContentSignature: NodeMapContentSignature {
|
||||
let positionCount = node.positions?.count ?? 0
|
||||
let lastPositionTime = (node.positions?.lastObject as? PositionEntity)?.time
|
||||
let positionCount = node.positions.count
|
||||
let lastPositionTime = node.positions.last?.time
|
||||
return NodeMapContentSignature(nodeNum: node.num, positionCount: positionCount, lastPositionTime: lastPositionTime, showNodeHistory: showNodeHistory, showRouteLines: showRouteLines, showConvexHull: showConvexHull, favorite: node.favorite)
|
||||
}
|
||||
|
||||
|
|
@ -208,7 +206,7 @@ struct NodeMapSwiftUI: View {
|
|||
.glassButtonStyle()
|
||||
}
|
||||
|
||||
if node.positions?.count ?? 0 > 1 {
|
||||
if node.positions.count > 1 {
|
||||
Button(action: {
|
||||
if isLookingAround {
|
||||
isLookingAround = false
|
||||
|
|
@ -241,15 +239,15 @@ struct NodeMapSwiftUI: View {
|
|||
private func handleNodeChange() {
|
||||
isLookingAround = false
|
||||
isShowingAltitude = false
|
||||
let newMostRecent = node.positions?.lastObject as? PositionEntity
|
||||
if node.positions?.count ?? 0 > 1 {
|
||||
let newMostRecent = node.positions.last
|
||||
if node.positions.count > 1 {
|
||||
position = .automatic
|
||||
} else if let mrCoord = newMostRecent?.coordinate {
|
||||
} else if let mrCoord = newMostRecent?.nodeCoordinate {
|
||||
position = .camera(MapCamera(centerCoordinate: mrCoord, distance: distance, heading: 0, pitch: 0))
|
||||
}
|
||||
if let newMostRecent {
|
||||
if let newMostRecent, let coord = newMostRecent.nodeCoordinate {
|
||||
Task {
|
||||
scene = try? await fetchScene(for: newMostRecent.coordinate)
|
||||
scene = try? await fetchScene(for: coord)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -257,13 +255,13 @@ struct NodeMapSwiftUI: View {
|
|||
private func handleAppear() {
|
||||
UIApplication.shared.isIdleTimerDisabled = true
|
||||
updateMapStyle(for: selectedMapLayer)
|
||||
let mostRecent = node.positions?.lastObject as? PositionEntity
|
||||
if node.positions?.count ?? 0 > 1 {
|
||||
let mostRecent = node.positions.last
|
||||
if node.positions.count > 1 {
|
||||
position = .automatic
|
||||
} else if let mrCoord = mostRecent?.coordinate {
|
||||
} else if let mrCoord = mostRecent?.nodeCoordinate {
|
||||
position = .camera(MapCamera(centerCoordinate: mrCoord, distance: distance, heading: 0, pitch: 0))
|
||||
}
|
||||
if scene == nil, let mrCoord = mostRecent?.coordinate {
|
||||
if scene == nil, let mrCoord = mostRecent?.nodeCoordinate {
|
||||
Task {
|
||||
scene = try? await fetchScene(for: mrCoord)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,16 +16,12 @@ struct PositionAltitude {
|
|||
|
||||
struct PositionAltitudeChart: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@ObservedObject var node: NodeInfoEntity
|
||||
@Bindable var node: NodeInfoEntity
|
||||
@State private var lineWidth = 2.0
|
||||
|
||||
var data: [PositionAltitude] {
|
||||
let fiveYearsAgo = Calendar.current.date(byAdding: .year, value: -5, to: Date())
|
||||
guard let nodePositions = node.positions,
|
||||
let positions = Array(nodePositions) as? [PositionEntity]
|
||||
else {
|
||||
return []
|
||||
}
|
||||
let positions = node.positions
|
||||
|
||||
let filteredPositions = positions.filter({$0.time != nil && ($0.time ?? fiveYearsAgo!) > fiveYearsAgo!})
|
||||
return filteredPositions.map {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import MapKit
|
|||
struct PositionPopover: View {
|
||||
|
||||
@ObservedObject var locationsHandler = LocationsHandler.shared
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@Environment(\.modelContext) private var context
|
||||
@EnvironmentObject var appState: AppState
|
||||
private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom }
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
|
@ -81,7 +81,7 @@ struct PositionPopover: View {
|
|||
.padding(.bottom, 5)
|
||||
/// Coordinate
|
||||
Label {
|
||||
Text("\(String(format: "%.6f", position.coordinate.latitude)), \(String(format: "%.6f", position.coordinate.longitude))")
|
||||
Text("\(String(format: "%.6f", position.nodeCoordinate?.latitude ?? 0)), \(String(format: "%.6f", position.nodeCoordinate?.longitude ?? 0))")
|
||||
.textSelection(.enabled)
|
||||
.foregroundColor(.primary)
|
||||
.font(idiom == .phone ? .callout : .body)
|
||||
|
|
@ -169,7 +169,8 @@ struct PositionPopover: View {
|
|||
if let lastLocation = locationsHandler.locationsArray.last {
|
||||
/// Distance
|
||||
if lastLocation.distance(from: CLLocation(latitude: LocationsHandler.DefaultLocation.latitude, longitude: LocationsHandler.DefaultLocation.longitude)) > 0.0 {
|
||||
let metersAway = position.coordinate.distance(from: CLLocationCoordinate2D(latitude: lastLocation.coordinate.latitude, longitude: lastLocation.coordinate.longitude))
|
||||
let posCoord = position.nodeCoordinate ?? LocationsHandler.DefaultLocation
|
||||
let metersAway = posCoord.distance(from: CLLocationCoordinate2D(latitude: lastLocation.coordinate.latitude, longitude: lastLocation.coordinate.longitude))
|
||||
Label {
|
||||
Text("Distance".localized + ": \(distanceFormatter.string(fromDistance: Double(metersAway)))")
|
||||
.foregroundColor(.primary)
|
||||
|
|
@ -263,7 +264,7 @@ struct PositionPopover: View {
|
|||
.presentationBackgroundInteraction(.enabled(upThrough: .large))
|
||||
.navigationDestination(isPresented: $navigateToCompass) {
|
||||
CompassView(
|
||||
waypointLocation: position.coordinate,
|
||||
waypointLocation: position.nodeCoordinate ?? LocationsHandler.DefaultLocation,
|
||||
waypointLongName: position.nodePosition?.user?.longName ?? "Unknown node",
|
||||
waypointShortName: position.nodePosition?.user?.shortName ?? "???",
|
||||
color: (position.nodePosition?.user?.num != nil && position.nodePosition?.user?.num != 0) ? Color(UIColor(hex: UInt32(position.nodePosition!.user!.num))) : .orange
|
||||
|
|
|
|||
|
|
@ -10,12 +10,12 @@ import MapKit
|
|||
import MeshtasticProtobufs
|
||||
import OSLog
|
||||
import SwiftUI
|
||||
import CoreData
|
||||
import SwiftData
|
||||
|
||||
struct WaypointForm: View {
|
||||
|
||||
@EnvironmentObject var accessoryManager: AccessoryManager
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@Environment(\.modelContext) private var context
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@State var waypoint: WaypointEntity
|
||||
let distanceFormatter = MKDistanceFormatter()
|
||||
|
|
@ -43,20 +43,20 @@ struct WaypointForm: View {
|
|||
Divider()
|
||||
Form {
|
||||
if let cl = LocationsHandler.currentLocation {
|
||||
let distance = CLLocation(latitude: cl.latitude, longitude: cl.longitude).distance(from: CLLocation(latitude: waypoint.coordinate.latitude, longitude: waypoint.coordinate.longitude ))
|
||||
let distance = CLLocation(latitude: cl.latitude, longitude: cl.longitude).distance(from: CLLocation(latitude: waypoint.mapCoordinate.latitude, longitude: waypoint.mapCoordinate.longitude ))
|
||||
Section(header: Text("Coordinate") ) {
|
||||
HStack {
|
||||
Text("Location:")
|
||||
.foregroundColor(.secondary)
|
||||
Text("\(String(format: "%.5f", waypoint.coordinate.latitude) + "," + String(format: "%.5f", waypoint.coordinate.longitude))")
|
||||
Text("\(String(format: "%.5f", waypoint.mapCoordinate.latitude) + "," + String(format: "%.5f", waypoint.mapCoordinate.longitude))")
|
||||
.textSelection(.enabled)
|
||||
.foregroundColor(.secondary)
|
||||
.font(.caption)
|
||||
|
||||
}
|
||||
Button {
|
||||
waypoint.coordinate.longitude = cl.longitude
|
||||
waypoint.coordinate.latitude = cl.latitude
|
||||
waypoint.longitudeI = Int32(cl.longitude * 1e7)
|
||||
waypoint.latitudeI = Int32(cl.latitude * 1e7)
|
||||
} label: {
|
||||
HStack {
|
||||
Text("Use my Location")
|
||||
|
|
@ -65,7 +65,7 @@ struct WaypointForm: View {
|
|||
}
|
||||
.accessibilityLabel("Set to current location")
|
||||
HStack {
|
||||
if waypoint.coordinate.latitude != 0 && waypoint.coordinate.longitude != 0 {
|
||||
if waypoint.mapCoordinate.latitude != 0 && waypoint.mapCoordinate.longitude != 0 {
|
||||
DistanceText(meters: distance)
|
||||
.foregroundColor(Color.gray)
|
||||
}
|
||||
|
|
@ -288,7 +288,7 @@ struct WaypointForm: View {
|
|||
Text(waypoint.name ?? "?")
|
||||
.font(.largeTitle)
|
||||
Spacer()
|
||||
if waypoint.locked > 0 && waypoint.locked != UInt32(accessoryManager.activeDeviceNum ?? 0) {
|
||||
if waypoint.locked {
|
||||
Image(systemName: "lock.fill")
|
||||
.font(.largeTitle)
|
||||
} else {
|
||||
|
|
@ -359,7 +359,7 @@ struct WaypointForm: View {
|
|||
Label {
|
||||
Text("Coordinates:")
|
||||
.foregroundColor(.primary)
|
||||
Text("\(String(format: "%.6f", waypoint.coordinate.latitude)), \(String(format: "%.6f", waypoint.coordinate.longitude))")
|
||||
Text("\(String(format: "%.6f", waypoint.mapCoordinate.latitude)), \(String(format: "%.6f", waypoint.mapCoordinate.longitude))")
|
||||
.textSelection(.enabled)
|
||||
.foregroundColor(.secondary)
|
||||
.font(.caption2)
|
||||
|
|
@ -369,7 +369,7 @@ struct WaypointForm: View {
|
|||
.padding(.bottom)
|
||||
// Drop Maps Pin
|
||||
Button(action: {
|
||||
if let url = URL(string: "http://maps.apple.com/?ll=\(waypoint.coordinate.latitude),\(waypoint.coordinate.longitude)&q=\(waypoint.name ?? "Dropped Pin")") {
|
||||
if let url = URL(string: "http://maps.apple.com/?ll=\(waypoint.mapCoordinate.latitude),\(waypoint.mapCoordinate.longitude)&q=\(waypoint.name ?? "Dropped Pin")") {
|
||||
UIApplication.shared.open(url)
|
||||
}
|
||||
}) {
|
||||
|
|
@ -410,7 +410,7 @@ struct WaypointForm: View {
|
|||
/// Distance
|
||||
if let cl = LocationsHandler.currentLocation {
|
||||
if cl.distance(from: cl) > 0.0 {
|
||||
let metersAway = waypoint.coordinate.distance(from: cl)
|
||||
let metersAway = waypoint.mapCoordinate.distance(from: cl)
|
||||
Label {
|
||||
Text("Distance".localized + ": \(distanceFormatter.string(fromDistance: Double(metersAway)))")
|
||||
.foregroundColor(.primary)
|
||||
|
|
@ -479,9 +479,8 @@ struct WaypointForm: View {
|
|||
} else {
|
||||
expires = false
|
||||
}
|
||||
if waypoint.locked > 0 {
|
||||
if waypoint.locked {
|
||||
locked = true
|
||||
lockedTo = waypoint.locked
|
||||
}
|
||||
} else {
|
||||
name = ""
|
||||
|
|
@ -490,8 +489,8 @@ struct WaypointForm: View {
|
|||
expires = false
|
||||
expire = Date.now.addingTimeInterval(60 * 480)
|
||||
icon = "📍"
|
||||
latitude = waypoint.coordinate.latitude
|
||||
longitude = waypoint.coordinate.longitude
|
||||
latitude = waypoint.mapCoordinate.latitude
|
||||
longitude = waypoint.mapCoordinate.longitude
|
||||
}
|
||||
}
|
||||
.presentationBackgroundInteraction(.enabled(upThrough: .fraction(0.85)))
|
||||
|
|
@ -501,12 +500,14 @@ struct WaypointForm: View {
|
|||
private func fetchNodeInfo() async {
|
||||
// --- Fetch createdBy node ---
|
||||
if waypoint.createdBy != 0 {
|
||||
let createdByFetch: NSFetchRequest<NodeInfoEntity> = NodeInfoEntity.fetchRequest()
|
||||
createdByFetch.predicate = NSPredicate(format: "num == %lld", Int64(waypoint.createdBy))
|
||||
createdByFetch.fetchLimit = 1
|
||||
let createdByNum = Int64(waypoint.createdBy)
|
||||
var createdByDescriptor = FetchDescriptor<NodeInfoEntity>(
|
||||
predicate: #Predicate<NodeInfoEntity> { $0.num == createdByNum }
|
||||
)
|
||||
createdByDescriptor.fetchLimit = 1
|
||||
|
||||
do {
|
||||
let nodes = try context.fetch(createdByFetch)
|
||||
let nodes = try context.fetch(createdByDescriptor)
|
||||
createdByNode = nodes.first
|
||||
} catch {
|
||||
Logger.services.warning("Error fetching createdBy node: \(error.localizedDescription)")
|
||||
|
|
@ -516,12 +517,14 @@ struct WaypointForm: View {
|
|||
// --- Fetch lastUpdatedBy node (only if different from createdBy) ---
|
||||
if waypoint.lastUpdatedBy != 0,
|
||||
waypoint.lastUpdatedBy != waypoint.createdBy {
|
||||
let updatedByFetch: NSFetchRequest<NodeInfoEntity> = NodeInfoEntity.fetchRequest()
|
||||
updatedByFetch.predicate = NSPredicate(format: "num == %lld", Int64(waypoint.lastUpdatedBy))
|
||||
updatedByFetch.fetchLimit = 1
|
||||
let updatedByNum = Int64(waypoint.lastUpdatedBy)
|
||||
var updatedByDescriptor = FetchDescriptor<NodeInfoEntity>(
|
||||
predicate: #Predicate<NodeInfoEntity> { $0.num == updatedByNum }
|
||||
)
|
||||
updatedByDescriptor.fetchLimit = 1
|
||||
|
||||
do {
|
||||
let nodes = try context.fetch(updatedByFetch)
|
||||
let nodes = try context.fetch(updatedByDescriptor)
|
||||
lastUpdatedByNode = nodes.first
|
||||
} catch {
|
||||
Logger.services.warning("Error fetching lastUpdatedBy node: \(error.localizedDescription)")
|
||||
|
|
|
|||
|
|
@ -19,13 +19,13 @@ struct NodeDetail: View {
|
|||
var modemPreset: ModemPresets = ModemPresets(
|
||||
rawValue: UserDefaults.modemPreset
|
||||
) ?? ModemPresets.longFast
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@Environment(\.modelContext) private var context
|
||||
@EnvironmentObject var accessoryManager: AccessoryManager
|
||||
@State private var showingShutdownConfirm: Bool = false
|
||||
@State private var showingRebootConfirm: Bool = false
|
||||
@State private var dateFormatRelative: Bool = true
|
||||
var connectedNode: NodeInfoEntity?
|
||||
@ObservedObject var node: NodeInfoEntity
|
||||
@Bindable var node: NodeInfoEntity
|
||||
@State private var environmentSectionHeight: CGFloat = 0
|
||||
@State var showingCompassSheet = false
|
||||
|
||||
|
|
@ -69,7 +69,7 @@ struct NodeDetail: View {
|
|||
}
|
||||
.accessibilityElement(children: .combine)
|
||||
}
|
||||
if node.telemetries?.count ?? 0 > 0 {
|
||||
if node.telemetries.count > 0 {
|
||||
Spacer()
|
||||
BatteryGauge(node: node)
|
||||
}
|
||||
|
|
@ -134,9 +134,7 @@ struct NodeDetail: View {
|
|||
}
|
||||
Spacer()
|
||||
Button(action: {
|
||||
context.perform {
|
||||
UIPasteboard.general.string = publicKey
|
||||
}
|
||||
UIPasteboard.general.string = publicKey
|
||||
}) {
|
||||
HStack {
|
||||
Image(systemName: "key.horizontal.fill")
|
||||
|
|
@ -185,7 +183,7 @@ struct NodeDetail: View {
|
|||
}
|
||||
.accessibilityElement(children: .combine)
|
||||
}
|
||||
if let dm = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0")).lastObject as? TelemetryEntity, let uptimeSeconds = dm.uptimeSeconds {
|
||||
if let dm = node.telemetries.filter({ $0.metricsType == 0 }).last, let uptimeSeconds = dm.uptimeSeconds {
|
||||
HStack {
|
||||
Label {
|
||||
Text("\("Uptime".localized)")
|
||||
|
|
@ -401,7 +399,7 @@ struct NodeDetail: View {
|
|||
.symbolRenderingMode(.multicolor)
|
||||
}
|
||||
}
|
||||
.disabled(node.traceRoutes?.count ?? 0 == 0)
|
||||
.disabled(node.traceRoutes.count == 0)
|
||||
NavigationLink {
|
||||
PowerMetricsLog(node: node)
|
||||
} label: {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import MapKit
|
|||
|
||||
struct NodeInfoItem: View {
|
||||
|
||||
@ObservedObject var node: NodeInfoEntity
|
||||
@Bindable var node: NodeInfoEntity
|
||||
|
||||
var body: some View {
|
||||
|
||||
|
|
@ -48,10 +48,10 @@ struct NodeInfoItem: View {
|
|||
.font(.caption2)
|
||||
}
|
||||
}
|
||||
let deviceMetrics = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0"))
|
||||
if deviceMetrics?.count ?? 0 >= 1 {
|
||||
let deviceMetrics = node.telemetries.filter { $0.metricsType == 0 }
|
||||
if deviceMetrics.count >= 1 {
|
||||
Divider()
|
||||
let mostRecent = deviceMetrics?.lastObject as? TelemetryEntity
|
||||
let mostRecent = deviceMetrics.last
|
||||
VStack(alignment: .center) {
|
||||
BatteryGauge(batteryLevel: Double(mostRecent?.batteryLevel ?? 0))
|
||||
if mostRecent?.voltage ?? 0 > 0 {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import MapKit
|
|||
|
||||
struct NodeInfoItem: View {
|
||||
|
||||
@ObservedObject var node: NodeInfoEntity
|
||||
@Bindable var node: NodeInfoEntity
|
||||
@State private var currentDevice: DeviceHardware?
|
||||
|
||||
var body: some View {
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ struct NodeListItem: View {
|
|||
return desc
|
||||
}
|
||||
|
||||
@ObservedObject var node: NodeInfoEntity
|
||||
@Bindable var node: NodeInfoEntity
|
||||
var isDirectlyConnected: Bool
|
||||
var connectedNode: Int64
|
||||
var modemPreset: ModemPresets = ModemPresets(rawValue: UserDefaults.modemPreset) ?? ModemPresets.longFast
|
||||
|
|
@ -109,7 +109,7 @@ struct NodeListItem: View {
|
|||
}
|
||||
|
||||
var locationData: (PositionEntity, CLLocation)? {
|
||||
guard let lastPostion = node.positions?.lastObject as? PositionEntity else {
|
||||
guard let lastPostion = node.positions.last else {
|
||||
return nil
|
||||
}
|
||||
guard let currentLocation = LocationsHandler.shared.locationsArray.last else {
|
||||
|
|
@ -172,7 +172,7 @@ struct NodeListItem: View {
|
|||
text: "Store & Forward".localized)
|
||||
}
|
||||
|
||||
if node.positions?.count ?? 0 > 0 && connectedNode != node.num {
|
||||
if node.positions.count > 0 && connectedNode != node.num {
|
||||
HStack {
|
||||
if let (lastPostion, myCoord) = locationData {
|
||||
let nodeCoord = CLLocation(latitude: lastPostion.nodeCoordinate!.latitude, longitude: lastPostion.nodeCoordinate!.longitude)
|
||||
|
|
@ -298,9 +298,8 @@ struct IconAndText: View {
|
|||
IconAndText(systemName: "antenna.radiowaves.left.and.right.circle.fill", text: "foo")
|
||||
IconAndText(systemName: "antenna.radiowaves.left.and.right.circle", text: "bar")
|
||||
NodeListItem(node: {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
let nodeInfo = NodeInfoEntity(context: context)
|
||||
let user = UserEntity(context: context)
|
||||
let nodeInfo = NodeInfoEntity()
|
||||
let user = UserEntity()
|
||||
user.longName = "Test User"
|
||||
user.shortName = "TU"
|
||||
nodeInfo.user = user
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import CoreImage.CIFilterBuiltins
|
|||
#if canImport(UIKit)
|
||||
import UIKit
|
||||
#endif
|
||||
import CoreData
|
||||
import SwiftData
|
||||
import MeshtasticProtobufs
|
||||
import OSLog
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
//
|
||||
|
||||
import SwiftUI
|
||||
import CoreData
|
||||
import SwiftData
|
||||
import CoreLocation
|
||||
import Foundation
|
||||
import OSLog
|
||||
|
|
@ -14,7 +14,7 @@ import MapKit
|
|||
|
||||
struct MeshMap: View {
|
||||
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@Environment(\.modelContext) private var context
|
||||
@EnvironmentObject var accessoryManager: AccessoryManager
|
||||
|
||||
@ObservedObject
|
||||
|
|
@ -101,7 +101,7 @@ struct MeshMap: View {
|
|||
centerMapAt(coordinate: coordinate)
|
||||
|
||||
newWaypointCoord = coordinate
|
||||
editingWaypoint = WaypointEntity(context: context)
|
||||
editingWaypoint = WaypointEntity()
|
||||
editingWaypoint!.name = "Waypoint Pin"
|
||||
editingWaypoint!.expire = Date.now.addingTimeInterval(60 * 480)
|
||||
editingWaypoint!.latitudeI = Int32((newWaypointCoord?.latitude ?? 0) * 1e7)
|
||||
|
|
|
|||
|
|
@ -7,14 +7,14 @@
|
|||
import SwiftUI
|
||||
import CoreLocation
|
||||
import OSLog
|
||||
import CoreData
|
||||
import SwiftData
|
||||
import Foundation
|
||||
|
||||
struct NodeList: View {
|
||||
/// Debounce delay for node selection changes (100ms)
|
||||
private static let nodeSelectionDebounceNs: UInt64 = 100_000_000
|
||||
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@Environment(\.modelContext) private var context
|
||||
@EnvironmentObject var accessoryManager: AccessoryManager
|
||||
@StateObject var router: Router
|
||||
@State private var selectedNode: NodeInfoEntity?
|
||||
|
|
@ -27,6 +27,7 @@ struct NodeList: View {
|
|||
@State private var shareContactNode: NodeInfoEntity?
|
||||
@StateObject var filters = NodeFilterParameters()
|
||||
@State var isEditingFilters = false
|
||||
@State private var filteredNodeCount: Int = 0
|
||||
@SceneStorage("selectedDetailView") var selectedDetailView: String?
|
||||
|
||||
var connectedNode: NodeInfoEntity? {
|
||||
|
|
@ -45,7 +46,8 @@ struct NodeList: View {
|
|||
connectedNode: connectedNode,
|
||||
isPresentingDeleteNodeAlert: $isPresentingDeleteNodeAlert,
|
||||
deleteNodeId: $deleteNodeId,
|
||||
shareContactNode: $shareContactNode
|
||||
shareContactNode: $shareContactNode,
|
||||
filteredNodeCount: $filteredNodeCount
|
||||
)
|
||||
.sheet(isPresented: $isEditingFilters) {
|
||||
NodeListFilter(
|
||||
|
|
@ -72,7 +74,7 @@ struct NodeList: View {
|
|||
.searchable(text: $filters.searchText, placement: .navigationBarDrawer(displayMode: .always), prompt: "Find a node")
|
||||
.autocorrectionDisabled(true)
|
||||
.scrollDismissesKeyboard(.immediately)
|
||||
.navigationTitle(String.localizedStringWithFormat("Nodes (%@)".localized, String(getNodeCount())))
|
||||
.navigationTitle(String.localizedStringWithFormat("Nodes (%@)".localized, String(filteredNodeCount)))
|
||||
.listStyle(.plain)
|
||||
.alert("Position Exchange Requested", isPresented: $isPresentingPositionSentAlert) {
|
||||
Button("OK") { }.keyboardShortcut(.defaultAction)
|
||||
|
|
@ -152,12 +154,6 @@ struct NodeList: View {
|
|||
}
|
||||
}
|
||||
|
||||
// Helper to get the count of nodes for the navigation title
|
||||
private func getNodeCount() -> Int {
|
||||
let request: NSFetchRequest<NodeInfoEntity> = NodeInfoEntity.fetchRequest()
|
||||
request.predicate = filters.buildPredicate()
|
||||
return (try? context.count(for: request)) ?? 0
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
|
@ -166,8 +162,9 @@ struct NodeList: View {
|
|||
//
|
||||
fileprivate struct FilteredNodeList: View {
|
||||
@EnvironmentObject var accessoryManager: AccessoryManager
|
||||
@FetchRequest private var nodes: FetchedResults<NodeInfoEntity>
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@Query(sort: \NodeInfoEntity.lastHeard, order: .reverse)
|
||||
private var allNodes: [NodeInfoEntity]
|
||||
@Environment(\.modelContext) private var context
|
||||
var router: Router
|
||||
|
||||
@Binding var selectedNode: NodeInfoEntity?
|
||||
|
|
@ -175,6 +172,8 @@ fileprivate struct FilteredNodeList: View {
|
|||
@Binding var isPresentingDeleteNodeAlert: Bool
|
||||
@Binding var deleteNodeId: Int64
|
||||
@Binding var shareContactNode: NodeInfoEntity?
|
||||
@Binding var filteredNodeCount: Int
|
||||
private var filters: NodeFilterParameters
|
||||
|
||||
// The initializer for the FetchRequest
|
||||
init(
|
||||
|
|
@ -184,26 +183,21 @@ fileprivate struct FilteredNodeList: View {
|
|||
connectedNode: NodeInfoEntity?,
|
||||
isPresentingDeleteNodeAlert: Binding<Bool>,
|
||||
deleteNodeId: Binding<Int64>,
|
||||
shareContactNode: Binding<NodeInfoEntity?>
|
||||
shareContactNode: Binding<NodeInfoEntity?>,
|
||||
filteredNodeCount: Binding<Int>
|
||||
) {
|
||||
self.router = router
|
||||
let request: NSFetchRequest<NodeInfoEntity> = NodeInfoEntity.fetchRequest()
|
||||
request.sortDescriptors = [
|
||||
NSSortDescriptor(key: "ignored", ascending: true),
|
||||
NSSortDescriptor(key: "favorite", ascending: false),
|
||||
NSSortDescriptor(key: "lastHeard", ascending: false),
|
||||
NSSortDescriptor(key: "user.longName", ascending: true)
|
||||
]
|
||||
request.predicate = withFilters.buildPredicate()
|
||||
request.fetchBatchSize = 50
|
||||
request.relationshipKeyPathsForPrefetching = ["user"]
|
||||
self._nodes = FetchRequest(fetchRequest: request)
|
||||
|
||||
self.filters = withFilters
|
||||
self._selectedNode = selectedNode
|
||||
self.connectedNode = connectedNode
|
||||
self._isPresentingDeleteNodeAlert = isPresentingDeleteNodeAlert
|
||||
self._deleteNodeId = deleteNodeId
|
||||
self._shareContactNode = shareContactNode
|
||||
self._filteredNodeCount = filteredNodeCount
|
||||
}
|
||||
|
||||
private var nodes: [NodeInfoEntity] {
|
||||
allNodes.filter { filters.matches(node: $0) }
|
||||
}
|
||||
|
||||
// The body of the view
|
||||
|
|
@ -243,9 +237,11 @@ fileprivate struct FilteredNodeList: View {
|
|||
}
|
||||
.onAppear {
|
||||
router.updateNodeIndex(from: nodes)
|
||||
filteredNodeCount = nodes.count
|
||||
}
|
||||
.onChange(of: nodes.count) { _, _ in
|
||||
.onChange(of: nodes.count) { _, newCount in
|
||||
router.updateNodeIndex(from: nodes)
|
||||
filteredNodeCount = newCount
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -330,6 +326,73 @@ fileprivate struct FilteredNodeList: View {
|
|||
//
|
||||
|
||||
fileprivate extension NodeFilterParameters {
|
||||
func matches(node: NodeInfoEntity) -> Bool {
|
||||
// Search text
|
||||
if !searchText.isEmpty {
|
||||
let text = searchText.lowercased()
|
||||
let fields = [node.user?.userId, node.user?.numString, node.user?.hwModel,
|
||||
node.user?.hwDisplayName, node.user?.longName, node.user?.shortName]
|
||||
let matchesSearch = fields.compactMap { $0?.lowercased() }.contains { $0.contains(text) }
|
||||
if !matchesSearch { return false }
|
||||
}
|
||||
// Favorite
|
||||
if isFavorite && !node.favorite { return false }
|
||||
// Via Lora/MQTT
|
||||
if viaLora && !viaMqtt && node.viaMqtt { return false }
|
||||
if !viaLora && viaMqtt && !node.viaMqtt { return false }
|
||||
// Roles
|
||||
if roleFilter && !deviceRoles.isEmpty {
|
||||
let userRole = Int(node.user?.role ?? 0)
|
||||
if !deviceRoles.contains(userRole) { return false }
|
||||
}
|
||||
// Hops Away
|
||||
if hopsAway == 0 && node.hopsAway != 0 { return false }
|
||||
if hopsAway > 0 && (node.hopsAway <= 0 || node.hopsAway > Int32(hopsAway)) { return false }
|
||||
// Online
|
||||
if isOnline {
|
||||
let twoHoursAgo = Calendar.current.date(byAdding: .minute, value: -120, to: Date()) ?? Date.distantPast
|
||||
if let lastHeard = node.lastHeard, lastHeard < twoHoursAgo { return false }
|
||||
if node.lastHeard == nil { return false }
|
||||
}
|
||||
// Encrypted
|
||||
if isPkiEncrypted && node.user?.pkiEncrypted != true { return false }
|
||||
// Ignored
|
||||
if isIgnored {
|
||||
if !node.ignored { return false }
|
||||
} else {
|
||||
if node.ignored { return false }
|
||||
}
|
||||
// Environment
|
||||
if isEnvironment {
|
||||
let hasEnvTelemetry = (node.telemetries ?? []).contains { $0.metricsType == 1 }
|
||||
if !hasEnvTelemetry { return false }
|
||||
}
|
||||
// Distance
|
||||
if distanceFilter {
|
||||
if let poi = LocationsHandler.currentLocation,
|
||||
poi.latitude != LocationsHandler.DefaultLocation.latitude,
|
||||
poi.longitude != LocationsHandler.DefaultLocation.longitude {
|
||||
let d = maxDistance * 1.1
|
||||
let r: Double = 6371009
|
||||
let meanLat = poi.latitude * .pi / 180
|
||||
let deltaLat = d / r * 180 / .pi
|
||||
let deltaLon = d / (r * cos(meanLat)) * 180 / .pi
|
||||
let minLat = poi.latitude - deltaLat
|
||||
let maxLat = poi.latitude + deltaLat
|
||||
let minLon = poi.longitude - deltaLon
|
||||
let maxLon = poi.longitude + deltaLon
|
||||
let hasNearbyPosition = (node.positions ?? []).contains { pos in
|
||||
guard pos.latest else { return false }
|
||||
let lon = Double(pos.longitudeI) / 1e7
|
||||
let lat = Double(pos.latitudeI) / 1e7
|
||||
return lon >= minLon && lon <= maxLon && lat >= minLat && lat <= maxLat
|
||||
}
|
||||
if !hasNearbyPosition { return false }
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func buildPredicate() -> NSPredicate? {
|
||||
var predicates: [NSPredicate] = []
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import OSLog
|
|||
|
||||
struct PaxCounterLog: View {
|
||||
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@Environment(\.modelContext) private var context
|
||||
@EnvironmentObject var accessoryManager: AccessoryManager
|
||||
|
||||
@State private var isPresentingClearLogConfirm: Bool = false
|
||||
|
|
@ -21,66 +21,70 @@ struct PaxCounterLog: View {
|
|||
@State private var bleChartColor: Color = .blue
|
||||
@State private var wifiChartColor: Color = .orange
|
||||
@State private var paxChartColor: Color = .green
|
||||
@ObservedObject var node: NodeInfoEntity
|
||||
@Bindable var node: NodeInfoEntity
|
||||
|
||||
@ViewBuilder
|
||||
private func paxChart(chartData: [PaxCounterEntity], maxValue: Int32) -> some View {
|
||||
Chart {
|
||||
ForEach(chartData, id: \.self) { point in
|
||||
Plot {
|
||||
PointMark(
|
||||
x: .value("x", point.time!),
|
||||
y: .value("y", (point.wifi + point.ble))
|
||||
)
|
||||
}
|
||||
.accessibilityLabel("Total PAX")
|
||||
.accessibilityValue("X: \(point.time!), Y: \(point.wifi + point.ble)")
|
||||
.foregroundStyle(paxChartColor)
|
||||
.interpolationMethod(.cardinal)
|
||||
|
||||
Plot {
|
||||
PointMark(
|
||||
x: .value("x", point.time!),
|
||||
y: .value("y", point.wifi)
|
||||
)
|
||||
}
|
||||
.accessibilityLabel("WiFi")
|
||||
.accessibilityValue("X: \(point.time!), Y: \(point.wifi)")
|
||||
.foregroundStyle(wifiChartColor)
|
||||
|
||||
Plot {
|
||||
PointMark(
|
||||
x: .value("x", point.time!),
|
||||
y: .value("y", point.ble)
|
||||
)
|
||||
}
|
||||
.accessibilityLabel("BLE")
|
||||
.accessibilityValue("X: \(point.time!), Y: \(point.ble)")
|
||||
.foregroundStyle(bleChartColor)
|
||||
}
|
||||
}
|
||||
.chartXAxis(content: {
|
||||
AxisMarks(position: .top)
|
||||
})
|
||||
.chartXAxis(.automatic)
|
||||
.chartYScale(domain: 0...maxValue)
|
||||
.chartForegroundStyleScale([
|
||||
"BLE".localized: .blue,
|
||||
"WiFi".localized: .orange,
|
||||
"Total PAX".localized: .green
|
||||
])
|
||||
.chartLegend(position: .automatic, alignment: .bottom)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
if node.hasPax {
|
||||
|
||||
let oneWeekAgo = Calendar.current.date(byAdding: .day, value: -7, to: Date())
|
||||
let pax = node.pax?.reversed() as? [PaxCounterEntity] ?? []
|
||||
let pax = Array(node.pax.reversed())
|
||||
let chartData = pax
|
||||
.filter { $0.time != nil && $0.time! >= oneWeekAgo! }
|
||||
.sorted { $0.time! < $1.time! }
|
||||
let maxValue = (chartData.map { $0.wifi }.max() ?? 0) + (chartData.map { $0.ble }.max() ?? 0) + 5
|
||||
if chartData.count > 0 {
|
||||
GroupBox(label: Label("\(pax.count) Readings Total", systemImage: "chart.xyaxis.line")) {
|
||||
|
||||
Chart {
|
||||
ForEach(chartData, id: \.self) { point in
|
||||
Plot {
|
||||
PointMark(
|
||||
x: .value("x", point.time!),
|
||||
y: .value("y", (point.wifi + point.ble))
|
||||
)
|
||||
}
|
||||
.accessibilityLabel("Total PAX")
|
||||
.accessibilityValue("X: \(point.time!), Y: \(point.wifi + point.ble)")
|
||||
.foregroundStyle(paxChartColor)
|
||||
.interpolationMethod(.cardinal)
|
||||
|
||||
Plot {
|
||||
PointMark(
|
||||
x: .value("x", point.time!),
|
||||
y: .value("y", point.wifi)
|
||||
)
|
||||
}
|
||||
.accessibilityLabel("WiFi")
|
||||
.accessibilityValue("X: \(point.time!), Y: \(point.wifi)")
|
||||
.foregroundStyle(wifiChartColor)
|
||||
|
||||
Plot {
|
||||
PointMark(
|
||||
x: .value("x", point.time!),
|
||||
y: .value("y", point.ble)
|
||||
)
|
||||
}
|
||||
.accessibilityLabel("BLE")
|
||||
.accessibilityValue("X: \(point.time!), Y: \(point.ble)")
|
||||
.foregroundStyle(bleChartColor)
|
||||
}
|
||||
}
|
||||
.chartXAxis(content: {
|
||||
AxisMarks(position: .top)
|
||||
})
|
||||
.chartXAxis(.automatic)
|
||||
.chartYScale(domain: 0...maxValue)
|
||||
.chartForegroundStyleScale([
|
||||
"BLE".localized: .blue,
|
||||
"WiFi".localized: .orange,
|
||||
"Total PAX".localized: .green
|
||||
])
|
||||
.chartLegend(position: .automatic, alignment: .bottom)
|
||||
paxChart(chartData: chartData, maxValue: maxValue)
|
||||
}
|
||||
.frame(minHeight: 250)
|
||||
}
|
||||
|
|
@ -225,15 +229,17 @@ struct PaxCounterLog: View {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: Fix preview for SwiftData
|
||||
/*
|
||||
#Preview {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
let node = NodeInfoEntity(context: context)
|
||||
let node = NodeInfoEntity()
|
||||
node.num = 123456789
|
||||
let user = UserEntity(context: context)
|
||||
let user = UserEntity()
|
||||
user.longName = "Test Node"
|
||||
user.shortName = "TN"
|
||||
node.user = user
|
||||
return PaxCounterLog(node: node)
|
||||
PaxCounterLog(node: node)
|
||||
.environmentObject(AccessoryManager.shared)
|
||||
.environment(\.managedObjectContext, context)
|
||||
.modelContainer(PersistenceController.preview.container)
|
||||
}
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import SwiftUI
|
|||
import OSLog
|
||||
|
||||
struct PositionLog: View {
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@Environment(\.modelContext) private var context
|
||||
@EnvironmentObject var accessoryManager: AccessoryManager
|
||||
@Environment(\.verticalSizeClass) var verticalSizeClass: UserInterfaceSizeClass?
|
||||
@Environment(\.horizontalSizeClass) var horizontalSizeClass: UserInterfaceSizeClass?
|
||||
|
|
@ -18,7 +18,7 @@ struct PositionLog: View {
|
|||
}
|
||||
@State var isExporting = false
|
||||
@State var exportString = ""
|
||||
@ObservedObject var node: NodeInfoEntity
|
||||
@Bindable var node: NodeInfoEntity
|
||||
@State private var isPresentingClearLogConfirm = false
|
||||
@State private var sortOrder = [KeyPathComparator(\PositionEntity.time)]
|
||||
|
||||
|
|
@ -29,7 +29,7 @@ struct PositionLog: View {
|
|||
let dateFormatString = (localeDateFormat ?? "MM/dd/YY j:mma").replacingOccurrences(of: ",", with: "")
|
||||
if UIDevice.current.userInterfaceIdiom == .pad && !useGrid || UIDevice.current.userInterfaceIdiom == .mac {
|
||||
// Add a table for mac and ipad
|
||||
let positions = node.positions?.reversed() as? [PositionEntity] ?? []
|
||||
let positions = node.positions.reversed()
|
||||
Table(positions, sortOrder: $sortOrder) {
|
||||
TableColumn("Latitude") { position in
|
||||
Text(String(format: "%.5f", position.latitude ?? 0))
|
||||
|
|
@ -93,8 +93,7 @@ struct PositionLog: View {
|
|||
.font(.caption2)
|
||||
.fontWeight(.bold)
|
||||
}
|
||||
if let positions = node.positions?.reversed() as? [PositionEntity] {
|
||||
ForEach(positions, id: \.self) { (mappin: PositionEntity) in
|
||||
ForEach(node.positions.reversed(), id: \.self) { (mappin: PositionEntity) in
|
||||
let altitude = Measurement(value: Double(mappin.altitude), unit: UnitLength.meters)
|
||||
GridRow {
|
||||
Text(String(format: "%.5f", mappin.latitude ?? 0))
|
||||
|
|
@ -109,7 +108,6 @@ struct PositionLog: View {
|
|||
.font(.caption2)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.leading)
|
||||
|
|
@ -141,7 +139,7 @@ struct PositionLog: View {
|
|||
}
|
||||
}
|
||||
Button {
|
||||
exportString = positionToCsvFile(positions: node.positions!.array as? [PositionEntity] ?? [])
|
||||
exportString = positionToCsvFile(positions: node.positions)
|
||||
isExporting = true
|
||||
} label: {
|
||||
Label("Save", systemImage: "square.and.arrow.down")
|
||||
|
|
@ -172,7 +170,7 @@ struct PositionLog: View {
|
|||
ContentUnavailableView("No Positions", systemImage: "mappin.slash")
|
||||
}
|
||||
}
|
||||
.navigationTitle("Position Log \(node.positions?.count ?? 0) Points")
|
||||
.navigationTitle("Position Log \(node.positions.count) Points")
|
||||
.navigationBarItems(
|
||||
trailing:
|
||||
ZStack {
|
||||
|
|
@ -182,15 +180,17 @@ struct PositionLog: View {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: Fix preview for SwiftData
|
||||
/*
|
||||
#Preview {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
let node = NodeInfoEntity(context: context)
|
||||
let node = NodeInfoEntity()
|
||||
node.num = 123456789
|
||||
let user = UserEntity(context: context)
|
||||
let user = UserEntity()
|
||||
user.longName = "Test Node"
|
||||
user.shortName = "TN"
|
||||
node.user = user
|
||||
return PositionLog(node: node)
|
||||
PositionLog(node: node)
|
||||
.environmentObject(AccessoryManager.shared)
|
||||
.environment(\.managedObjectContext, context)
|
||||
.modelContainer(PersistenceController.preview.container)
|
||||
}
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -12,9 +12,9 @@ import OSLog
|
|||
|
||||
struct PowerMetricsLog: View {
|
||||
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@Environment(\.modelContext) private var context
|
||||
@EnvironmentObject var accessoryManager: AccessoryManager
|
||||
@ObservedObject var node: NodeInfoEntity
|
||||
@Bindable var node: NodeInfoEntity
|
||||
private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom }
|
||||
@State private var sortOrder = [KeyPathComparator(\TelemetryEntity.time, order: .reverse)]
|
||||
@State private var selection: TelemetryEntity.ID?
|
||||
|
|
@ -27,8 +27,7 @@ struct PowerMetricsLog: View {
|
|||
@State private var channelSelection = 0
|
||||
|
||||
var powerMetrics: [TelemetryEntity] {
|
||||
let telemetries = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 2"))
|
||||
return (telemetries?.reversed() as? [TelemetryEntity]) ?? []
|
||||
return node.telemetries.filter { $0.metricsType == 2 }.reversed()
|
||||
}
|
||||
|
||||
var minMax: (min: Double, max: Double) {
|
||||
|
|
@ -298,15 +297,17 @@ struct PowerMetricsLog: View {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: Fix preview for SwiftData
|
||||
/*
|
||||
#Preview {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
let node = NodeInfoEntity(context: context)
|
||||
let node = NodeInfoEntity()
|
||||
node.num = 123456789
|
||||
let user = UserEntity(context: context)
|
||||
let user = UserEntity()
|
||||
user.longName = "Test Node"
|
||||
user.shortName = "TN"
|
||||
node.user = user
|
||||
return PowerMetricsLog(node: node)
|
||||
PowerMetricsLog(node: node)
|
||||
.environmentObject(AccessoryManager.shared)
|
||||
.environment(\.managedObjectContext, context)
|
||||
.modelContainer(PersistenceController.preview.container)
|
||||
}
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -6,19 +6,19 @@
|
|||
//
|
||||
|
||||
import SwiftUI
|
||||
import CoreData
|
||||
import SwiftData
|
||||
import OSLog
|
||||
import MapKit
|
||||
|
||||
struct TraceRouteLog: View {
|
||||
private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom }
|
||||
@ObservedObject var locationsHandler = LocationsHandler.shared
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@Environment(\.modelContext) private var context
|
||||
@EnvironmentObject var accessoryManager: AccessoryManager
|
||||
@State private var isPresentingClearLogConfirm: Bool = false
|
||||
@State var isExporting = false
|
||||
@State var exportString = ""
|
||||
@ObservedObject var node: NodeInfoEntity
|
||||
@Bindable var node: NodeInfoEntity
|
||||
@State private var selectedRoute: TraceRouteEntity?
|
||||
// Map Configuration
|
||||
@Namespace var mapScope
|
||||
|
|
@ -35,7 +35,7 @@ struct TraceRouteLog: View {
|
|||
HStack(alignment: .top) {
|
||||
VStack {
|
||||
VStack {
|
||||
List(node.traceRoutes?.reversed() as? [TraceRouteEntity] ?? [], id: \.self, selection: $selectedRoute) { route in
|
||||
List(node.traceRoutes.reversed(), id: \.self, selection: $selectedRoute) { route in
|
||||
Label {
|
||||
let routeTime = route.time?.formatted() ?? "Unknown".localized
|
||||
if route.response && route.hopsTowards == route.hopsBack {
|
||||
|
|
@ -139,7 +139,7 @@ struct TraceRouteLog: View {
|
|||
// Set the view rotation animation after the view appeared,
|
||||
// to avoid animating initial rotation
|
||||
DispatchQueue.main.async {
|
||||
indexes = (selectedRoute?.hops?.array.count ?? 0) * 2
|
||||
indexes = (selectedRoute?.hops.count ?? 0) * 2
|
||||
animation = .easeInOut(duration: 1.0)
|
||||
withAnimation(.easeInOut(duration: 2.0)) {
|
||||
angle = (angle == .degrees(-90) ? .degrees(-90) : .degrees(-90))
|
||||
|
|
@ -165,7 +165,7 @@ struct TraceRouteLog: View {
|
|||
// .annotationTitles(.automatic)
|
||||
// // Direct Trace Route
|
||||
// if selectedRoute?.response ?? false && selectedRoute?.hops?.count ?? 0 == 0 {
|
||||
// if selectedRoute?.node?.positions?.count ?? 0 > 0, let mostRecent = selectedRoute?.node?.positions?.lastObject as? PositionEntity {
|
||||
// if selectedRoute?.node?.positions?.count ?? 0 > 0, let mostRecent = selectedRoute?.node?.positions?.last {
|
||||
// let traceRouteCoords: [CLLocationCoordinate2D] = [selectedRoute?.coordinate ?? LocationsHandler.DefaultLocation, mostRecent.coordinate]
|
||||
// Annotation(selectedRoute?.node?.user?.shortName ?? "???", coordinate: mostRecent.nodeCoordinate ?? LocationHelper.DefaultLocation) {
|
||||
// ZStack {
|
||||
|
|
@ -190,7 +190,7 @@ struct TraceRouteLog: View {
|
|||
// /// Distance
|
||||
// if selectedRoute?.node?.positions?.count ?? 0 > 0,
|
||||
// selectedRoute?.coordinate != nil,
|
||||
// let mostRecent = selectedRoute?.node?.positions?.lastObject as? PositionEntity {
|
||||
// let mostRecent = selectedRoute?.node?.positions?.last {
|
||||
// let startPoint = CLLocation(latitude: selectedRoute?.coordinate?.latitude ?? LocationsHandler.DefaultLocation.latitude, longitude: selectedRoute?.coordinate?.longitude ?? LocationsHandler.DefaultLocation.longitude)
|
||||
// if startPoint.distance(from: CLLocation(latitude: LocationsHandler.DefaultLocation.latitude, longitude: LocationsHandler.DefaultLocation.longitude)) > 0.0 {
|
||||
// let metersAway = selectedRoute?.coordinate?.distance(from: CLLocationCoordinate2D(latitude: mostRecent.latitude ?? LocationsHandler.DefaultLocation.latitude, longitude: mostRecent.longitude ?? LocationsHandler.DefaultLocation.longitude))
|
||||
|
|
@ -224,7 +224,7 @@ struct TraceRouteLog: View {
|
|||
@ViewBuilder func contents(animation: Animation? = nil) -> some View {
|
||||
ForEach(0..<indexes, id: \.self) { idx in
|
||||
TraceRouteComponent(animation: animation) {
|
||||
let hops = selectedRoute?.hops?.array as? [TraceRouteHopEntity] ?? [] // getTraceRouteHops(context: PersistenceController.preview.container.viewContext)//
|
||||
let hops = selectedRoute?.hops ?? []
|
||||
if idx % 2 == 0 {
|
||||
let i = idx / 2
|
||||
let snrColor = getSnrColor(snr: hops[i].snr, preset: modemPreset)
|
||||
|
|
@ -250,31 +250,31 @@ struct TraceRouteLog: View {
|
|||
}
|
||||
}
|
||||
|
||||
func getTraceRouteHops(context: NSManagedObjectContext) -> [TraceRouteHopEntity] {
|
||||
/// static let context = PersistenceController.preview.container.viewContext
|
||||
func getTraceRouteHops(context: ModelContext) -> [TraceRouteHopEntity] {
|
||||
/// static let context = PersistenceController.preview.context
|
||||
var array = [TraceRouteHopEntity]()
|
||||
let trh1 = TraceRouteHopEntity(context: context)
|
||||
let trh1 = TraceRouteHopEntity()
|
||||
trh1.num = 366311664
|
||||
trh1.snr = 12.5
|
||||
let trh2 = TraceRouteHopEntity(context: context)
|
||||
let trh2 = TraceRouteHopEntity()
|
||||
trh2.num = 3662955168
|
||||
trh2.snr = -115.00
|
||||
let trh3 = TraceRouteHopEntity(context: context)
|
||||
let trh3 = TraceRouteHopEntity()
|
||||
trh3.num = 3663982804
|
||||
trh3.snr = 17.5
|
||||
let trh4 = TraceRouteHopEntity(context: context)
|
||||
let trh4 = TraceRouteHopEntity()
|
||||
trh4.num = 4202719792
|
||||
trh4.snr = 7.0
|
||||
let trh5 = TraceRouteHopEntity(context: context)
|
||||
let trh5 = TraceRouteHopEntity()
|
||||
trh5.num = 603700594
|
||||
trh5.snr = 8.9
|
||||
let trh6 = TraceRouteHopEntity(context: context)
|
||||
let trh6 = TraceRouteHopEntity()
|
||||
trh6.num = 836212501
|
||||
trh6.snr = -24.0
|
||||
let trh7 = TraceRouteHopEntity(context: context)
|
||||
let trh7 = TraceRouteHopEntity()
|
||||
trh7.num = 3663116644
|
||||
trh7.snr = -6.0
|
||||
let trh8 = TraceRouteHopEntity(context: context)
|
||||
let trh8 = TraceRouteHopEntity()
|
||||
trh8.num = 8362955168
|
||||
trh8.snr = 7.5
|
||||
array.append(trh1)
|
||||
|
|
@ -288,15 +288,17 @@ func getTraceRouteHops(context: NSManagedObjectContext) -> [TraceRouteHopEntity]
|
|||
return array
|
||||
}
|
||||
|
||||
// TODO: Fix preview for SwiftData
|
||||
/*
|
||||
#Preview {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
let node = NodeInfoEntity(context: context)
|
||||
let node = NodeInfoEntity()
|
||||
node.num = 123456789
|
||||
let user = UserEntity(context: context)
|
||||
let user = UserEntity()
|
||||
user.longName = "Test Node"
|
||||
user.shortName = "TN"
|
||||
node.user = user
|
||||
return TraceRouteLog(node: node)
|
||||
TraceRouteLog(node: node)
|
||||
.environmentObject(AccessoryManager.shared)
|
||||
.environment(\.managedObjectContext, context)
|
||||
.modelContainer(PersistenceController.preview.container)
|
||||
}
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -7,12 +7,12 @@
|
|||
|
||||
import SwiftUI
|
||||
import OSLog
|
||||
import CoreData
|
||||
import SwiftData
|
||||
import Foundation
|
||||
|
||||
struct AppData: View {
|
||||
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@Environment(\.modelContext) private var context
|
||||
@EnvironmentObject var accessoryManager: AccessoryManager
|
||||
@State private var files = [URL]()
|
||||
private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom }
|
||||
|
|
@ -145,8 +145,7 @@ struct AppData: View {
|
|||
}
|
||||
|
||||
#Preview {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
return AppData()
|
||||
AppData()
|
||||
.environmentObject(AccessoryManager.shared)
|
||||
.environment(\.managedObjectContext, context)
|
||||
.modelContainer(PersistenceController.preview.container)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import SwiftUI
|
|||
|
||||
struct AppIconPicker: View {
|
||||
private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom }
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@Environment(\.modelContext) private var context
|
||||
@Binding var isPresenting: Bool
|
||||
@State private var didError = false
|
||||
@State private var errorDetails: String?
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import OSLog
|
|||
|
||||
struct AppSettings: View {
|
||||
private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom }
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@Environment(\.modelContext) private var context
|
||||
@EnvironmentObject var accessoryManager: AccessoryManager
|
||||
@State var totalDownloadedTileSize = ""
|
||||
@State private var isPresentingCoreDataResetConfirm = false
|
||||
|
|
@ -159,9 +159,8 @@ struct AppSettings: View {
|
|||
Logger.services.error("🗄 Error Deleting Meshtastic.sqlite file \(error, privacy: .public)")
|
||||
}
|
||||
}
|
||||
await MeshPackets.shared.clearCoreDataDatabase(includeRoutes: true)
|
||||
await PersistenceController.shared.clearDatabase(includeRoutes: true)
|
||||
clearNotifications()
|
||||
context.refreshAllObjects()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
// Copyright(c) Garth Vander Houwen 4/8/22.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
import SwiftData
|
||||
import MapKit
|
||||
import MeshtasticProtobufs
|
||||
import OSLog
|
||||
|
|
@ -22,7 +22,7 @@ func generateChannelKey(size: Int) -> String {
|
|||
|
||||
struct Channels: View {
|
||||
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@Environment(\.modelContext) private var context
|
||||
@EnvironmentObject var accessoryManager: AccessoryManager
|
||||
@Environment(\.dismiss) private var goBack
|
||||
@Environment(\.sizeCategory) var sizeCategory
|
||||
|
|
@ -50,13 +50,8 @@ struct Channels: View {
|
|||
@State var minimumVersion = "2.2.24"
|
||||
@State private var showingHelp = false
|
||||
|
||||
@FetchRequest(
|
||||
sortDescriptors: [NSSortDescriptor(key: "favorite", ascending: false),
|
||||
NSSortDescriptor(key: "lastHeard", ascending: false),
|
||||
NSSortDescriptor(key: "user.longName", ascending: true)],
|
||||
animation: .default)
|
||||
|
||||
var nodes: FetchedResults<NodeInfoEntity>
|
||||
@Query(sort: \NodeInfoEntity.lastHeard, order: .reverse)
|
||||
var nodes: [NodeInfoEntity]
|
||||
|
||||
var body: some View {
|
||||
|
||||
|
|
@ -66,7 +61,7 @@ struct Channels: View {
|
|||
.tipBackground(colorScheme == .dark ? Color(.systemBackground) : Color(.secondarySystemBackground))
|
||||
.listRowSeparator(.hidden)
|
||||
if node != nil && node?.myInfo != nil {
|
||||
ForEach(node?.myInfo?.channels?.array as? [ChannelEntity] ?? [], id: \.self) { (channel: ChannelEntity) in
|
||||
ForEach(node?.myInfo?.channels ?? [], id: \.self) { (channel: ChannelEntity) in
|
||||
Button(action: {
|
||||
channelIndex = channel.index
|
||||
channelRole = Int(channel.role)
|
||||
|
|
@ -177,17 +172,15 @@ struct Channels: View {
|
|||
selectedChannel!.downlinkEnabled = downlink
|
||||
selectedChannel!.positionPrecision = Int32(positionPrecision)
|
||||
|
||||
guard let mutableChannels = node?.myInfo?.channels?.mutableCopy() as? NSMutableOrderedSet else {
|
||||
guard var channels = node?.myInfo?.channels else {
|
||||
return
|
||||
}
|
||||
if mutableChannels.contains(selectedChannel as Any) {
|
||||
let replaceChannel = mutableChannels.first(where: { selectedChannel?.psk == ($0 as AnyObject).psk && selectedChannel?.name == ($0 as AnyObject).name})
|
||||
mutableChannels.replaceObject(at: mutableChannels.index(of: replaceChannel as Any), with: selectedChannel as Any)
|
||||
if let idx = channels.firstIndex(where: { $0.psk == selectedChannel?.psk && $0.name == selectedChannel?.name }) {
|
||||
channels[idx] = selectedChannel!
|
||||
} else {
|
||||
mutableChannels.add(selectedChannel as Any)
|
||||
channels.append(selectedChannel!)
|
||||
}
|
||||
node?.myInfo?.channels = mutableChannels.copy() as? NSOrderedSet
|
||||
context.refresh(selectedChannel!, mergeChanges: true)
|
||||
node?.myInfo?.channels = channels
|
||||
if channel.role != Channel.Role.disabled {
|
||||
do {
|
||||
try context.save()
|
||||
|
|
@ -246,12 +239,10 @@ struct Channels: View {
|
|||
#endif
|
||||
}
|
||||
}
|
||||
if node?.myInfo?.channels?.array.count ?? 0 < 8 && node != nil {
|
||||
if node?.myInfo?.channels.count ?? 0 < 8 && node != nil {
|
||||
|
||||
Button {
|
||||
let channelIndexes = node?.myInfo?.channels?.compactMap({(ch) -> Int in
|
||||
return (ch as AnyObject).index
|
||||
})
|
||||
let channelIndexes = node?.myInfo?.channels.map { Int($0.index) }
|
||||
let firstChannelIndex = firstMissingChannelIndex(channelIndexes ?? [])
|
||||
channelKeySize = 16
|
||||
let key = generateChannelKey(size: channelKeySize)
|
||||
|
|
@ -265,7 +256,8 @@ struct Channels: View {
|
|||
uplink = false
|
||||
downlink = false
|
||||
|
||||
let newChannel = ChannelEntity(context: context)
|
||||
let newChannel = ChannelEntity()
|
||||
context.insert(newChannel)
|
||||
newChannel.id = channelIndex
|
||||
newChannel.index = channelIndex
|
||||
newChannel.uplinkEnabled = uplink
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import OSLog
|
|||
import SwiftUI
|
||||
|
||||
struct BluetoothConfig: View {
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@Environment(\.modelContext) private var context
|
||||
@EnvironmentObject var accessoryManager: AccessoryManager
|
||||
@Environment(\.dismiss) private var goBack
|
||||
var node: NodeInfoEntity?
|
||||
|
|
@ -149,8 +149,7 @@ struct BluetoothConfig: View {
|
|||
}
|
||||
|
||||
#Preview {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
return BluetoothConfig(node: nil)
|
||||
BluetoothConfig(node: nil)
|
||||
.environmentObject(AccessoryManager.shared)
|
||||
.environment(\.managedObjectContext, context)
|
||||
.modelContainer(PersistenceController.preview.container)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import SwiftUI
|
||||
import CoreData
|
||||
import SwiftData
|
||||
|
||||
struct ConfigHeader<T>: View {
|
||||
@EnvironmentObject var accessoryManager: AccessoryManager
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import SwiftUI
|
|||
|
||||
struct DeviceConfig: View {
|
||||
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@Environment(\.modelContext) private var context
|
||||
@EnvironmentObject var accessoryManager: AccessoryManager
|
||||
@Environment(\.dismiss) private var goBack
|
||||
|
||||
|
|
@ -175,7 +175,7 @@ struct DeviceConfig: View {
|
|||
try await accessoryManager.sendNodeDBReset(fromUser: node!.user!, toUser: node!.user!)
|
||||
try await Task.sleep(for: .seconds(1))
|
||||
try await accessoryManager.disconnect()
|
||||
await MeshPackets.shared.clearCoreDataDatabase(includeRoutes: false)
|
||||
await PersistenceController.shared.clearDatabase(includeRoutes: false)
|
||||
clearNotifications()
|
||||
} catch {
|
||||
Logger.mesh.error("NodeDB Reset Failed")
|
||||
|
|
@ -200,7 +200,7 @@ struct DeviceConfig: View {
|
|||
try await accessoryManager.sendFactoryReset(fromUser: node!.user!, toUser: node!.user!)
|
||||
try await Task.sleep(for: .seconds(1))
|
||||
try await accessoryManager.disconnect()
|
||||
await MeshPackets.shared.clearCoreDataDatabase(includeRoutes: false)
|
||||
await PersistenceController.shared.clearDatabase(includeRoutes: false)
|
||||
clearNotifications()
|
||||
} catch {
|
||||
Logger.mesh.error("Factory Reset Failed")
|
||||
|
|
@ -213,7 +213,7 @@ struct DeviceConfig: View {
|
|||
try await accessoryManager.sendFactoryReset(fromUser: node!.user!, toUser: node!.user!, resetDevice: true)
|
||||
try? await Task.sleep(for: .seconds(1))
|
||||
try await accessoryManager.disconnect()
|
||||
await MeshPackets.shared.clearCoreDataDatabase(includeRoutes: false)
|
||||
await PersistenceController.shared.clearDatabase(includeRoutes: false)
|
||||
clearNotifications()
|
||||
} catch {
|
||||
Logger.mesh.error("Factory Reset Failed")
|
||||
|
|
@ -347,8 +347,7 @@ struct DeviceConfig: View {
|
|||
}
|
||||
|
||||
#Preview {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
return DeviceConfig(node: nil)
|
||||
DeviceConfig(node: nil)
|
||||
.environmentObject(AccessoryManager.shared)
|
||||
.environment(\.managedObjectContext, context)
|
||||
.modelContainer(PersistenceController.preview.container)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import SwiftUI
|
|||
|
||||
struct DisplayConfig: View {
|
||||
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@Environment(\.modelContext) private var context
|
||||
@EnvironmentObject var accessoryManager: AccessoryManager
|
||||
@Environment(\.dismiss) private var goBack
|
||||
|
||||
|
|
@ -237,8 +237,7 @@ struct DisplayConfig: View {
|
|||
}
|
||||
|
||||
#Preview {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
return DisplayConfig(node: nil)
|
||||
DisplayConfig(node: nil)
|
||||
.environmentObject(AccessoryManager.shared)
|
||||
.environment(\.managedObjectContext, context)
|
||||
.modelContainer(PersistenceController.preview.container)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
//
|
||||
|
||||
import SwiftUI
|
||||
import CoreData
|
||||
import SwiftData
|
||||
import MeshtasticProtobufs
|
||||
import OSLog
|
||||
|
||||
|
|
@ -24,7 +24,7 @@ struct LoRaConfig: View {
|
|||
return formatter
|
||||
}()
|
||||
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@Environment(\.modelContext) private var context
|
||||
@EnvironmentObject var accessoryManager: AccessoryManager
|
||||
@Environment(\.dismiss) private var goBack
|
||||
@FocusState var focusedField: Field?
|
||||
|
|
@ -323,8 +323,7 @@ struct LoRaConfig: View {
|
|||
}
|
||||
|
||||
#Preview {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
return LoRaConfig(node: nil)
|
||||
LoRaConfig(node: nil)
|
||||
.environmentObject(AccessoryManager.shared)
|
||||
.environment(\.managedObjectContext, context)
|
||||
.modelContainer(PersistenceController.preview.container)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import OSLog
|
|||
|
||||
struct AmbientLightingConfig: View {
|
||||
@Environment(\.self) var environment
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@Environment(\.modelContext) private var context
|
||||
@EnvironmentObject var accessoryManager: AccessoryManager
|
||||
@Environment(\.dismiss) private var goBack
|
||||
|
||||
|
|
@ -136,8 +136,7 @@ struct AmbientLightingConfig: View {
|
|||
}
|
||||
|
||||
#Preview {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
return AmbientLightingConfig(node: nil)
|
||||
AmbientLightingConfig(node: nil)
|
||||
.environmentObject(AccessoryManager.shared)
|
||||
.environment(\.managedObjectContext, context)
|
||||
.modelContainer(PersistenceController.preview.container)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import OSLog
|
|||
import SwiftUI
|
||||
|
||||
struct CannedMessagesConfig: View {
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@Environment(\.modelContext) private var context
|
||||
@EnvironmentObject var accessoryManager: AccessoryManager
|
||||
@Environment(\.dismiss) private var goBack
|
||||
var node: NodeInfoEntity?
|
||||
|
|
@ -357,8 +357,7 @@ struct CannedMessagesConfig: View {
|
|||
}
|
||||
|
||||
#Preview {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
return CannedMessagesConfig(node: nil)
|
||||
CannedMessagesConfig(node: nil)
|
||||
.environmentObject(AccessoryManager.shared)
|
||||
.environment(\.managedObjectContext, context)
|
||||
.modelContainer(PersistenceController.preview.container)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ enum DetectionSensorRole: String, CaseIterable, Equatable, Decodable {
|
|||
|
||||
struct DetectionSensorConfig: View {
|
||||
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@Environment(\.modelContext) private var context
|
||||
@EnvironmentObject var accessoryManager: AccessoryManager
|
||||
@Environment(\.dismiss) private var goBack
|
||||
var node: NodeInfoEntity?
|
||||
|
|
@ -263,8 +263,7 @@ struct DetectionSensorConfig: View {
|
|||
}
|
||||
|
||||
#Preview {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
return DetectionSensorConfig(node: nil)
|
||||
DetectionSensorConfig(node: nil)
|
||||
.environmentObject(AccessoryManager.shared)
|
||||
.environment(\.managedObjectContext, context)
|
||||
.modelContainer(PersistenceController.preview.container)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import SwiftUI
|
|||
|
||||
struct ExternalNotificationConfig: View {
|
||||
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@Environment(\.modelContext) private var context
|
||||
@EnvironmentObject var accessoryManager: AccessoryManager
|
||||
@Environment(\.dismiss) private var goBack
|
||||
|
||||
|
|
@ -284,8 +284,7 @@ struct ExternalNotificationConfig: View {
|
|||
}
|
||||
|
||||
#Preview {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
return ExternalNotificationConfig(node: nil)
|
||||
ExternalNotificationConfig(node: nil)
|
||||
.environmentObject(AccessoryManager.shared)
|
||||
.environment(\.managedObjectContext, context)
|
||||
.modelContainer(PersistenceController.preview.container)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import SwiftUI
|
|||
|
||||
struct MQTTConfig: View {
|
||||
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@Environment(\.modelContext) private var context
|
||||
@EnvironmentObject var accessoryManager: AccessoryManager
|
||||
@Environment(\.dismiss) private var goBack
|
||||
var node: NodeInfoEntity?
|
||||
|
|
@ -466,8 +466,7 @@ struct MQTTConfig: View {
|
|||
}
|
||||
|
||||
#Preview {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
return MQTTConfig(node: nil)
|
||||
MQTTConfig(node: nil)
|
||||
.environmentObject(AccessoryManager.shared)
|
||||
.environment(\.managedObjectContext, context)
|
||||
.modelContainer(PersistenceController.preview.container)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import SwiftUI
|
|||
import OSLog
|
||||
|
||||
struct PaxCounterConfig: View {
|
||||
@Environment(\.managedObjectContext) private var context
|
||||
@Environment(\.modelContext) private var context
|
||||
@EnvironmentObject private var accessoryManager: AccessoryManager
|
||||
@Environment(\.dismiss) private var goBack
|
||||
|
||||
|
|
@ -125,8 +125,7 @@ struct PaxCounterConfig: View {
|
|||
}
|
||||
|
||||
#Preview {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
return PaxCounterConfig(node: nil)
|
||||
PaxCounterConfig(node: nil)
|
||||
.environmentObject(AccessoryManager.shared)
|
||||
.environment(\.managedObjectContext, context)
|
||||
.modelContainer(PersistenceController.preview.container)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,13 +5,13 @@
|
|||
// Copyright (c) Garth Vander Houwen 6/13/22.
|
||||
//
|
||||
import MeshtasticProtobufs
|
||||
import CoreData
|
||||
import SwiftData
|
||||
import OSLog
|
||||
import SwiftUI
|
||||
|
||||
struct RangeTestConfig: View {
|
||||
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@Environment(\.modelContext) private var context
|
||||
@EnvironmentObject var accessoryManager: AccessoryManager
|
||||
@Environment(\.dismiss) private var goBack
|
||||
|
||||
|
|
@ -23,7 +23,7 @@ struct RangeTestConfig: View {
|
|||
@State var save = false
|
||||
@State private var sender: UpdateInterval = UpdateInterval(from: 0)
|
||||
private var isPrimaryChannelPublic: Bool {
|
||||
guard let channels = node?.myInfo?.channels?.array as? [ChannelEntity] else {
|
||||
guard let channels = node?.myInfo?.channels else {
|
||||
return false
|
||||
}
|
||||
// Treat the primary channel on this node as "public" when it is effectively unencrypted
|
||||
|
|
@ -144,8 +144,7 @@ struct RangeTestConfig: View {
|
|||
}
|
||||
|
||||
#Preview {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
return RangeTestConfig(node: nil)
|
||||
RangeTestConfig(node: nil)
|
||||
.environmentObject(AccessoryManager.shared)
|
||||
.environment(\.managedObjectContext, context)
|
||||
.modelContainer(PersistenceController.preview.container)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import SwiftUI
|
|||
import OSLog
|
||||
|
||||
struct RtttlConfig: View {
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@Environment(\.modelContext) private var context
|
||||
@EnvironmentObject var accessoryManager: AccessoryManager
|
||||
@Environment(\.dismiss) private var goBack
|
||||
|
||||
|
|
@ -116,8 +116,7 @@ struct RtttlConfig: View {
|
|||
}
|
||||
|
||||
#Preview {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
return RtttlConfig(node: nil)
|
||||
RtttlConfig(node: nil)
|
||||
.environmentObject(AccessoryManager.shared)
|
||||
.environment(\.managedObjectContext, context)
|
||||
.modelContainer(PersistenceController.preview.container)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import SwiftUI
|
|||
|
||||
struct SerialConfig: View {
|
||||
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@Environment(\.modelContext) private var context
|
||||
@EnvironmentObject var accessoryManager: AccessoryManager
|
||||
@Environment(\.dismiss) private var goBack
|
||||
|
||||
|
|
@ -207,8 +207,7 @@ struct SerialConfig: View {
|
|||
}
|
||||
|
||||
#Preview {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
return SerialConfig(node: nil)
|
||||
SerialConfig(node: nil)
|
||||
.environmentObject(AccessoryManager.shared)
|
||||
.environment(\.managedObjectContext, context)
|
||||
.modelContainer(PersistenceController.preview.container)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import SwiftUI
|
|||
|
||||
struct StoreForwardConfig: View {
|
||||
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@Environment(\.modelContext) private var context
|
||||
@EnvironmentObject var accessoryManager: AccessoryManager
|
||||
@Environment(\.dismiss) private var goBack
|
||||
var node: NodeInfoEntity?
|
||||
|
|
@ -199,8 +199,7 @@ struct StoreForwardConfig: View {
|
|||
}
|
||||
|
||||
#Preview {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
return StoreForwardConfig(node: nil)
|
||||
StoreForwardConfig(node: nil)
|
||||
.environmentObject(AccessoryManager.shared)
|
||||
.environment(\.managedObjectContext, context)
|
||||
.modelContainer(PersistenceController.preview.container)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,12 +2,12 @@
|
|||
// TAKModuleConfig.swift
|
||||
// Meshtastic
|
||||
import SwiftUI
|
||||
import CoreData
|
||||
import SwiftData
|
||||
import OSLog
|
||||
import MeshtasticProtobufs
|
||||
|
||||
struct TAKModuleConfig: View {
|
||||
@Environment(\.managedObjectContext) private var context
|
||||
@Environment(\.modelContext) private var context
|
||||
@EnvironmentObject private var accessoryManager: AccessoryManager
|
||||
@Environment(\.dismiss) private var goBack
|
||||
|
||||
|
|
@ -262,8 +262,7 @@ struct TAKModuleConfig: View {
|
|||
}
|
||||
|
||||
#Preview {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
return TAKModuleConfig(node: nil)
|
||||
TAKModuleConfig(node: nil)
|
||||
.environmentObject(AccessoryManager.shared)
|
||||
.environment(\.managedObjectContext, context)
|
||||
.modelContainer(PersistenceController.preview.container)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import SwiftUI
|
|||
|
||||
struct TelemetryConfig: View {
|
||||
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@Environment(\.modelContext) private var context
|
||||
@EnvironmentObject var accessoryManager: AccessoryManager
|
||||
@Environment(\.dismiss) private var goBack
|
||||
|
||||
|
|
@ -243,8 +243,7 @@ struct TelemetryConfig: View {
|
|||
}
|
||||
|
||||
#Preview {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
return TelemetryConfig(node: nil)
|
||||
TelemetryConfig(node: nil)
|
||||
.environmentObject(AccessoryManager.shared)
|
||||
.environment(\.managedObjectContext, context)
|
||||
.modelContainer(PersistenceController.preview.container)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import SwiftUI
|
|||
|
||||
struct NetworkConfig: View {
|
||||
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@Environment(\.modelContext) private var context
|
||||
@EnvironmentObject var accessoryManager: AccessoryManager
|
||||
@Environment(\.dismiss) private var goBack
|
||||
|
||||
|
|
@ -211,8 +211,7 @@ struct NetworkConfig: View {
|
|||
}
|
||||
|
||||
#Preview {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
return NetworkConfig(node: nil)
|
||||
NetworkConfig(node: nil)
|
||||
.environmentObject(AccessoryManager.shared)
|
||||
.environment(\.managedObjectContext, context)
|
||||
.modelContainer(PersistenceController.preview.container)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ struct PositionFlags: OptionSet {
|
|||
|
||||
struct PositionConfig: View {
|
||||
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@Environment(\.modelContext) private var context
|
||||
@EnvironmentObject var accessoryManager: AccessoryManager
|
||||
@Environment(\.dismiss) private var goBack
|
||||
var node: NodeInfoEntity?
|
||||
|
|
@ -548,9 +548,7 @@ struct PositionConfig: View {
|
|||
Logger.mesh.error("Remove Fixed Position Failed")
|
||||
}
|
||||
}
|
||||
let mutablePositions = node?.positions?.mutableCopy() as? NSMutableOrderedSet
|
||||
mutablePositions?.removeAllObjects()
|
||||
node?.positions = mutablePositions
|
||||
node?.positions = []
|
||||
node?.positionConfig?.fixedPosition = false
|
||||
do {
|
||||
try context.save()
|
||||
|
|
@ -564,8 +562,7 @@ struct PositionConfig: View {
|
|||
}
|
||||
|
||||
#Preview {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
return PositionConfig(node: nil)
|
||||
PositionConfig(node: nil)
|
||||
.environmentObject(AccessoryManager.shared)
|
||||
.environment(\.managedObjectContext, context)
|
||||
.modelContainer(PersistenceController.preview.container)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import MeshtasticProtobufs
|
|||
import OSLog
|
||||
|
||||
struct PowerConfig: View {
|
||||
@Environment(\.managedObjectContext) private var context
|
||||
@Environment(\.modelContext) private var context
|
||||
@EnvironmentObject var accessoryManager: AccessoryManager
|
||||
@Environment(\.dismiss) private var goBack
|
||||
|
||||
|
|
@ -228,8 +228,7 @@ private struct FloatField: View {
|
|||
}
|
||||
|
||||
#Preview {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
return PowerConfig(node: nil)
|
||||
PowerConfig(node: nil)
|
||||
.environmentObject(AccessoryManager.shared)
|
||||
.environment(\.managedObjectContext, context)
|
||||
.modelContainer(PersistenceController.preview.container)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import CoreData
|
||||
import SwiftData
|
||||
import MeshtasticProtobufs
|
||||
import OSLog
|
||||
import CryptoKit
|
||||
|
|
@ -15,7 +15,7 @@ import CryptoKit
|
|||
struct SecurityConfig: View {
|
||||
|
||||
private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom }
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@Environment(\.modelContext) private var context
|
||||
@EnvironmentObject var accessoryManager: AccessoryManager
|
||||
@Environment(\.dismiss) private var goBack
|
||||
|
||||
|
|
@ -430,8 +430,7 @@ struct SecurityConfig: View {
|
|||
}
|
||||
|
||||
#Preview {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
return SecurityConfig(node: nil)
|
||||
SecurityConfig(node: nil)
|
||||
.environmentObject(AccessoryManager.shared)
|
||||
.environment(\.managedObjectContext, context)
|
||||
.modelContainer(PersistenceController.preview.container)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import StoreKit
|
|||
import OSLog
|
||||
|
||||
struct Firmware: View {
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@Environment(\.modelContext) private var context
|
||||
@EnvironmentObject var accessoryManager: AccessoryManager
|
||||
var node: NodeInfoEntity?
|
||||
@State var minimumVersion = "2.5.4"
|
||||
|
|
@ -208,8 +208,7 @@ struct Firmware: View {
|
|||
}
|
||||
|
||||
#Preview {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
return Firmware(node: nil)
|
||||
Firmware(node: nil)
|
||||
.environmentObject(AccessoryManager.shared)
|
||||
.environment(\.managedObjectContext, context)
|
||||
.modelContainer(PersistenceController.preview.container)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
//
|
||||
|
||||
import SwiftUI
|
||||
import CoreData
|
||||
import SwiftData
|
||||
import MapKit
|
||||
import CoreLocation
|
||||
import CoreMotion
|
||||
|
|
@ -15,7 +15,7 @@ import OSLog
|
|||
struct RouteRecorder: View {
|
||||
|
||||
@ObservedObject var locationsHandler: LocationsHandler = LocationsHandler.shared
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@Environment(\.modelContext) private var context
|
||||
@State private var position: MapCameraPosition = .userLocation(followsHeading: true, fallback: .automatic)
|
||||
@State var mapStyle: MapStyle = MapStyle.standard(elevation: .realistic)
|
||||
@State var isShowingDetails = false
|
||||
|
|
@ -182,7 +182,8 @@ struct RouteRecorder: View {
|
|||
locationsHandler.elevationGain = 0.0
|
||||
locationsHandler.locationsArray.removeAll()
|
||||
locationsHandler.recordingStarted = Date()
|
||||
let newRoute = RouteEntity(context: context)
|
||||
let newRoute = RouteEntity()
|
||||
context.insert(newRoute)
|
||||
newRoute.date = Date()
|
||||
let at = ActivityType(rawValue: activity)
|
||||
newRoute.name = "\(newRoute.date?.relativeTimeOfDay() ?? "morning".localized) \(at?.fileNameString ?? "hike")"
|
||||
|
|
@ -241,7 +242,6 @@ struct RouteRecorder: View {
|
|||
rec.enabled = true
|
||||
rec.distance = locationsHandler.distanceTraveled
|
||||
rec.elevationGain = locationsHandler.elevationGain
|
||||
context.refresh(rec, mergeChanges: true)
|
||||
}
|
||||
locationsHandler.isRecording = false
|
||||
locationsHandler.isRecordingPaused = false
|
||||
|
|
@ -296,7 +296,8 @@ struct RouteRecorder: View {
|
|||
if locationsHandler.isRecording {
|
||||
if let loc = newLoc {
|
||||
if recording != nil {
|
||||
let locationEntity = LocationEntity(context: context)
|
||||
let locationEntity = LocationEntity()
|
||||
context.insert(locationEntity)
|
||||
locationEntity.routeLocation = recording
|
||||
locationEntity.id = Int32(locationsHandler.count)
|
||||
locationEntity.altitude = Int32(loc.altitude)
|
||||
|
|
|
|||
|
|
@ -6,14 +6,14 @@
|
|||
//
|
||||
|
||||
import SwiftUI
|
||||
import CoreData
|
||||
import SwiftData
|
||||
import MapKit
|
||||
import OSLog
|
||||
|
||||
struct Routes: View {
|
||||
|
||||
@State private var columnVisibility = NavigationSplitViewVisibility.doubleColumn
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@Environment(\.modelContext) private var context
|
||||
@EnvironmentObject var accessoryManager: AccessoryManager
|
||||
@State private var selectedRoute: RouteEntity?
|
||||
@State private var importing = false
|
||||
|
|
@ -27,9 +27,9 @@ struct Routes: View {
|
|||
@State var enabled = true
|
||||
@State var color = Color(red: 51, green: 199, blue: 88)
|
||||
|
||||
@FetchRequest(sortDescriptors: [NSSortDescriptor(key: "enabled", ascending: false), NSSortDescriptor(key: "name", ascending: true), NSSortDescriptor(key: "date", ascending: false)], animation: .default)
|
||||
|
||||
var routes: FetchedResults<RouteEntity>
|
||||
@Query(sort: [SortDescriptor(\RouteEntity.name, order: .forward),
|
||||
SortDescriptor(\RouteEntity.date, order: .reverse)])
|
||||
var routes: [RouteEntity]
|
||||
var body: some View {
|
||||
|
||||
VStack {
|
||||
|
|
@ -72,7 +72,8 @@ struct Routes: View {
|
|||
}
|
||||
}
|
||||
if latIndex >= 0 && longIndex >= 0 {
|
||||
let newRoute = RouteEntity(context: context)
|
||||
let newRoute = RouteEntity()
|
||||
context.insert(newRoute)
|
||||
newRoute.name = String(routeName)
|
||||
newRoute.id = Int32.random(in: Int32(Int8.max) ... Int32.max)
|
||||
newRoute.color = Int64(UIColor.random.hex)
|
||||
|
|
@ -84,13 +85,14 @@ struct Routes: View {
|
|||
if data.count > 1 {
|
||||
let latitude = latIndex >= 0 ? data[latIndex].trimmingCharacters(in: .whitespaces) : "0"
|
||||
let longitude = longIndex >= 0 ? data[longIndex].trimmingCharacters(in: .whitespaces) : "0"
|
||||
let loc = LocationEntity(context: context)
|
||||
let loc = LocationEntity()
|
||||
context.insert(loc)
|
||||
loc.latitudeI = Int32((Double(latitude) ?? 0) * 1e7)
|
||||
loc.longitudeI = Int32((Double(longitude) ?? 0) * 1e7)
|
||||
newLocations.append(loc)
|
||||
}
|
||||
}
|
||||
newRoute.locations? = NSOrderedSet(array: newLocations)
|
||||
newRoute.locations = newLocations
|
||||
do {
|
||||
try context.save()
|
||||
} catch let error as NSError {
|
||||
|
|
@ -143,7 +145,7 @@ struct Routes: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.badge(Text("\(Image(systemName: "mappin.and.ellipse")) \(route.locations?.count ?? 0)"))
|
||||
.badge(Text("\(Image(systemName: "mappin.and.ellipse")) \(route.locations.count)"))
|
||||
.font(.headline)
|
||||
.swipeActions {
|
||||
Button(role: .destructive) {
|
||||
|
|
@ -163,7 +165,7 @@ struct Routes: View {
|
|||
} else {
|
||||
VStack {
|
||||
if selectedRoute != nil {
|
||||
let locationArray = selectedRoute?.locations?.array as? [LocationEntity] ?? []
|
||||
let locationArray = selectedRoute?.locations ?? []
|
||||
let lineCoords = locationArray.compactMap({(location) -> CLLocationCoordinate2D in
|
||||
return location.locationCoordinate ?? LocationsHandler.DefaultLocation
|
||||
})
|
||||
|
|
@ -276,7 +278,7 @@ struct Routes: View {
|
|||
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
|
||||
.safeAreaInset(edge: .bottom, alignment: UIDevice.current.userInterfaceIdiom == .phone ? .leading : .trailing) {
|
||||
Button {
|
||||
exportString = routeToCsvFile(locations: selectedRoute!.locations!.array as? [LocationEntity] ?? [])
|
||||
exportString = routeToCsvFile(locations: selectedRoute?.locations ?? [])
|
||||
isExporting = true
|
||||
} label: {
|
||||
Label("Export", systemImage: "square.and.arrow.down")
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
// Copyright(c) Garth Vander Houwen 7/13/22.
|
||||
//
|
||||
import SwiftUI
|
||||
import CoreData
|
||||
import SwiftData
|
||||
import OSLog
|
||||
import MeshtasticProtobufs
|
||||
|
||||
|
|
@ -17,7 +17,7 @@ struct SaveChannelLinkData: Identifiable {
|
|||
|
||||
struct SaveChannelQRCode: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@Environment(\.modelContext) private var context
|
||||
let channelSetLink: String
|
||||
@State var addChannels: Bool = false
|
||||
var accessoryManager: AccessoryManager
|
||||
|
|
@ -165,12 +165,14 @@ struct SaveChannelQRCode: View {
|
|||
channelData = channelSetLink
|
||||
}
|
||||
Logger.data.info("Processing channel data: \(channelData)")
|
||||
// Fetch current LoRa config from Core Data
|
||||
let fetchRequest = NodeInfoEntity.fetchRequest()
|
||||
fetchRequest.predicate = NSPredicate(format: "num == %lld", Int64(accessoryManager.activeDeviceNum ?? 0))
|
||||
// Fetch current LoRa config
|
||||
let activeNum = Int64(accessoryManager.activeDeviceNum ?? 0)
|
||||
let descriptor = FetchDescriptor<NodeInfoEntity>(
|
||||
predicate: #Predicate { $0.num == activeNum }
|
||||
)
|
||||
|
||||
do {
|
||||
let nodes = try context.fetch(fetchRequest)
|
||||
let nodes = try context.fetch(descriptor)
|
||||
if let node = nodes.first {
|
||||
currentLoRaConfig = node.loRaConfig?.toProto()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,24 +6,17 @@
|
|||
//
|
||||
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
import OSLog
|
||||
import TipKit
|
||||
import MeshtasticProtobufs
|
||||
|
||||
struct Settings: View {
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@Environment(\.modelContext) private var context
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
@EnvironmentObject var accessoryManager: AccessoryManager
|
||||
@FetchRequest(
|
||||
sortDescriptors: [
|
||||
NSSortDescriptor(key: "favorite", ascending: false),
|
||||
NSSortDescriptor(key: "user.pkiEncrypted", ascending: false),
|
||||
NSSortDescriptor(key: "viaMqtt", ascending: true),
|
||||
NSSortDescriptor(key: "user.longName", ascending: true)
|
||||
],
|
||||
animation: .default
|
||||
)
|
||||
private var nodes: FetchedResults<NodeInfoEntity>
|
||||
@Query(sort: \NodeInfoEntity.lastHeard, order: .reverse)
|
||||
private var nodes: [NodeInfoEntity]
|
||||
|
||||
@State private var selectedNode: Int = 0
|
||||
@State private var preferredNodeNum: Int = 0
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
// Copyright(c) Garth Vander Houwen 4/8/22.
|
||||
//
|
||||
import SwiftUI
|
||||
import CoreData
|
||||
import SwiftData
|
||||
import CoreImage.CIFilterBuiltins
|
||||
import MeshtasticProtobufs
|
||||
import TipKit
|
||||
|
|
@ -33,7 +33,7 @@ struct QrCodeImage {
|
|||
|
||||
struct ShareChannels: View {
|
||||
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@Environment(\.modelContext) private var context
|
||||
@EnvironmentObject var accessoryManager: AccessoryManager
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@State var channelSet: ChannelSet = ChannelSet()
|
||||
|
|
@ -78,7 +78,7 @@ struct ShareChannels: View {
|
|||
.font(.caption)
|
||||
.fontWeight(.bold)
|
||||
}
|
||||
ForEach(node?.myInfo?.channels?.array as? [ChannelEntity] ?? [], id: \.self) { (channel: ChannelEntity) in
|
||||
ForEach(node?.myInfo?.channels ?? [], id: \.self) { (channel: ChannelEntity) in
|
||||
GridRow {
|
||||
Spacer()
|
||||
if channel.index == 0 {
|
||||
|
|
@ -285,8 +285,8 @@ struct ShareChannels: View {
|
|||
loRaConfig.ignoreMqtt = node?.loRaConfig?.ignoreMqtt ?? false
|
||||
loRaConfig.overrideFrequency = node?.loRaConfig?.overrideFrequency ?? 0.0
|
||||
channelSet.loraConfig = loRaConfig
|
||||
if node?.myInfo?.channels != nil && node?.myInfo?.channels?.count ?? 0 > 0 {
|
||||
for ch in node?.myInfo?.channels?.array as? [ChannelEntity] ?? [] where ch.role > 0 {
|
||||
if node?.myInfo != nil && (node?.myInfo?.channels.count ?? 0) > 0 {
|
||||
for ch in node?.myInfo?.channels ?? [] where ch.role > 0 {
|
||||
var includeChannel = false
|
||||
switch ch.index {
|
||||
case 0:
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
import SwiftUI
|
||||
import UniformTypeIdentifiers
|
||||
import OSLog
|
||||
import CoreData
|
||||
import SwiftData
|
||||
|
||||
enum CertificateImportType {
|
||||
case p12
|
||||
|
|
@ -16,14 +16,12 @@ enum CertificateImportType {
|
|||
}
|
||||
|
||||
struct TAKServerConfig: View {
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@Environment(\.modelContext) private var context
|
||||
@EnvironmentObject var accessoryManager: AccessoryManager
|
||||
|
||||
@FetchRequest(
|
||||
sortDescriptors: [NSSortDescriptor(keyPath: \ChannelEntity.index, ascending: true)],
|
||||
predicate: NSPredicate(format: "role > 0"),
|
||||
animation: .default
|
||||
) private var channels: FetchedResults<ChannelEntity>
|
||||
@Query(filter: #Predicate<ChannelEntity> { $0.role > 0 },
|
||||
sort: \ChannelEntity.index)
|
||||
private var channels: [ChannelEntity]
|
||||
|
||||
@StateObject private var takServer = TAKServerManager.shared
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import OSLog
|
|||
@available(iOS 18, *)
|
||||
struct Tools: View {
|
||||
@EnvironmentObject var accessoryManager: AccessoryManager
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@Environment(\.modelContext) private var context
|
||||
|
||||
#if !targetEnvironment(macCatalyst)
|
||||
@StateObject private var nfcReader = NFCReader()
|
||||
|
|
@ -72,10 +72,9 @@ struct Tools: View {
|
|||
|
||||
@available(iOS 18, *)
|
||||
#Preview {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
return Tools()
|
||||
Tools()
|
||||
.environmentObject(AccessoryManager.shared)
|
||||
.environment(\.managedObjectContext, context)
|
||||
.modelContainer(PersistenceController.preview.container)
|
||||
}
|
||||
|
||||
#if !targetEnvironment(macCatalyst)
|
||||
|
|
|
|||
|
|
@ -4,13 +4,13 @@
|
|||
//
|
||||
// Copyright (c) Garth Vander Houwen 6/27/22.
|
||||
//
|
||||
import CoreData
|
||||
import SwiftData
|
||||
import MeshtasticProtobufs
|
||||
import SwiftUI
|
||||
|
||||
struct UserConfig: View {
|
||||
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@Environment(\.modelContext) private var context
|
||||
@EnvironmentObject var accessoryManager: AccessoryManager
|
||||
@Environment(\.dismiss) private var goBack
|
||||
|
||||
|
|
@ -255,8 +255,7 @@ struct UserConfig: View {
|
|||
}
|
||||
|
||||
#Preview {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
return UserConfig(node: nil)
|
||||
UserConfig(node: nil)
|
||||
.environmentObject(AccessoryManager.shared)
|
||||
.environment(\.managedObjectContext, context)
|
||||
.modelContainer(PersistenceController.preview.container)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue