From fc0e15245500756ca590c519d908e87d0a88f8b3 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 15 Sep 2023 18:40:52 -0700 Subject: [PATCH] Tips and empty content views --- Meshtastic.xcodeproj/project.pbxproj | 4 + Meshtastic/Tips/BluetoothTips.swift | 2 +- Meshtastic/Tips/MessagesTips.swift | 29 ++ Meshtastic/Views/Bluetooth/Connect.swift | 2 - Meshtastic/Views/Messages/Messages.swift | 6 + Meshtastic/Views/Nodes/DeviceMetricsLog.swift | 328 +++++++++--------- .../Views/Nodes/EnvironmentMetricsLog.swift | 321 ++++++++--------- .../Views/Nodes/Helpers/NodeMapSwiftUI.swift | 2 +- Meshtastic/Views/Settings/ShareChannels.swift | 8 +- 9 files changed, 378 insertions(+), 324 deletions(-) create mode 100644 Meshtastic/Tips/MessagesTips.swift diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 4277748c..d2f91d02 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -117,6 +117,7 @@ DDB8F4102A9EE5B400230ECE /* Messages.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB8F40F2A9EE5B400230ECE /* Messages.swift */; }; DDB8F4122A9EE5DD00230ECE /* UserList.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB8F4112A9EE5DD00230ECE /* UserList.swift */; }; DDB8F4142A9EE5F000230ECE /* ChannelList.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB8F4132A9EE5F000230ECE /* ChannelList.swift */; }; + DDC1B81A2AB5377B00C71E39 /* MessagesTips.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC1B8192AB5377B00C71E39 /* MessagesTips.swift */; }; DDC2E15826CE248E0042C5E4 /* MeshtasticApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC2E15726CE248E0042C5E4 /* MeshtasticApp.swift */; }; DDC2E15C26CE248F0042C5E4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DDC2E15B26CE248F0042C5E4 /* Assets.xcassets */; }; DDC2E15F26CE248F0042C5E4 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DDC2E15E26CE248F0042C5E4 /* Preview Assets.xcassets */; }; @@ -329,6 +330,7 @@ DDB8F4112A9EE5DD00230ECE /* UserList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserList.swift; sourceTree = ""; }; DDB8F4132A9EE5F000230ECE /* ChannelList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelList.swift; sourceTree = ""; }; DDBA45EC299ED78100DEEDDC /* MeshtasticDataModelV8.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV8.xcdatamodel; sourceTree = ""; }; + DDC1B8192AB5377B00C71E39 /* MessagesTips.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessagesTips.swift; sourceTree = ""; }; DDC2E15426CE248E0042C5E4 /* Meshtastic.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Meshtastic.app; sourceTree = BUILT_PRODUCTS_DIR; }; DDC2E15726CE248E0042C5E4 /* MeshtasticApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshtasticApp.swift; sourceTree = ""; }; DDC2E15B26CE248F0042C5E4 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = ../Assets.xcassets; sourceTree = ""; }; @@ -577,6 +579,7 @@ children = ( DD77093A2AA1ABB8007A8BF0 /* BluetoothTips.swift */, DD77093C2AA1AFA3007A8BF0 /* ChannelTips.swift */, + DDC1B8192AB5377B00C71E39 /* MessagesTips.swift */, ); path = Tips; sourceTree = ""; @@ -1150,6 +1153,7 @@ DDA1C48E28DB49D3009933EC /* ChannelRoles.swift in Sources */, DD8ED9C8289CE4B900B3B0AB /* RoutingError.swift in Sources */, DD5E5202298EE33B00D21B61 /* admin.pb.swift in Sources */, + DDC1B81A2AB5377B00C71E39 /* MessagesTips.swift in Sources */, DD964FC62975DBFD007C176F /* QueryCoreData.swift in Sources */, DDB75A112A059258006ED576 /* Url.swift in Sources */, DD8169FB271F1F3A00F4AB02 /* MeshLog.swift in Sources */, diff --git a/Meshtastic/Tips/BluetoothTips.swift b/Meshtastic/Tips/BluetoothTips.swift index 6d540744..630f5216 100644 --- a/Meshtastic/Tips/BluetoothTips.swift +++ b/Meshtastic/Tips/BluetoothTips.swift @@ -16,7 +16,7 @@ struct BluetoothConnectionTip: Tip { return "tip-bluetooth-connect" } var title: Text { - Text("Connected LoRa Radio Info") + Text("Connected LoRa Radio") } var message: Text? { diff --git a/Meshtastic/Tips/MessagesTips.swift b/Meshtastic/Tips/MessagesTips.swift new file mode 100644 index 00000000..1360e553 --- /dev/null +++ b/Meshtastic/Tips/MessagesTips.swift @@ -0,0 +1,29 @@ +// +// MessagesTips.swift +// Meshtastic +// +// Copyright(c) Garth Vander Houwen 9/15/23. +// +import SwiftUI +#if canImport(TipKit) +import TipKit +#endif + +@available(iOS 17.0, macOS 14.0, *) +struct MessagesTip: Tip { + + var id: String { + return "tip-messages" + } + var title: Text { + Text("Messages") + } + + var message: Text? { + Text("You can send and receive channel (group chats) and direct messages. From any message you can long press to see available actions like copy, reply, tapback and delete as well as delivery details.") + } + + var image: Image? { + Image(systemName: "questionmark.circle") + } +} diff --git a/Meshtastic/Views/Bluetooth/Connect.swift b/Meshtastic/Views/Bluetooth/Connect.swift index 642a8780..d879667a 100644 --- a/Meshtastic/Views/Bluetooth/Connect.swift +++ b/Meshtastic/Views/Bluetooth/Connect.swift @@ -43,7 +43,6 @@ struct Connect: View { }) } var body: some View { - NavigationStack { VStack { List { @@ -90,7 +89,6 @@ struct Connect: View { .foregroundColor(Color.gray) .padding([.top, .bottom]) .swipeActions { - Button(role: .destructive) { if bleManager.connectedPeripheral != nil && bleManager.connectedPeripheral.peripheral.state == CBPeripheralState.connected { bleManager.disconnectPeripheral(reconnect: false) diff --git a/Meshtastic/Views/Messages/Messages.swift b/Meshtastic/Views/Messages/Messages.swift index c7cb7917..c4bd1f89 100644 --- a/Meshtastic/Views/Messages/Messages.swift +++ b/Meshtastic/Views/Messages/Messages.swift @@ -7,6 +7,9 @@ import SwiftUI import CoreData +#if canImport(TipKit) +import TipKit +#endif struct Messages: View { @@ -52,6 +55,9 @@ struct Messages: View { .font(.title2) .badge(appState.unreadDirectMessages) } + if #available(iOS 17.0, macOS 14.0, *) { + TipView(MessagesTip(), arrowEdge: .top) + } } .navigationTitle("messages") .navigationBarTitleDisplayMode(.large) diff --git a/Meshtastic/Views/Nodes/DeviceMetricsLog.swift b/Meshtastic/Views/Nodes/DeviceMetricsLog.swift index adf91aa8..1dab8c04 100644 --- a/Meshtastic/Views/Nodes/DeviceMetricsLog.swift +++ b/Meshtastic/Views/Nodes/DeviceMetricsLog.swift @@ -22,184 +22,192 @@ struct DeviceMetricsLog: View { @ObservedObject var node: NodeInfoEntity var body: some 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 chartData = deviceMetrics + .filter { $0.time != nil && $0.time! >= oneWeekAgo! } + .sorted { $0.time! < $1.time! } + if chartData.count > 0 { + GroupBox(label: Label("\(deviceMetrics.count) Readings Total", systemImage: "chart.xyaxis.line")) { - 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 chartData = deviceMetrics - .filter { $0.time != nil && $0.time! >= oneWeekAgo! } - .sorted { $0.time! < $1.time! } - - if chartData.count > 0 { - GroupBox(label: Label("\(deviceMetrics.count) Readings Total", systemImage: "chart.xyaxis.line")) { + Chart { - Chart { + ForEach(chartData, id: \.self) { point in - ForEach(chartData, id: \.self) { point in + Plot { + LineMark( + x: .value("x", point.time!), + y: .value("y", point.batteryLevel) + ) + } + .accessibilityLabel("Line Series") + .accessibilityValue("X: \(point.time!), Y: \(point.batteryLevel)") + .foregroundStyle(batteryChartColor) + .interpolationMethod(.catmullRom(alpha: 1.0)) - Plot { - LineMark( - x: .value("x", point.time!), - y: .value("y", point.batteryLevel) - ) + Plot { + PointMark( + x: .value("x", point.time!), + y: .value("y", point.channelUtilization) + ) + } + .accessibilityLabel("Line Series") + .accessibilityValue("X: \(point.time!), Y: \(point.channelUtilization)") + .foregroundStyle(channelUtilizationChartColor) + + RuleMark(y: .value("Limit", 10)) + .lineStyle(StrokeStyle(lineWidth: 1, dash: [5, 10])) + .foregroundStyle(airtimeChartColor) + + Plot { + PointMark( + x: .value("x", point.time!), + y: .value("y", point.airUtilTx) + ) + } + .accessibilityLabel("Line Series") + .accessibilityValue("X: \(point.time!), Y: \(point.airUtilTx)") + .foregroundStyle(airtimeChartColor) + } } - .accessibilityLabel("Line Series") - .accessibilityValue("X: \(point.time!), Y: \(point.batteryLevel)") - .foregroundStyle(batteryChartColor) - .interpolationMethod(.catmullRom(alpha: 1.0)) - - Plot { - PointMark( - x: .value("x", point.time!), - y: .value("y", point.channelUtilization) - ) - } - .accessibilityLabel("Line Series") - .accessibilityValue("X: \(point.time!), Y: \(point.channelUtilization)") - .foregroundStyle(channelUtilizationChartColor) - - RuleMark(y: .value("Limit", 10)) - .lineStyle(StrokeStyle(lineWidth: 1, dash: [5, 10])) - .foregroundStyle(airtimeChartColor) - - Plot { - PointMark( - x: .value("x", point.time!), - y: .value("y", point.airUtilTx) - ) - } - .accessibilityLabel("Line Series") - .accessibilityValue("X: \(point.time!), Y: \(point.airUtilTx)") - .foregroundStyle(airtimeChartColor) + .chartXAxis(content: { + AxisMarks(position: .top) + }) + .chartXAxis(.automatic) + .chartYScale(domain: 0...100) + .chartForegroundStyleScale([ + "Battery Level": .blue, + "Channel Utilization": .green, + "Airtime": .orange + ]) + .chartLegend(position: .automatic, alignment: .bottom) } + .frame(minHeight: 250) } - .chartXAxis(content: { - AxisMarks(position: .top) - }) - .chartXAxis(.automatic) - .chartYScale(domain: 0...100) - .chartForegroundStyleScale([ - "Battery Level": .blue, - "Channel Utilization": .green, - "Airtime": .orange - ]) - .chartLegend(position: .automatic, alignment: .bottom) - } - .frame(minHeight: 250) - } - 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 || UIDevice.current.userInterfaceIdiom == .mac { - // Add a table for mac and ipad - // Table(Array(deviceMetrics),id: \.self) { - Table(deviceMetrics) { - TableColumn("battery.level") { dm in - if dm.batteryLevel > 100 { - Text("Powered") - } else { - Text("\(String(dm.batteryLevel))%") - } - } - TableColumn("voltage") { dm in - Text("\(String(format: "%.2f", dm.voltage))") - } - TableColumn("channel.utilization") { dm in - Text("\(String(format: "%.2f", dm.channelUtilization))%") - } - TableColumn("airtime") { dm in - Text("\(String(format: "%.2f", dm.airUtilTx))%") - } - TableColumn("timestamp") { dm in - Text(dm.time?.formattedDate(format: dateFormatString) ?? "unknown.age".localized) - } - .width(min: 180) - } - } else { - ScrollView { - let columns = [ - GridItem(.flexible(minimum: 30, maximum: 45), spacing: 0.1), - GridItem(.flexible(minimum: 30, maximum: 50), spacing: 0.1), - GridItem(.flexible(minimum: 30, maximum: 70), spacing: 0.1), - GridItem(.flexible(minimum: 30, maximum: 65), spacing: 0.1), - GridItem(.flexible(minimum: 130, maximum: 200), spacing: 0.1) - ] - LazyVGrid(columns: columns, alignment: .leading, spacing: 1) { - GridRow { - Text("Batt") - .font(.caption) - .fontWeight(.bold) - Text("Volt") - .font(.caption) - .fontWeight(.bold) - Text("ChUtil") - .font(.caption) - .fontWeight(.bold) - Text("AirTm") - .font(.caption) - .fontWeight(.bold) - Text("timestamp") - .font(.caption) - .fontWeight(.bold) - } - ForEach(deviceMetrics) { dm in - GridRow { + 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 || UIDevice.current.userInterfaceIdiom == .mac { + // Add a table for mac and ipad + // Table(Array(deviceMetrics),id: \.self) { + Table(deviceMetrics) { + TableColumn("battery.level") { dm in if dm.batteryLevel > 100 { - Text("PWD") - .font(.caption) + Text("Powered") } else { Text("\(String(dm.batteryLevel))%") - .font(.caption) } - Text(String(dm.voltage)) - .font(.caption) + } + TableColumn("voltage") { dm in + Text("\(String(format: "%.2f", dm.voltage))") + } + TableColumn("channel.utilization") { dm in Text("\(String(format: "%.2f", dm.channelUtilization))%") - .font(.caption) + } + TableColumn("airtime") { dm in Text("\(String(format: "%.2f", dm.airUtilTx))%") - .font(.caption) + } + TableColumn("timestamp") { dm in Text(dm.time?.formattedDate(format: dateFormatString) ?? "unknown.age".localized) - .font(.caption) + } + .width(min: 180) + } + } else { + ScrollView { + let columns = [ + GridItem(.flexible(minimum: 30, maximum: 45), spacing: 0.1), + GridItem(.flexible(minimum: 30, maximum: 50), spacing: 0.1), + GridItem(.flexible(minimum: 30, maximum: 70), spacing: 0.1), + GridItem(.flexible(minimum: 30, maximum: 65), spacing: 0.1), + GridItem(.flexible(minimum: 130, maximum: 200), spacing: 0.1) + ] + LazyVGrid(columns: columns, alignment: .leading, spacing: 1) { + GridRow { + Text("Batt") + .font(.caption) + .fontWeight(.bold) + Text("Volt") + .font(.caption) + .fontWeight(.bold) + Text("ChUtil") + .font(.caption) + .fontWeight(.bold) + Text("AirTm") + .font(.caption) + .fontWeight(.bold) + Text("timestamp") + .font(.caption) + .fontWeight(.bold) + } + ForEach(deviceMetrics) { dm in + GridRow { + if dm.batteryLevel > 100 { + Text("PWD") + .font(.caption) + } else { + Text("\(String(dm.batteryLevel))%") + .font(.caption) + } + Text(String(dm.voltage)) + .font(.caption) + Text("\(String(format: "%.2f", dm.channelUtilization))%") + .font(.caption) + Text("\(String(format: "%.2f", dm.airUtilTx))%") + .font(.caption) + Text(dm.time?.formattedDate(format: dateFormatString) ?? "unknown.age".localized) + .font(.caption) + } + } + } + .padding(.leading, 15) + .padding(.trailing, 5) + } + } + 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("device.metrics.delete", role: .destructive) { + if clearTelemetry(destNum: node.num, metricsType: 0, context: context) { + print("Cleared Device Metrics for \(node.num)") + } else { + print("Clear Device Metrics Log Failed") + } } } - } - .padding(.leading, 15) - .padding(.trailing, 5) - } - } - 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("device.metrics.delete", role: .destructive) { - if clearTelemetry(destNum: node.num, metricsType: 0, context: context) { - print("Cleared Device Metrics for \(node.num)") - } else { - print("Clear Device Metrics Log Failed") - } - } - } - Button { - exportString = telemetryToCsvFile(telemetry: deviceMetrics, metricsType: 0) - isExporting = true - } label: { - Label("save", systemImage: "square.and.arrow.down") + Button { + exportString = telemetryToCsvFile(telemetry: deviceMetrics, metricsType: 0) + isExporting = true + } label: { + Label("save", systemImage: "square.and.arrow.down") + } + .buttonStyle(.bordered) + .buttonBorderShape(.capsule) + .controlSize(.large) + .padding(.bottom) + .padding(.trailing) + } + } else { + if #available (iOS 17, *) { + ContentUnavailableView("No Device Metrics", systemImage: "slash.circle") + } else { + Text("No Device Metrics") + } } - .buttonStyle(.bordered) - .buttonBorderShape(.capsule) - .controlSize(.large) - .padding(.bottom) - .padding(.trailing) } .navigationTitle("device.metrics.log") .navigationBarTitleDisplayMode(.inline) diff --git a/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift b/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift index 370b050a..7abf4094 100644 --- a/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift +++ b/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift @@ -11,181 +11,190 @@ struct EnvironmentMetricsLog: View { @Environment(\.managedObjectContext) var context @EnvironmentObject var bleManager: BLEManager - @State private var isPresentingClearLogConfirm: Bool = false - @State var isExporting = false @State var exportString = "" - @ObservedObject var node: NodeInfoEntity var body: some View { - let oneWeekAgo = Calendar.current.date(byAdding: .day, value: -7, to: Date()) - let environmentMetrics = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 1")).reversed() as? [TelemetryEntity] ?? [] - let chartData = environmentMetrics - .filter { $0.time != nil && $0.time! >= oneWeekAgo! } - .sorted { $0.time! < $1.time! } - let locale = NSLocale.current as NSLocale - let localeUnit = locale.object(forKey: NSLocale.Key(rawValue: "kCFLocaleTemperatureUnitKey")) - let format: UnitTemperature = localeUnit as? String ?? "Celsius" == "Fahrenheit" ? .fahrenheit : .celsius VStack { - if chartData.count > 0 { - GroupBox(label: Label("\(environmentMetrics.count) Readings Total", systemImage: "chart.xyaxis.line")) { - Chart { - ForEach(chartData, id: \.time) { dataPoint in - AreaMark( - x: .value("Time", dataPoint.time!), - y: .value("Temperature", dataPoint.temperature.localeTemperature()), - stacking: .unstacked - ) - .interpolationMethod(.cardinal) - .foregroundStyle( - .linearGradient( - colors: [.blue, .yellow, .orange, .red, .red], - startPoint: .bottom, endPoint: .top - ) - .opacity(0.6) - ) - .alignsMarkStylesWithPlotArea() - .accessibilityHidden(true) - LineMark( - x: .value("Time", dataPoint.time!), - y: .value("Temperature", dataPoint.temperature.localeTemperature()) - ) - .interpolationMethod(.cardinal) - .foregroundStyle( - .linearGradient( - colors: [.blue, .yellow, .orange, .red, .red], - startPoint: .bottom, endPoint: .top - ) - ) - .lineStyle(StrokeStyle(lineWidth: 4)) - .alignsMarkStylesWithPlotArea() + if node.hasEnvironmentMetrics { + let oneWeekAgo = Calendar.current.date(byAdding: .day, value: -7, to: Date()) + let environmentMetrics = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 1")).reversed() as? [TelemetryEntity] ?? [] + let chartData = environmentMetrics + .filter { $0.time != nil && $0.time! >= oneWeekAgo! } + .sorted { $0.time! < $1.time! } + let locale = NSLocale.current as NSLocale + let localeUnit = locale.object(forKey: NSLocale.Key(rawValue: "kCFLocaleTemperatureUnitKey")) + let format: UnitTemperature = localeUnit as? String ?? "Celsius" == "Fahrenheit" ? .fahrenheit : .celsius + VStack { + if chartData.count > 0 { + GroupBox(label: Label("\(environmentMetrics.count) Readings Total", systemImage: "chart.xyaxis.line")) { + Chart { + ForEach(chartData, id: \.time) { dataPoint in + AreaMark( + x: .value("Time", dataPoint.time!), + y: .value("Temperature", dataPoint.temperature.localeTemperature()), + stacking: .unstacked + ) + .interpolationMethod(.cardinal) + .foregroundStyle( + .linearGradient( + colors: [.blue, .yellow, .orange, .red, .red], + startPoint: .bottom, endPoint: .top + ) + .opacity(0.6) + ) + .alignsMarkStylesWithPlotArea() + .accessibilityHidden(true) + LineMark( + x: .value("Time", dataPoint.time!), + y: .value("Temperature", dataPoint.temperature.localeTemperature()) + ) + .interpolationMethod(.cardinal) + .foregroundStyle( + .linearGradient( + colors: [.blue, .yellow, .orange, .red, .red], + startPoint: .bottom, endPoint: .top + ) + ) + .lineStyle(StrokeStyle(lineWidth: 4)) + .alignsMarkStylesWithPlotArea() + } + } + .chartXAxis(content: { + AxisMarks(position: .top) + }) + .chartYScale(domain: format == .celsius ? -20...55 : 0...125) + .chartForegroundStyleScale([ + "Temperature": .clear + ]) + .chartLegend(position: .automatic, alignment: .bottom) } } - .chartXAxis(content: { - AxisMarks(position: .top) - }) - .chartYScale(domain: format == .celsius ? -20...55 : 0...125) - .chartForegroundStyleScale([ - "Temperature": .clear - ]) - .chartLegend(position: .automatic, alignment: .bottom) - } - } - 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 || UIDevice.current.userInterfaceIdiom == .mac { - // Add a table for mac and ipad - Table(environmentMetrics) { - TableColumn("Temperature") { em in - Text(em.temperature.formattedTemperature()) - } - TableColumn("Humidity") { em in - Text("\(String(format: "%.2f", em.relativeHumidity))%") - } - TableColumn("Barometric Pressure") { em in - Text("\(String(format: "%.2f", em.barometricPressure)) hPa") - } - TableColumn("gas.resistance") { em in - Text("\(String(format: "%.2f", em.gasResistance)) ohms") - } - TableColumn("current") { em in - Text("\(String(format: "%.2f", em.current))") - } - TableColumn("voltage") { em in - Text("\(String(format: "%.2f", em.voltage))") - } - TableColumn("timestamp") { em in - Text(em.time?.formattedDate(format: dateFormatString) ?? "unknown.age".localized) - } - .width(min: 180) - } - } else { - ScrollView { - let columns = [ - GridItem(.flexible(minimum: 30, maximum: 50), spacing: 0.1), - GridItem(.flexible(minimum: 30, maximum: 60), spacing: 0.1), - GridItem(.flexible(minimum: 30, maximum: 60), spacing: 0.1), - GridItem(.flexible(minimum: 30, maximum: 50), spacing: 0.1), - GridItem(spacing: 0) - ] - LazyVGrid(columns: columns, alignment: .leading, spacing: 1, pinnedViews: [.sectionHeaders]) { - - GridRow { - Text("Temp") - .font(.caption) - .fontWeight(.bold) - Text("Hum") - .font(.caption) - .fontWeight(.bold) - Text("Bar") - .font(.caption) - .fontWeight(.bold) - Text("gas") - .font(.caption) - .fontWeight(.bold) - Text("timestamp") - .font(.caption) - .fontWeight(.bold) - } - ForEach(environmentMetrics, id: \.self) { em in - - GridRow { - + 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 || UIDevice.current.userInterfaceIdiom == .mac { + // Add a table for mac and ipad + Table(environmentMetrics) { + TableColumn("Temperature") { em in Text(em.temperature.formattedTemperature()) - .font(.caption) + } + TableColumn("Humidity") { em in Text("\(String(format: "%.2f", em.relativeHumidity))%") - .font(.caption) - Text("\(String(format: "%.2f", em.barometricPressure))") - .font(.caption) - Text("\(String(format: "%.2f", em.gasResistance))") - .font(.caption) + } + TableColumn("Barometric Pressure") { em in + Text("\(String(format: "%.2f", em.barometricPressure)) hPa") + } + TableColumn("gas.resistance") { em in + Text("\(String(format: "%.2f", em.gasResistance)) ohms") + } + TableColumn("current") { em in + Text("\(String(format: "%.2f", em.current))") + } + TableColumn("voltage") { em in + Text("\(String(format: "%.2f", em.voltage))") + } + TableColumn("timestamp") { em in Text(em.time?.formattedDate(format: dateFormatString) ?? "unknown.age".localized) - .font(.caption) + } + .width(min: 180) + } + } else { + ScrollView { + let columns = [ + GridItem(.flexible(minimum: 30, maximum: 50), spacing: 0.1), + GridItem(.flexible(minimum: 30, maximum: 60), spacing: 0.1), + GridItem(.flexible(minimum: 30, maximum: 60), spacing: 0.1), + GridItem(.flexible(minimum: 30, maximum: 50), spacing: 0.1), + GridItem(spacing: 0) + ] + LazyVGrid(columns: columns, alignment: .leading, spacing: 1, pinnedViews: [.sectionHeaders]) { + + GridRow { + Text("Temp") + .font(.caption) + .fontWeight(.bold) + Text("Hum") + .font(.caption) + .fontWeight(.bold) + Text("Bar") + .font(.caption) + .fontWeight(.bold) + Text("gas") + .font(.caption) + .fontWeight(.bold) + Text("timestamp") + .font(.caption) + .fontWeight(.bold) + } + ForEach(environmentMetrics, id: \.self) { em in + + GridRow { + + Text(em.temperature.formattedTemperature()) + .font(.caption) + Text("\(String(format: "%.2f", em.relativeHumidity))%") + .font(.caption) + Text("\(String(format: "%.2f", em.barometricPressure))") + .font(.caption) + Text("\(String(format: "%.2f", em.gasResistance))") + .font(.caption) + Text(em.time?.formattedDate(format: dateFormatString) ?? "unknown.age".localized) + .font(.caption) + } + } + } + .padding(.leading, 15) + .padding(.trailing, 5) + } + } + } + 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 environment metrics?", role: .destructive) { + if clearTelemetry(destNum: node.num, metricsType: 1, context: context) { + print("Clear Environment Metrics Log Failed") } } } - .padding(.leading, 15) - .padding(.trailing, 5) - } - } - } - 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 environment metrics?", role: .destructive) { - if clearTelemetry(destNum: node.num, metricsType: 1, context: context) { - print("Clear Environment Metrics Log Failed") + Button { + exportString = telemetryToCsvFile(telemetry: environmentMetrics, metricsType: 1) + isExporting = true + } label: { + Label("save", systemImage: "square.and.arrow.down") } + .buttonStyle(.bordered) + .buttonBorderShape(.capsule) + .controlSize(.large) + .padding(.bottom) + .padding(.trailing) + } + + } else { + if #available (iOS 17, *) { + ContentUnavailableView("No Environment Metrics", systemImage: "slash.circle") + } else { + Text("No Environment Metrics") } } - Button { - exportString = telemetryToCsvFile(telemetry: environmentMetrics, metricsType: 1) - isExporting = true - } label: { - Label("save", systemImage: "square.and.arrow.down") - } - .buttonStyle(.bordered) - .buttonBorderShape(.capsule) - .controlSize(.large) - .padding(.bottom) - .padding(.trailing) } + .navigationTitle("Environment Metrics Log") .navigationBarTitleDisplayMode(.inline) .navigationBarItems(trailing: diff --git a/Meshtastic/Views/Nodes/Helpers/NodeMapSwiftUI.swift b/Meshtastic/Views/Nodes/Helpers/NodeMapSwiftUI.swift index d7dbb3b0..298bed7a 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeMapSwiftUI.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeMapSwiftUI.swift @@ -126,7 +126,7 @@ struct NodeMapSwiftUI: View { } } .mapScope(mapScope) - .mapStyle(.hybrid(elevation: .realistic)) + .mapStyle(.hybrid(elevation: .realistic, pointsOfInterest: .all, showsTraffic: true)) .mapControls { MapScaleView(scope: mapScope) .mapControlVisibility(.visible) diff --git a/Meshtastic/Views/Settings/ShareChannels.swift b/Meshtastic/Views/Settings/ShareChannels.swift index 0f90bea2..ebc6e98e 100644 --- a/Meshtastic/Views/Settings/ShareChannels.swift +++ b/Meshtastic/Views/Settings/ShareChannels.swift @@ -207,10 +207,10 @@ struct ShareChannels: View { .resizable() .scaledToFit() .frame( - minWidth: smallest * 0.95, - maxWidth: smallest * 0.95, - minHeight: smallest * 0.95, - maxHeight: smallest * 0.95, + minWidth: smallest * (UIDevice.current.userInterfaceIdiom == .phone ? 0.9 : 0.6), + maxWidth: smallest * (UIDevice.current.userInterfaceIdiom == .phone ? 0.9 : 0.6), + minHeight: smallest * (UIDevice.current.userInterfaceIdiom == .phone ? 0.9 : 0.6), + maxHeight: smallest * (UIDevice.current.userInterfaceIdiom == .phone ? 0.9 : 0.6), alignment: .top ) }