mirror of
https://github.com/meshtastic/Meshtastic-Apple.git
synced 2026-04-20 22:13:56 +00:00
Tips and empty content views
This commit is contained in:
parent
76d5da1d21
commit
fc0e152455
9 changed files with 378 additions and 324 deletions
|
|
@ -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 */,
|
||||
|
|
|
|||
|
|
@ -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? {
|
||||
|
|
|
|||
29
Meshtastic/Tips/MessagesTips.swift
Normal file
29
Meshtastic/Tips/MessagesTips.swift
Normal 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")
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue