Tips and empty content views

This commit is contained in:
Garth Vander Houwen 2023-09-15 18:40:52 -07:00
parent 76d5da1d21
commit fc0e152455
9 changed files with 378 additions and 324 deletions

View file

@ -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 = "<group>"; };
DDB8F4132A9EE5F000230ECE /* ChannelList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelList.swift; sourceTree = "<group>"; };
DDBA45EC299ED78100DEEDDC /* MeshtasticDataModelV8.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV8.xcdatamodel; sourceTree = "<group>"; };
DDC1B8192AB5377B00C71E39 /* MessagesTips.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessagesTips.swift; sourceTree = "<group>"; };
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 = "<group>"; };
DDC2E15B26CE248F0042C5E4 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = ../Assets.xcassets; sourceTree = "<group>"; };
@ -577,6 +579,7 @@
children = (
DD77093A2AA1ABB8007A8BF0 /* BluetoothTips.swift */,
DD77093C2AA1AFA3007A8BF0 /* ChannelTips.swift */,
DDC1B8192AB5377B00C71E39 /* MessagesTips.swift */,
);
path = Tips;
sourceTree = "<group>";
@ -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 */,

View file

@ -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? {

View file

@ -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")
}
}

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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:

View file

@ -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)

View file

@ -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
)
}