From 919edac1f705d4bbd9a04719fbb99e0b6ed32a8a Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 14 Sep 2023 20:17:14 -0700 Subject: [PATCH] Add some empty content views, clean up node map --- .../Views/Nodes/Helpers/NodeMapSwiftUI.swift | 31 ++- Meshtastic/Views/Nodes/NodeList.swift | 2 +- Meshtastic/Views/Nodes/PositionLog.swift | 248 +++++++++--------- 3 files changed, 150 insertions(+), 131 deletions(-) diff --git a/Meshtastic/Views/Nodes/Helpers/NodeMapSwiftUI.swift b/Meshtastic/Views/Nodes/Helpers/NodeMapSwiftUI.swift index 36565c1d..748c67bb 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeMapSwiftUI.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeMapSwiftUI.swift @@ -10,7 +10,7 @@ import CoreLocation import MapKit import WeatherKit -@available(iOS 17.0, *) +@available(iOS 17.0, macOS 14.0, *) struct NodeMapSwiftUI: View { @Environment(\.managedObjectContext) var context @@ -23,11 +23,11 @@ struct NodeMapSwiftUI: View { @State private var position = MapCameraPosition.automatic @State private var scene: MKLookAroundScene? @State private var showUserLocation: Bool = false + @State var selected: PositionEntity? /// Unused map items @State private var selectedMapLayer: MapLayer = .standard @State var waypointCoordinate: WaypointCoordinate? @State var editingWaypoint: Int = 0 - /// Data @ObservedObject var node: NodeInfoEntity @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "name", ascending: false)], @@ -36,7 +36,6 @@ struct NodeMapSwiftUI: View { ), animation: .none) private var waypoints: FetchedResults - var body: some View { let nodeColor = UIColor(hex: UInt32(node.num)) let positionArray = node.positions?.array as? [PositionEntity] ?? [] @@ -45,7 +44,7 @@ struct NodeMapSwiftUI: View { return position.nodeCoordinate ?? LocationHelper.DefaultLocation }) - if mostRecent != nil { + if node.hasPositions { ZStack { Map(position: $position, bounds: MapCameraBounds(minimumDistance: 100, maximumDistance: .infinity), scope: mapScope) { /// Route Lines @@ -72,7 +71,6 @@ struct NodeMapSwiftUI: View { Circle() .foregroundStyle(Color(nodeColor.lighter()).opacity(0.4)) .frame(width: 60, height: 60) - if pf.contains(.Heading) { Image(systemName: pf.contains(.Speed) && position.speed > 1 ? "location.north.fill" : "location.north") .symbolEffect(.pulse.byLayer) @@ -81,6 +79,11 @@ struct NodeMapSwiftUI: View { .background(Color(UIColor(hex: UInt32(node.num)).darker())) .clipShape(Circle()) .rotationEffect(.degrees(Double(position.heading))) + .onTapGesture { + selected = (selected == position ? nil : position) // <-- here + print("tapity tap tap \(position.time)") + } + } else { Image(systemName: "flipphone") .symbolEffect(.pulse.byLayer) @@ -88,6 +91,11 @@ struct NodeMapSwiftUI: View { .foregroundStyle(Color(nodeColor).isLight() ? .black : .white) .background(Color(UIColor(hex: UInt32(node.num)).darker())) .clipShape(Circle()) + .onTapGesture { + selected = (selected == position ? nil : position) // <-- here + print("tapity tap tap \(position.time)") + } + } } else { if showNodeHistory { @@ -134,7 +142,7 @@ struct NodeMapSwiftUI: View { } .controlSize(.regular) .overlay(alignment: .bottom) { - if scene != nil { + if scene != nil{ LookAroundPreview(scene: $scene, allowsNavigation: false, badgePosition: .bottomTrailing) .frame(height: 175) .clipShape(RoundedRectangle(cornerRadius: 12)) @@ -143,11 +151,8 @@ struct NodeMapSwiftUI: View { } } .onChange(of: node) { - print("Node changed") let mostRecent = node.positions?.lastObject as? PositionEntity position = .camera(MapCamera(centerCoordinate: mostRecent!.coordinate, distance: 1500, heading: 0, pitch: 60)) - } - .onChange(of: mostRecent) { if let mostRecent { Task { scene = try? await fetchScene(for: mostRecent.coordinate) @@ -155,15 +160,19 @@ struct NodeMapSwiftUI: View { } } .onAppear { + UIApplication.shared.isIdleTimerDisabled = true if self.scene == nil { Task { scene = try? await fetchScene(for: mostRecent!.coordinate) } } } + .onDisappear { + UIApplication.shared.isIdleTimerDisabled = false + } } - .navigationBarTitle(String("Node Map " + (node.user?.shortName ?? "unknown".localized)), displayMode: .inline) + .navigationBarTitle(String((node.user?.shortName ?? "unknown".localized) + (" \(node.positions?.count ?? 0) points")), displayMode: .inline) .navigationBarItems(trailing: ZStack { ConnectedDevice( @@ -171,6 +180,8 @@ struct NodeMapSwiftUI: View { deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") }) + } else { + ContentUnavailableView("No Positions", systemImage: "mappin.slash") } } diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index 59108fa4..7a36fe44 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -28,7 +28,7 @@ struct NodeList: View { @EnvironmentObject var bleManager: BLEManager @FetchRequest( - sortDescriptors: [NSSortDescriptor(key: "lastHeard", ascending: false)], + sortDescriptors: [NSSortDescriptor(key: "user.vip", ascending: false), NSSortDescriptor(key: "lastHeard", ascending: false)], animation: .default) private var nodes: FetchedResults diff --git a/Meshtastic/Views/Nodes/PositionLog.swift b/Meshtastic/Views/Nodes/PositionLog.swift index 0ccbd6cd..e0b5a78f 100644 --- a/Meshtastic/Views/Nodes/PositionLog.swift +++ b/Meshtastic/Views/Nodes/PositionLog.swift @@ -23,141 +23,149 @@ struct PositionLog: View { var body: some View { NavigationStack { - - let localeDateFormat = DateFormatter.dateFormat(fromTemplate: "yyMMddjmma", options: 0, locale: Locale.current) - 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] ?? [] - Table(positions, sortOrder: $sortOrder) { - TableColumn("Latitude") { position in - Text(String(format: "%.5f", position.latitude ?? 0)) - } - .width(min: 120) - TableColumn("Longitude") { position in - Text(String(format: "%.5f", position.longitude ?? 0)) - } - .width(min: 120) - TableColumn("Altitude") { position in - let altitude = Measurement(value: Double(position.altitude), unit: UnitLength.meters) - Text(String(altitude.formatted())) - } - TableColumn("Sats") { position in - Text(String(position.satsInView)) - } - TableColumn("Speed") { position in - let speed = Measurement(value: Double(position.speed), unit: UnitSpeed.kilometersPerHour) - Text(speed.formatted()) - } - TableColumn("Heading") { position in - Text("\(position.heading)°") - } - TableColumn("SNR") { position in - Text("\(String(format: "%.2f", position.snr)) dB") - } - TableColumn("Time Stamp") { position in - Text(position.time?.formattedDate(format: dateFormatString) ?? "unknown.age".localized) - } - .width(min: 180) - } - - } else { - ScrollView { - // Use a grid on iOS as a table only shows a single column - let columns = [ - GridItem(spacing: 0.1), - GridItem(spacing: 0.1), - GridItem(.flexible(minimum: 35, maximum: 40), spacing: 0.1), - GridItem(.flexible(minimum: 45, maximum: 50), spacing: 0.1), - GridItem(spacing: 0) - ] - LazyVGrid(columns: columns, alignment: .leading, spacing: 1) { - GridRow { - Text("Latitude") - .font(.caption2) - .fontWeight(.bold) - Text("Longitude") - .font(.caption2) - .fontWeight(.bold) - Text("Sats") - .font(.caption2) - .fontWeight(.bold) - Text("Alt") - .font(.caption2) - .fontWeight(.bold) - Text("timestamp") - .font(.caption2) - .fontWeight(.bold) + if node.hasPositions { + let localeDateFormat = DateFormatter.dateFormat(fromTemplate: "yyMMddjmma", options: 0, locale: Locale.current) + 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] ?? [] + Table(positions, sortOrder: $sortOrder) { + TableColumn("Latitude") { position in + Text(String(format: "%.5f", position.latitude ?? 0)) } - ForEach(node.positions!.reversed() as! [PositionEntity], id: \.self) { (mappin: PositionEntity) in - let altitude = Measurement(value: Double(mappin.altitude), unit: UnitLength.meters) + .width(min: 120) + TableColumn("Longitude") { position in + Text(String(format: "%.5f", position.longitude ?? 0)) + } + .width(min: 120) + TableColumn("Altitude") { position in + let altitude = Measurement(value: Double(position.altitude), unit: UnitLength.meters) + Text(String(altitude.formatted())) + } + TableColumn("Sats") { position in + Text(String(position.satsInView)) + } + TableColumn("Speed") { position in + let speed = Measurement(value: Double(position.speed), unit: UnitSpeed.kilometersPerHour) + Text(speed.formatted()) + } + TableColumn("Heading") { position in + Text("\(position.heading)°") + } + TableColumn("SNR") { position in + Text("\(String(format: "%.2f", position.snr)) dB") + } + TableColumn("Time Stamp") { position in + Text(position.time?.formattedDate(format: dateFormatString) ?? "unknown.age".localized) + } + .width(min: 180) + } + + } else { + ScrollView { + // Use a grid on iOS as a table only shows a single column + let columns = [ + GridItem(spacing: 0.1), + GridItem(spacing: 0.1), + GridItem(.flexible(minimum: 35, maximum: 40), spacing: 0.1), + GridItem(.flexible(minimum: 45, maximum: 50), spacing: 0.1), + GridItem(spacing: 0) + ] + LazyVGrid(columns: columns, alignment: .leading, spacing: 1) { GridRow { - Text(String(format: "%.5f", mappin.latitude ?? 0)) + Text("Latitude") .font(.caption2) - Text(String(format: "%.5f", mappin.longitude ?? 0)) + .fontWeight(.bold) + Text("Longitude") .font(.caption2) - Text(String(mappin.satsInView)) + .fontWeight(.bold) + Text("Sats") .font(.caption2) - Text(altitude.formatted()) + .fontWeight(.bold) + Text("Alt") .font(.caption2) - Text(mappin.time?.formattedDate(format: dateFormatString) ?? "unknown.age".localized) + .fontWeight(.bold) + Text("timestamp") .font(.caption2) + .fontWeight(.bold) + } + ForEach(node.positions!.reversed() as! [PositionEntity], id: \.self) { (mappin: PositionEntity) in + let altitude = Measurement(value: Double(mappin.altitude), unit: UnitLength.meters) + GridRow { + Text(String(format: "%.5f", mappin.latitude ?? 0)) + .font(.caption2) + Text(String(format: "%.5f", mappin.longitude ?? 0)) + .font(.caption2) + Text(String(mappin.satsInView)) + .font(.caption2) + Text(altitude.formatted()) + .font(.caption2) + Text(mappin.time?.formattedDate(format: dateFormatString) ?? "unknown.age".localized) + .font(.caption2) + } } } } + .padding(.leading) } - .padding(.leading) - } - HStack { - Button(role: .destructive) { - isPresentingClearLogConfirm = true - } label: { - Label("clear.log", systemImage: "trash.fill") - } - .buttonStyle(.bordered) - .buttonBorderShape(.capsule) - .controlSize(.large) - .padding(.bottom) - .padding(.leading) - .confirmationDialog( - "are.you.sure", - isPresented: $isPresentingClearLogConfirm, - titleVisibility: .visible - ) { - Button("Delete all positions?", role: .destructive) { - if clearPositions(destNum: node.num, context: context) { - print("Successfully Cleared Position Log") - } else { - print("Clear Position Log Failed") + HStack { + Button(role: .destructive) { + isPresentingClearLogConfirm = true + } label: { + Label("clear.log", systemImage: "trash.fill") + } + .buttonStyle(.bordered) + .buttonBorderShape(.capsule) + .controlSize(.large) + .padding(.bottom) + .padding(.leading) + .confirmationDialog( + "are.you.sure", + isPresented: $isPresentingClearLogConfirm, + titleVisibility: .visible + ) { + Button("Delete all positions?", role: .destructive) { + if clearPositions(destNum: node.num, context: context) { + print("Successfully Cleared Position Log") + } else { + print("Clear Position Log Failed") + } } } - } - Button { - exportString = positionToCsvFile(positions: node.positions!.array as? [PositionEntity] ?? []) - isExporting = true - } label: { - Label("save", systemImage: "square.and.arrow.down") - } - .buttonStyle(.bordered) - .buttonBorderShape(.capsule) - .controlSize(.large) - .padding(.bottom) - .padding(.trailing) - } - .fileExporter( - isPresented: $isExporting, - document: CsvDocument(emptyCsv: exportString), - contentType: .commaSeparatedText, - defaultFilename: String("\(node.user?.longName ?? "Node") Position Log"), - onCompletion: { result in - if case .success = result { - print("Position log download succeeded.") - self.isExporting = false - } else { - print("Position log download failed: \(result).") + Button { + exportString = positionToCsvFile(positions: node.positions!.array as? [PositionEntity] ?? []) + isExporting = true + } label: { + Label("save", systemImage: "square.and.arrow.down") } + .buttonStyle(.bordered) + .buttonBorderShape(.capsule) + .controlSize(.large) + .padding(.bottom) + .padding(.trailing) } - ) + .fileExporter( + isPresented: $isExporting, + document: CsvDocument(emptyCsv: exportString), + contentType: .commaSeparatedText, + defaultFilename: String("\(node.user?.longName ?? "Node") Position Log"), + onCompletion: { result in + if case .success = result { + print("Position log download succeeded.") + self.isExporting = false + } else { + print("Position log download failed: \(result).") + } + } + ) + + } else { + if #available (iOS 17, *) { + ContentUnavailableView("No Positions", systemImage: "mappin.slash") + } else { + Text("No Positions") + } + } } .navigationTitle("Position Log \(node.positions?.count ?? 0) Points") .navigationBarItems(