mirror of
https://github.com/meshtastic/Meshtastic-Apple.git
synced 2026-04-20 22:13:56 +00:00
Add map to new 3 column layout, comment out old views
This commit is contained in:
parent
dad6654374
commit
7ca655535a
5 changed files with 475 additions and 390 deletions
|
|
@ -19,16 +19,11 @@ struct ContentView: View {
|
|||
Label("bluetooth", systemImage: "antenna.radiowaves.left.and.right")
|
||||
}
|
||||
.tag(Tab.ble)
|
||||
NodeList()
|
||||
.tabItem {
|
||||
Label("nodes", systemImage: "flipphone")
|
||||
}
|
||||
.tag(Tab.nodes)
|
||||
NodeListSplit()
|
||||
.tabItem {
|
||||
Label("nodes", systemImage: "flipphone")
|
||||
}
|
||||
.tag(Tab.nodes2)
|
||||
.tag(Tab.nodes)
|
||||
NodeMap()
|
||||
.tabItem {
|
||||
Label("map", systemImage: "map")
|
||||
|
|
@ -56,6 +51,5 @@ enum Tab {
|
|||
case map
|
||||
case ble
|
||||
case nodes
|
||||
case nodes2
|
||||
case settings
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ struct NodeDetailItem: View {
|
|||
NavigationLink {
|
||||
PositionLog(node: node)
|
||||
} label: {
|
||||
Image(systemName: "building.columns")
|
||||
Image(systemName: "mappin.and.ellipse")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.font(.title)
|
||||
|
||||
|
|
|
|||
|
|
@ -10,9 +10,100 @@ import MapKit
|
|||
|
||||
struct NodeMapControl: View {
|
||||
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@EnvironmentObject var bleManager: BLEManager
|
||||
@Environment(\.colorScheme) var colorScheme: ColorScheme
|
||||
@AppStorage("meshMapType") private var meshMapType = 0
|
||||
@AppStorage("meshMapShowNodeHistory") private var meshMapShowNodeHistory = false
|
||||
@AppStorage("meshMapShowRouteLines") private var meshMapShowRouteLines = false
|
||||
@State private var selectedMapLayer: MapLayer = .standard
|
||||
@State var waypointCoordinate: WaypointCoordinate?
|
||||
@State var editingWaypoint: Int = 0
|
||||
@State private var customMapOverlay: MapViewSwiftUI.CustomMapOverlay? = MapViewSwiftUI.CustomMapOverlay(
|
||||
mapName: "offlinemap",
|
||||
tileType: "png",
|
||||
canReplaceMapContent: true
|
||||
)
|
||||
@FetchRequest(sortDescriptors: [NSSortDescriptor(key: "name", ascending: false)],
|
||||
predicate: NSPredicate(
|
||||
format: "expire == nil || expire >= %@", Date() as NSDate
|
||||
), animation: .none)
|
||||
private var waypoints: FetchedResults<WaypointEntity>
|
||||
@ObservedObject var node: NodeInfoEntity
|
||||
|
||||
var body: some View {
|
||||
Text("I am a map")
|
||||
|
||||
let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral?.num ?? -1, context: context)
|
||||
NavigationStack {
|
||||
GeometryReader { bounds in
|
||||
VStack {
|
||||
if node.hasPositions {
|
||||
ZStack {
|
||||
let positionArray = node.positions?.array as? [PositionEntity] ?? []
|
||||
let lastTenThousand = Array(positionArray.prefix(10000))
|
||||
// let todaysPositions = positionArray.filter { $0.time! >= Calendar.current.startOfDay(for: Date()) }
|
||||
ZStack {
|
||||
MapViewSwiftUI(onLongPress: { coord in
|
||||
waypointCoordinate = WaypointCoordinate(id: .init(), coordinate: coord, waypointId: 0)
|
||||
}, onWaypointEdit: { wpId in
|
||||
if wpId > 0 {
|
||||
waypointCoordinate = WaypointCoordinate(id: .init(), coordinate: nil, waypointId: Int64(wpId))
|
||||
}
|
||||
},
|
||||
selectedMapLayer: selectedMapLayer,
|
||||
positions: lastTenThousand,
|
||||
waypoints: Array(waypoints),
|
||||
userTrackingMode: MKUserTrackingMode.none,
|
||||
showNodeHistory: meshMapShowNodeHistory,
|
||||
showRouteLines: meshMapShowRouteLines,
|
||||
customMapOverlay: self.customMapOverlay
|
||||
)
|
||||
VStack(alignment: .leading) {
|
||||
Spacer()
|
||||
HStack(alignment: .bottom, spacing: 1) {
|
||||
Picker("Map Type", selection: $selectedMapLayer) {
|
||||
ForEach(MapLayer.allCases, id: \.self) { layer in
|
||||
if layer == MapLayer.offline && UserDefaults.enableOfflineMaps {
|
||||
Text(layer.localized)
|
||||
} else if layer != MapLayer.offline {
|
||||
Text(layer.localized)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: (selectedMapLayer)) { newMapLayer in
|
||||
UserDefaults.mapLayer = newMapLayer
|
||||
}
|
||||
.background(.thinMaterial, in: RoundedRectangle(cornerRadius: 12, style: .continuous))
|
||||
.pickerStyle(.menu)
|
||||
.padding(5)
|
||||
}
|
||||
}
|
||||
}
|
||||
.ignoresSafeArea(.all, edges: [.top, .leading, .trailing])
|
||||
.frame(idealWidth: bounds.size.width, minHeight: bounds.size.height / 1.65)
|
||||
}
|
||||
} else {
|
||||
HStack {
|
||||
}
|
||||
.padding([.top], 20)
|
||||
}
|
||||
}
|
||||
.edgesIgnoringSafeArea([.leading, .trailing])
|
||||
.sheet(item: $waypointCoordinate, content: { wpc in
|
||||
WaypointFormView(coordinate: wpc)
|
||||
.presentationDetents([.medium, .large])
|
||||
.presentationDragIndicator(.automatic)
|
||||
})
|
||||
.navigationBarTitle(String(node.user?.longName ?? "unknown".localized), displayMode: .inline)
|
||||
.navigationBarItems(trailing:
|
||||
ZStack {
|
||||
ConnectedDevice(
|
||||
bluetoothOn: bleManager.isSwitchedOn,
|
||||
deviceConnected: bleManager.connectedPeripheral != nil,
|
||||
name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?")
|
||||
})
|
||||
}
|
||||
.padding(.bottom, 2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,248 +1,248 @@
|
|||
/*
|
||||
Abstract:
|
||||
A view showing the details for a node.
|
||||
*/
|
||||
|
||||
import SwiftUI
|
||||
import WeatherKit
|
||||
import MapKit
|
||||
import CoreLocation
|
||||
|
||||
struct NodeDetail: View {
|
||||
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@EnvironmentObject var bleManager: BLEManager
|
||||
@Environment(\.colorScheme) var colorScheme: ColorScheme
|
||||
@AppStorage("meshMapType") private var meshMapType = 0
|
||||
@AppStorage("meshMapShowNodeHistory") private var meshMapShowNodeHistory = false
|
||||
@AppStorage("meshMapShowRouteLines") private var meshMapShowRouteLines = false
|
||||
@State private var selectedMapLayer: MapLayer = .standard
|
||||
@State var waypointCoordinate: WaypointCoordinate?
|
||||
@State var editingWaypoint: Int = 0
|
||||
@State private var loadedWeather: Bool = false
|
||||
@State private var showingDetailsPopover = false
|
||||
@State private var showingForecast = false
|
||||
@State private var showingShutdownConfirm: Bool = false
|
||||
@State private var showingRebootConfirm: Bool = false
|
||||
@State private var customMapOverlay: MapViewSwiftUI.CustomMapOverlay? = MapViewSwiftUI.CustomMapOverlay(
|
||||
mapName: "offlinemap",
|
||||
tileType: "png",
|
||||
canReplaceMapContent: true
|
||||
)
|
||||
var node: NodeInfoEntity
|
||||
@FetchRequest(sortDescriptors: [NSSortDescriptor(key: "name", ascending: false)],
|
||||
predicate: NSPredicate(
|
||||
format: "expire == nil || expire >= %@", Date() as NSDate
|
||||
), animation: .none)
|
||||
private var waypoints: FetchedResults<WaypointEntity>
|
||||
|
||||
/// The current weather condition for the city.
|
||||
@State private var condition: WeatherCondition?
|
||||
@State private var temperature: Measurement<UnitTemperature>?
|
||||
@State private var humidity: Int?
|
||||
@State private var symbolName: String = "cloud.fill"
|
||||
|
||||
@State private var attributionLink: URL?
|
||||
@State private var attributionLogo: URL?
|
||||
|
||||
var body: some View {
|
||||
|
||||
let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral?.num ?? -1, context: context)
|
||||
NavigationStack {
|
||||
GeometryReader { bounds in
|
||||
VStack {
|
||||
if node.hasPositions {
|
||||
ZStack {
|
||||
let positionArray = node.positions?.array as? [PositionEntity] ?? []
|
||||
let lastTenThousand = Array(positionArray.prefix(10000))
|
||||
// let todaysPositions = positionArray.filter { $0.time! >= Calendar.current.startOfDay(for: Date()) }
|
||||
ZStack {
|
||||
MapViewSwiftUI(onLongPress: { coord in
|
||||
waypointCoordinate = WaypointCoordinate(id: .init(), coordinate: coord, waypointId: 0)
|
||||
}, onWaypointEdit: { wpId in
|
||||
if wpId > 0 {
|
||||
waypointCoordinate = WaypointCoordinate(id: .init(), coordinate: nil, waypointId: Int64(wpId))
|
||||
}
|
||||
},
|
||||
selectedMapLayer: selectedMapLayer,
|
||||
positions: lastTenThousand,
|
||||
waypoints: Array(waypoints),
|
||||
userTrackingMode: MKUserTrackingMode.none,
|
||||
showNodeHistory: meshMapShowNodeHistory,
|
||||
showRouteLines: meshMapShowRouteLines,
|
||||
customMapOverlay: self.customMapOverlay
|
||||
)
|
||||
VStack(alignment: .leading) {
|
||||
Spacer()
|
||||
HStack(alignment: .bottom, spacing: 1) {
|
||||
Picker("Map Type", selection: $selectedMapLayer) {
|
||||
ForEach(MapLayer.allCases, id: \.self) { layer in
|
||||
if layer == MapLayer.offline && UserDefaults.enableOfflineMaps {
|
||||
Text(layer.localized)
|
||||
} else if layer != MapLayer.offline {
|
||||
Text(layer.localized)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: (selectedMapLayer)) { newMapLayer in
|
||||
UserDefaults.mapLayer = newMapLayer
|
||||
}
|
||||
.background(.thinMaterial, in: RoundedRectangle(cornerRadius: 12, style: .continuous))
|
||||
.pickerStyle(.menu)
|
||||
.padding(5)
|
||||
VStack {
|
||||
Label(temperature?.formatted(.measurement(width: .narrow)) ?? "??", systemImage: symbolName)
|
||||
.font(.caption)
|
||||
|
||||
Label("\(humidity ?? 0)%", systemImage: "humidity")
|
||||
.font(.caption2)
|
||||
}
|
||||
.padding(10)
|
||||
.background(.thinMaterial, in: RoundedRectangle(cornerRadius: 12, style: .continuous))
|
||||
.padding(5)
|
||||
#if targetEnvironment(macCatalyst)
|
||||
.popover(isPresented: $showingForecast,
|
||||
arrowEdge: .top) {
|
||||
Text("Today's Weather Forecast")
|
||||
.font(.title)
|
||||
.padding()
|
||||
let nodeLocation = node.positions?.lastObject as? PositionEntity
|
||||
NodeWeatherForecastView(location: CLLocation(latitude: nodeLocation?.nodeCoordinate!.latitude ?? LocationHelper.currentLocation.latitude, longitude: nodeLocation?.nodeCoordinate!.longitude ?? LocationHelper.currentLocation.longitude) )
|
||||
.frame(height: 250)
|
||||
}
|
||||
#else
|
||||
.sheet(isPresented: $showingForecast) {
|
||||
Text("Today's Weather Forecast")
|
||||
.font(.title)
|
||||
.padding()
|
||||
let nodeLocation = node.positions?.lastObject as? PositionEntity
|
||||
NodeWeatherForecastView(location: CLLocation(latitude: nodeLocation?.nodeCoordinate!.latitude ?? LocationHelper.currentLocation.latitude, longitude: nodeLocation?.nodeCoordinate!.longitude ?? LocationHelper.currentLocation.longitude) ).frame(height: 250)
|
||||
.presentationDetents([.medium])
|
||||
.presentationDragIndicator(.automatic)
|
||||
}
|
||||
#endif
|
||||
.gesture(
|
||||
LongPressGesture(minimumDuration: 0.5)
|
||||
.onEnded { _ in
|
||||
showingForecast = true
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
.ignoresSafeArea(.all, edges: [.top, .leading, .trailing])
|
||||
.frame(idealWidth: bounds.size.width, minHeight: bounds.size.height / 1.65)
|
||||
}
|
||||
} else {
|
||||
HStack {
|
||||
}
|
||||
.padding([.top], 20)
|
||||
}
|
||||
ScrollView {
|
||||
NodeInfoView(node: node)
|
||||
if self.bleManager.connectedPeripheral != nil && node.metadata != nil {
|
||||
HStack {
|
||||
if node.metadata?.canShutdown ?? false {
|
||||
|
||||
Button(action: {
|
||||
showingShutdownConfirm = true
|
||||
}) {
|
||||
Label("Power Off", systemImage: "power")
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.buttonBorderShape(.capsule)
|
||||
.controlSize(.large)
|
||||
.padding()
|
||||
.confirmationDialog(
|
||||
"are.you.sure",
|
||||
isPresented: $showingShutdownConfirm
|
||||
) {
|
||||
Button("Shutdown Node?", role: .destructive) {
|
||||
if !bleManager.sendShutdown(fromUser: connectedNode!.user!, toUser: node.user!, adminIndex: connectedNode!.myInfo!.adminIndex) {
|
||||
print("Shutdown Failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button(action: {
|
||||
showingRebootConfirm = true
|
||||
}) {
|
||||
Label("reboot", systemImage: "arrow.triangle.2.circlepath")
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.buttonBorderShape(.capsule)
|
||||
.controlSize(.large)
|
||||
.padding()
|
||||
.confirmationDialog("are.you.sure",
|
||||
isPresented: $showingRebootConfirm
|
||||
) {
|
||||
Button("reboot.node", role: .destructive) {
|
||||
if !bleManager.sendReboot(fromUser: connectedNode!.user!, toUser: node.user!, adminIndex: connectedNode!.myInfo!.adminIndex) {
|
||||
print("Reboot Failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(5)
|
||||
Divider()
|
||||
}
|
||||
if node.positions?.count ?? 0 > 0 {
|
||||
VStack {
|
||||
AsyncImage(url: attributionLogo) { image in
|
||||
image
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
} placeholder: {
|
||||
ProgressView()
|
||||
.controlSize(.mini)
|
||||
}
|
||||
.frame(height: 15)
|
||||
|
||||
Link("Other data sources", destination: attributionLink ?? URL(string: "https://weather-data.apple.com/legal-attribution.html")!)
|
||||
}
|
||||
.font(.footnote)
|
||||
}
|
||||
}
|
||||
}
|
||||
.edgesIgnoringSafeArea([.leading, .trailing])
|
||||
.sheet(item: $waypointCoordinate, content: { wpc in
|
||||
WaypointFormView(coordinate: wpc)
|
||||
.presentationDetents([.medium, .large])
|
||||
.presentationDragIndicator(.automatic)
|
||||
})
|
||||
.navigationBarTitle(String(node.user?.longName ?? "unknown".localized), displayMode: .inline)
|
||||
.navigationBarItems(trailing:
|
||||
ZStack {
|
||||
ConnectedDevice(
|
||||
bluetoothOn: bleManager.isSwitchedOn,
|
||||
deviceConnected: bleManager.connectedPeripheral != nil,
|
||||
name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?")
|
||||
})
|
||||
.task(id: node.num) {
|
||||
if !loadedWeather {
|
||||
do {
|
||||
if node.hasPositions {
|
||||
let mostRecent = node.positions?.lastObject as? PositionEntity
|
||||
let weather = try await WeatherService.shared.weather(for: mostRecent?.nodeLocation ?? CLLocation(latitude: LocationHelper.currentLocation.latitude, longitude: LocationHelper.currentLocation.longitude))
|
||||
condition = weather.currentWeather.condition
|
||||
temperature = weather.currentWeather.temperature
|
||||
humidity = Int(weather.currentWeather.humidity * 100)
|
||||
symbolName = weather.currentWeather.symbolName
|
||||
let attribution = try await WeatherService.shared.attribution
|
||||
attributionLink = attribution.legalPageURL
|
||||
attributionLogo = colorScheme == .light ? attribution.combinedMarkLightURL : attribution.combinedMarkDarkURL
|
||||
loadedWeather = true
|
||||
}
|
||||
} catch {
|
||||
print("Could not gather weather information...", error.localizedDescription)
|
||||
condition = .clear
|
||||
symbolName = "cloud.fill"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.bottom, 2)
|
||||
}
|
||||
}
|
||||
}
|
||||
///*
|
||||
// Abstract:
|
||||
// A view showing the details for a node.
|
||||
// */
|
||||
//
|
||||
//import SwiftUI
|
||||
//import WeatherKit
|
||||
//import MapKit
|
||||
//import CoreLocation
|
||||
//
|
||||
//struct NodeDetail: View {
|
||||
//
|
||||
// @Environment(\.managedObjectContext) var context
|
||||
// @EnvironmentObject var bleManager: BLEManager
|
||||
// @Environment(\.colorScheme) var colorScheme: ColorScheme
|
||||
// @AppStorage("meshMapType") private var meshMapType = 0
|
||||
// @AppStorage("meshMapShowNodeHistory") private var meshMapShowNodeHistory = false
|
||||
// @AppStorage("meshMapShowRouteLines") private var meshMapShowRouteLines = false
|
||||
// @State private var selectedMapLayer: MapLayer = .standard
|
||||
// @State var waypointCoordinate: WaypointCoordinate?
|
||||
// @State var editingWaypoint: Int = 0
|
||||
// @State private var loadedWeather: Bool = false
|
||||
// @State private var showingDetailsPopover = false
|
||||
// @State private var showingForecast = false
|
||||
// @State private var showingShutdownConfirm: Bool = false
|
||||
// @State private var showingRebootConfirm: Bool = false
|
||||
// @State private var customMapOverlay: MapViewSwiftUI.CustomMapOverlay? = MapViewSwiftUI.CustomMapOverlay(
|
||||
// mapName: "offlinemap",
|
||||
// tileType: "png",
|
||||
// canReplaceMapContent: true
|
||||
// )
|
||||
// var node: NodeInfoEntity
|
||||
// @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "name", ascending: false)],
|
||||
// predicate: NSPredicate(
|
||||
// format: "expire == nil || expire >= %@", Date() as NSDate
|
||||
// ), animation: .none)
|
||||
// private var waypoints: FetchedResults<WaypointEntity>
|
||||
//
|
||||
// /// The current weather condition for the city.
|
||||
// @State private var condition: WeatherCondition?
|
||||
// @State private var temperature: Measurement<UnitTemperature>?
|
||||
// @State private var humidity: Int?
|
||||
// @State private var symbolName: String = "cloud.fill"
|
||||
//
|
||||
// @State private var attributionLink: URL?
|
||||
// @State private var attributionLogo: URL?
|
||||
//
|
||||
// var body: some View {
|
||||
//
|
||||
// let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral?.num ?? -1, context: context)
|
||||
// NavigationStack {
|
||||
// GeometryReader { bounds in
|
||||
// VStack {
|
||||
// if node.hasPositions {
|
||||
// ZStack {
|
||||
// let positionArray = node.positions?.array as? [PositionEntity] ?? []
|
||||
// let lastTenThousand = Array(positionArray.prefix(10000))
|
||||
// // let todaysPositions = positionArray.filter { $0.time! >= Calendar.current.startOfDay(for: Date()) }
|
||||
// ZStack {
|
||||
// MapViewSwiftUI(onLongPress: { coord in
|
||||
// waypointCoordinate = WaypointCoordinate(id: .init(), coordinate: coord, waypointId: 0)
|
||||
// }, onWaypointEdit: { wpId in
|
||||
// if wpId > 0 {
|
||||
// waypointCoordinate = WaypointCoordinate(id: .init(), coordinate: nil, waypointId: Int64(wpId))
|
||||
// }
|
||||
// },
|
||||
// selectedMapLayer: selectedMapLayer,
|
||||
// positions: lastTenThousand,
|
||||
// waypoints: Array(waypoints),
|
||||
// userTrackingMode: MKUserTrackingMode.none,
|
||||
// showNodeHistory: meshMapShowNodeHistory,
|
||||
// showRouteLines: meshMapShowRouteLines,
|
||||
// customMapOverlay: self.customMapOverlay
|
||||
// )
|
||||
// VStack(alignment: .leading) {
|
||||
// Spacer()
|
||||
// HStack(alignment: .bottom, spacing: 1) {
|
||||
// Picker("Map Type", selection: $selectedMapLayer) {
|
||||
// ForEach(MapLayer.allCases, id: \.self) { layer in
|
||||
// if layer == MapLayer.offline && UserDefaults.enableOfflineMaps {
|
||||
// Text(layer.localized)
|
||||
// } else if layer != MapLayer.offline {
|
||||
// Text(layer.localized)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// .onChange(of: (selectedMapLayer)) { newMapLayer in
|
||||
// UserDefaults.mapLayer = newMapLayer
|
||||
// }
|
||||
// .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 12, style: .continuous))
|
||||
// .pickerStyle(.menu)
|
||||
// .padding(5)
|
||||
// VStack {
|
||||
// Label(temperature?.formatted(.measurement(width: .narrow)) ?? "??", systemImage: symbolName)
|
||||
// .font(.caption)
|
||||
//
|
||||
// Label("\(humidity ?? 0)%", systemImage: "humidity")
|
||||
// .font(.caption2)
|
||||
// }
|
||||
// .padding(10)
|
||||
// .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 12, style: .continuous))
|
||||
// .padding(5)
|
||||
// #if targetEnvironment(macCatalyst)
|
||||
// .popover(isPresented: $showingForecast,
|
||||
// arrowEdge: .top) {
|
||||
// Text("Today's Weather Forecast")
|
||||
// .font(.title)
|
||||
// .padding()
|
||||
// let nodeLocation = node.positions?.lastObject as? PositionEntity
|
||||
// NodeWeatherForecastView(location: CLLocation(latitude: nodeLocation?.nodeCoordinate!.latitude ?? LocationHelper.currentLocation.latitude, longitude: nodeLocation?.nodeCoordinate!.longitude ?? LocationHelper.currentLocation.longitude) )
|
||||
// .frame(height: 250)
|
||||
// }
|
||||
// #else
|
||||
// .sheet(isPresented: $showingForecast) {
|
||||
// Text("Today's Weather Forecast")
|
||||
// .font(.title)
|
||||
// .padding()
|
||||
// let nodeLocation = node.positions?.lastObject as? PositionEntity
|
||||
// NodeWeatherForecastView(location: CLLocation(latitude: nodeLocation?.nodeCoordinate!.latitude ?? LocationHelper.currentLocation.latitude, longitude: nodeLocation?.nodeCoordinate!.longitude ?? LocationHelper.currentLocation.longitude) ).frame(height: 250)
|
||||
// .presentationDetents([.medium])
|
||||
// .presentationDragIndicator(.automatic)
|
||||
// }
|
||||
// #endif
|
||||
// .gesture(
|
||||
// LongPressGesture(minimumDuration: 0.5)
|
||||
// .onEnded { _ in
|
||||
// showingForecast = true
|
||||
// }
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// .ignoresSafeArea(.all, edges: [.top, .leading, .trailing])
|
||||
// .frame(idealWidth: bounds.size.width, minHeight: bounds.size.height / 1.65)
|
||||
// }
|
||||
// } else {
|
||||
// HStack {
|
||||
// }
|
||||
// .padding([.top], 20)
|
||||
// }
|
||||
// ScrollView {
|
||||
// NodeInfoView(node: node)
|
||||
// if self.bleManager.connectedPeripheral != nil && node.metadata != nil {
|
||||
// HStack {
|
||||
// if node.metadata?.canShutdown ?? false {
|
||||
//
|
||||
// Button(action: {
|
||||
// showingShutdownConfirm = true
|
||||
// }) {
|
||||
// Label("Power Off", systemImage: "power")
|
||||
// }
|
||||
// .buttonStyle(.bordered)
|
||||
// .buttonBorderShape(.capsule)
|
||||
// .controlSize(.large)
|
||||
// .padding()
|
||||
// .confirmationDialog(
|
||||
// "are.you.sure",
|
||||
// isPresented: $showingShutdownConfirm
|
||||
// ) {
|
||||
// Button("Shutdown Node?", role: .destructive) {
|
||||
// if !bleManager.sendShutdown(fromUser: connectedNode!.user!, toUser: node.user!, adminIndex: connectedNode!.myInfo!.adminIndex) {
|
||||
// print("Shutdown Failed")
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// Button(action: {
|
||||
// showingRebootConfirm = true
|
||||
// }) {
|
||||
// Label("reboot", systemImage: "arrow.triangle.2.circlepath")
|
||||
// }
|
||||
// .buttonStyle(.bordered)
|
||||
// .buttonBorderShape(.capsule)
|
||||
// .controlSize(.large)
|
||||
// .padding()
|
||||
// .confirmationDialog("are.you.sure",
|
||||
// isPresented: $showingRebootConfirm
|
||||
// ) {
|
||||
// Button("reboot.node", role: .destructive) {
|
||||
// if !bleManager.sendReboot(fromUser: connectedNode!.user!, toUser: node.user!, adminIndex: connectedNode!.myInfo!.adminIndex) {
|
||||
// print("Reboot Failed")
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// .padding(5)
|
||||
// Divider()
|
||||
// }
|
||||
// if node.positions?.count ?? 0 > 0 {
|
||||
// VStack {
|
||||
// AsyncImage(url: attributionLogo) { image in
|
||||
// image
|
||||
// .resizable()
|
||||
// .scaledToFit()
|
||||
// } placeholder: {
|
||||
// ProgressView()
|
||||
// .controlSize(.mini)
|
||||
// }
|
||||
// .frame(height: 15)
|
||||
//
|
||||
// Link("Other data sources", destination: attributionLink ?? URL(string: "https://weather-data.apple.com/legal-attribution.html")!)
|
||||
// }
|
||||
// .font(.footnote)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// .edgesIgnoringSafeArea([.leading, .trailing])
|
||||
// .sheet(item: $waypointCoordinate, content: { wpc in
|
||||
// WaypointFormView(coordinate: wpc)
|
||||
// .presentationDetents([.medium, .large])
|
||||
// .presentationDragIndicator(.automatic)
|
||||
// })
|
||||
// .navigationBarTitle(String(node.user?.longName ?? "unknown".localized), displayMode: .inline)
|
||||
// .navigationBarItems(trailing:
|
||||
// ZStack {
|
||||
// ConnectedDevice(
|
||||
// bluetoothOn: bleManager.isSwitchedOn,
|
||||
// deviceConnected: bleManager.connectedPeripheral != nil,
|
||||
// name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?")
|
||||
// })
|
||||
// .task(id: node.num) {
|
||||
// if !loadedWeather {
|
||||
// do {
|
||||
// if node.hasPositions {
|
||||
// let mostRecent = node.positions?.lastObject as? PositionEntity
|
||||
// let weather = try await WeatherService.shared.weather(for: mostRecent?.nodeLocation ?? CLLocation(latitude: LocationHelper.currentLocation.latitude, longitude: LocationHelper.currentLocation.longitude))
|
||||
// condition = weather.currentWeather.condition
|
||||
// temperature = weather.currentWeather.temperature
|
||||
// humidity = Int(weather.currentWeather.humidity * 100)
|
||||
// symbolName = weather.currentWeather.symbolName
|
||||
// let attribution = try await WeatherService.shared.attribution
|
||||
// attributionLink = attribution.legalPageURL
|
||||
// attributionLogo = colorScheme == .light ? attribution.combinedMarkLightURL : attribution.combinedMarkDarkURL
|
||||
// loadedWeather = true
|
||||
// }
|
||||
// } catch {
|
||||
// print("Could not gather weather information...", error.localizedDescription)
|
||||
// condition = .clear
|
||||
// symbolName = "cloud.fill"
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// .padding(.bottom, 2)
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
|
|
|||
|
|
@ -1,136 +1,136 @@
|
|||
////
|
||||
//// NodeList.swift
|
||||
//// Meshtastic
|
||||
////
|
||||
//// Copyright(c) Garth Vander Houwen 8/7/21.
|
||||
////
|
||||
//
|
||||
// NodeList.swift
|
||||
// Meshtastic
|
||||
//// Abstract:
|
||||
//// A view showing a list of devices that have been seen on the mesh network from the perspective of the connected device.
|
||||
//
|
||||
// Copyright(c) Garth Vander Houwen 8/7/21.
|
||||
//import SwiftUI
|
||||
//import CoreLocation
|
||||
//
|
||||
|
||||
// Abstract:
|
||||
// A view showing a list of devices that have been seen on the mesh network from the perspective of the connected device.
|
||||
|
||||
import SwiftUI
|
||||
import CoreLocation
|
||||
|
||||
struct NodeList: View {
|
||||
|
||||
@State private var searchText = ""
|
||||
var nodesQuery: Binding<String> {
|
||||
Binding {
|
||||
searchText
|
||||
} set: { newValue in
|
||||
searchText = newValue
|
||||
nodes.nsPredicate = newValue.isEmpty ? nil : NSPredicate(format: "user.longName CONTAINS[c] %@ OR user.shortName CONTAINS[c] %@", newValue, newValue)
|
||||
}
|
||||
}
|
||||
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@EnvironmentObject var bleManager: BLEManager
|
||||
|
||||
@FetchRequest(
|
||||
sortDescriptors: [NSSortDescriptor(key: "lastHeard", ascending: false)],
|
||||
animation: .default)
|
||||
|
||||
private var nodes: FetchedResults<NodeInfoEntity>
|
||||
|
||||
@State private var selection: NodeInfoEntity? // Nothing selected by default.
|
||||
|
||||
var body: some View {
|
||||
|
||||
NavigationSplitView {
|
||||
let connectedNodeNum = Int(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral?.num ?? 0 : 0)
|
||||
let connectedNode = nodes.first(where: { $0.num == connectedNodeNum })
|
||||
List(nodes, id: \.self, selection: $selection) { node in
|
||||
if nodes.count == 0 {
|
||||
Text("no.nodes").font(.title)
|
||||
} else {
|
||||
NavigationLink(value: node) {
|
||||
let connected: Bool = (bleManager.connectedPeripheral != nil && bleManager.connectedPeripheral?.num ?? -1 == node.num)
|
||||
LazyVStack(alignment: .leading) {
|
||||
HStack {
|
||||
VStack(alignment: .leading) {
|
||||
CircleText(text: node.user?.shortName ?? "?", color: Color(UIColor(hex: UInt32(node.num))), circleSize: 65)
|
||||
.padding(.trailing, 5)
|
||||
let deviceMetrics = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0"))
|
||||
if deviceMetrics?.count ?? 0 >= 1 {
|
||||
let mostRecent = deviceMetrics?.lastObject as? TelemetryEntity
|
||||
BatteryLevelCompact(batteryLevel: mostRecent?.batteryLevel, font: .caption2, iconFont: .callout, color: .accentColor)
|
||||
}
|
||||
}
|
||||
VStack(alignment: .leading) {
|
||||
Text(node.user?.longName ?? "unknown".localized)
|
||||
.fontWeight(.medium)
|
||||
.font(.callout)
|
||||
if connected {
|
||||
HStack(alignment: .bottom) {
|
||||
Image(systemName: "antenna.radiowaves.left.and.right.circle.fill")
|
||||
.font(.footnote)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.foregroundColor(.green)
|
||||
Text("connected").font(.caption)
|
||||
}
|
||||
}
|
||||
HStack(alignment: .bottom) {
|
||||
Image(systemName: node.isOnline ? "checkmark.circle.fill" : "moon.circle.fill")
|
||||
.font(.footnote)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.foregroundColor(node.isOnline ? .green : .orange)
|
||||
LastHeardText(lastHeard: node.lastHeard)
|
||||
.font(.caption)
|
||||
}
|
||||
if node.positions?.count ?? 0 > 0 && (bleManager.connectedPeripheral != nil && bleManager.connectedPeripheral?.num ?? -1 != node.num) {
|
||||
HStack(alignment: .bottom) {
|
||||
let lastPostion = node.positions!.reversed()[0] as! PositionEntity
|
||||
let myCoord = CLLocation(latitude: LocationHelper.currentLocation.latitude, longitude: LocationHelper.currentLocation.longitude)
|
||||
if lastPostion.nodeCoordinate != nil && myCoord.coordinate.longitude != LocationHelper.DefaultLocation.longitude && myCoord.coordinate.latitude != LocationHelper.DefaultLocation.latitude {
|
||||
let nodeCoord = CLLocation(latitude: lastPostion.nodeCoordinate!.latitude, longitude: lastPostion.nodeCoordinate!.longitude)
|
||||
let metersAway = nodeCoord.distance(from: myCoord)
|
||||
Image(systemName: "lines.measurement.horizontal")
|
||||
.font(.footnote)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
DistanceText(meters: metersAway).font(.caption)
|
||||
}
|
||||
}
|
||||
}
|
||||
if node.channel > 0 {
|
||||
HStack(alignment: .bottom) {
|
||||
Image(systemName: "fibrechannel")
|
||||
.font(.footnote)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
Text("Channel: \(node.channel)")
|
||||
.font(.footnote)
|
||||
}
|
||||
}
|
||||
if !connected {
|
||||
HStack(alignment: .bottom) { let preset = ModemPresets(rawValue: Int(connectedNode?.loRaConfig?.modemPreset ?? 0))
|
||||
LoRaSignalStrengthMeter(snr: node.snr, rssi: node.rssi, preset: preset ?? ModemPresets.longFast, compact: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding([.top, .bottom])
|
||||
}
|
||||
}
|
||||
.listStyle(.plain)
|
||||
.navigationSplitViewColumnWidth(300)
|
||||
.navigationTitle(String.localizedStringWithFormat("nodes %@".localized, String(nodes.count)))
|
||||
.navigationBarItems(leading:
|
||||
MeshtasticLogo()
|
||||
)
|
||||
.onAppear {
|
||||
if self.bleManager.context == nil {
|
||||
self.bleManager.context = context
|
||||
}
|
||||
}
|
||||
} detail: {
|
||||
if let node = selection {
|
||||
NodeDetail(node: node)
|
||||
} else {
|
||||
Text("select.node")
|
||||
}
|
||||
}
|
||||
.searchable(text: nodesQuery, prompt: "Find a node")
|
||||
}
|
||||
}
|
||||
//struct NodeList: View {
|
||||
//
|
||||
// @State private var searchText = ""
|
||||
// var nodesQuery: Binding<String> {
|
||||
// Binding {
|
||||
// searchText
|
||||
// } set: { newValue in
|
||||
// searchText = newValue
|
||||
// nodes.nsPredicate = newValue.isEmpty ? nil : NSPredicate(format: "user.longName CONTAINS[c] %@ OR user.shortName CONTAINS[c] %@", newValue, newValue)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// @Environment(\.managedObjectContext) var context
|
||||
// @EnvironmentObject var bleManager: BLEManager
|
||||
//
|
||||
// @FetchRequest(
|
||||
// sortDescriptors: [NSSortDescriptor(key: "lastHeard", ascending: false)],
|
||||
// animation: .default)
|
||||
//
|
||||
// private var nodes: FetchedResults<NodeInfoEntity>
|
||||
//
|
||||
// @State private var selection: NodeInfoEntity? // Nothing selected by default.
|
||||
//
|
||||
// var body: some View {
|
||||
//
|
||||
// NavigationSplitView {
|
||||
// let connectedNodeNum = Int(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral?.num ?? 0 : 0)
|
||||
// let connectedNode = nodes.first(where: { $0.num == connectedNodeNum })
|
||||
// List(nodes, id: \.self, selection: $selection) { node in
|
||||
// if nodes.count == 0 {
|
||||
// Text("no.nodes").font(.title)
|
||||
// } else {
|
||||
// NavigationLink(value: node) {
|
||||
// let connected: Bool = (bleManager.connectedPeripheral != nil && bleManager.connectedPeripheral?.num ?? -1 == node.num)
|
||||
// LazyVStack(alignment: .leading) {
|
||||
// HStack {
|
||||
// VStack(alignment: .leading) {
|
||||
// CircleText(text: node.user?.shortName ?? "?", color: Color(UIColor(hex: UInt32(node.num))), circleSize: 65)
|
||||
// .padding(.trailing, 5)
|
||||
// let deviceMetrics = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0"))
|
||||
// if deviceMetrics?.count ?? 0 >= 1 {
|
||||
// let mostRecent = deviceMetrics?.lastObject as? TelemetryEntity
|
||||
// BatteryLevelCompact(batteryLevel: mostRecent?.batteryLevel, font: .caption2, iconFont: .callout, color: .accentColor)
|
||||
// }
|
||||
// }
|
||||
// VStack(alignment: .leading) {
|
||||
// Text(node.user?.longName ?? "unknown".localized)
|
||||
// .fontWeight(.medium)
|
||||
// .font(.callout)
|
||||
// if connected {
|
||||
// HStack(alignment: .bottom) {
|
||||
// Image(systemName: "antenna.radiowaves.left.and.right.circle.fill")
|
||||
// .font(.footnote)
|
||||
// .symbolRenderingMode(.hierarchical)
|
||||
// .foregroundColor(.green)
|
||||
// Text("connected").font(.caption)
|
||||
// }
|
||||
// }
|
||||
// HStack(alignment: .bottom) {
|
||||
// Image(systemName: node.isOnline ? "checkmark.circle.fill" : "moon.circle.fill")
|
||||
// .font(.footnote)
|
||||
// .symbolRenderingMode(.hierarchical)
|
||||
// .foregroundColor(node.isOnline ? .green : .orange)
|
||||
// LastHeardText(lastHeard: node.lastHeard)
|
||||
// .font(.caption)
|
||||
// }
|
||||
// if node.positions?.count ?? 0 > 0 && (bleManager.connectedPeripheral != nil && bleManager.connectedPeripheral?.num ?? -1 != node.num) {
|
||||
// HStack(alignment: .bottom) {
|
||||
// let lastPostion = node.positions!.reversed()[0] as! PositionEntity
|
||||
// let myCoord = CLLocation(latitude: LocationHelper.currentLocation.latitude, longitude: LocationHelper.currentLocation.longitude)
|
||||
// if lastPostion.nodeCoordinate != nil && myCoord.coordinate.longitude != LocationHelper.DefaultLocation.longitude && myCoord.coordinate.latitude != LocationHelper.DefaultLocation.latitude {
|
||||
// let nodeCoord = CLLocation(latitude: lastPostion.nodeCoordinate!.latitude, longitude: lastPostion.nodeCoordinate!.longitude)
|
||||
// let metersAway = nodeCoord.distance(from: myCoord)
|
||||
// Image(systemName: "lines.measurement.horizontal")
|
||||
// .font(.footnote)
|
||||
// .symbolRenderingMode(.hierarchical)
|
||||
// DistanceText(meters: metersAway).font(.caption)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// if node.channel > 0 {
|
||||
// HStack(alignment: .bottom) {
|
||||
// Image(systemName: "fibrechannel")
|
||||
// .font(.footnote)
|
||||
// .symbolRenderingMode(.hierarchical)
|
||||
// Text("Channel: \(node.channel)")
|
||||
// .font(.footnote)
|
||||
// }
|
||||
// }
|
||||
// if !connected {
|
||||
// HStack(alignment: .bottom) { let preset = ModemPresets(rawValue: Int(connectedNode?.loRaConfig?.modemPreset ?? 0))
|
||||
// LoRaSignalStrengthMeter(snr: node.snr, rssi: node.rssi, preset: preset ?? ModemPresets.longFast, compact: true)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// .frame(maxWidth: .infinity, alignment: .leading)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// .padding([.top, .bottom])
|
||||
// }
|
||||
// }
|
||||
// .listStyle(.plain)
|
||||
// .navigationSplitViewColumnWidth(300)
|
||||
// .navigationTitle(String.localizedStringWithFormat("nodes %@".localized, String(nodes.count)))
|
||||
// .navigationBarItems(leading:
|
||||
// MeshtasticLogo()
|
||||
// )
|
||||
// .onAppear {
|
||||
// if self.bleManager.context == nil {
|
||||
// self.bleManager.context = context
|
||||
// }
|
||||
// }
|
||||
// } detail: {
|
||||
// if let node = selection {
|
||||
// NodeDetail(node: node)
|
||||
// } else {
|
||||
// Text("select.node")
|
||||
// }
|
||||
// }
|
||||
// .searchable(text: nodesQuery, prompt: "Find a node")
|
||||
// }
|
||||
//}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue