Newer Older
302 lines | 11.259kb
Bogdan Timofte authored 2 weeks ago
1
//
Bogdan Timofte authored a week ago
2
//  MeterLiveContentView.swift
Bogdan Timofte authored 2 weeks ago
3
//  USB Meter
4
//
5
//  Created by Bogdan Timofte on 09/03/2020.
6
//  Copyright © 2020 Bogdan Timofte. All rights reserved.
7
//
8

            
9
import SwiftUI
10

            
Bogdan Timofte authored a week ago
11
struct MeterLiveContentView: View {
Bogdan Timofte authored 2 weeks ago
12
    @EnvironmentObject private var meter: Meter
Bogdan Timofte authored 2 weeks ago
13
    var compactLayout: Bool = false
14
    var availableSize: CGSize? = nil
Bogdan Timofte authored 2 weeks ago
15

            
16
    var body: some View {
Bogdan Timofte authored 2 weeks ago
17
        VStack(alignment: .leading, spacing: 16) {
Bogdan Timofte authored 2 weeks ago
18
            HStack {
Bogdan Timofte authored 2 weeks ago
19
                Text("Live Data")
20
                    .font(.headline)
21
                Spacer()
22
                statusBadge
23
            }
24

            
Bogdan Timofte authored a week ago
25
            MeterInfoCardView(title: "Detected Meter", tint: .indigo) {
26
                MeterInfoRowView(label: "Name", value: meter.name.isEmpty ? "Meter" : meter.name)
27
                MeterInfoRowView(label: "Model", value: meter.deviceModelSummary)
28
                MeterInfoRowView(label: "Advertised Model", value: meter.modelString)
29
                MeterInfoRowView(label: "MAC", value: meter.btSerial.macAddress.description)
30
                MeterInfoRowView(label: "Last Seen", value: meterHistoryText(for: meter.lastSeen))
31
                MeterInfoRowView(label: "Last Connected", value: meterHistoryText(for: meter.lastConnectedAt))
32
            }
Bogdan Timofte authored 6 days ago
33
            .frame(maxWidth: .infinity, alignment: .leading)
Bogdan Timofte authored a week ago
34

            
Bogdan Timofte authored 2 weeks ago
35
            LazyVGrid(columns: liveMetricColumns, spacing: compactLayout ? 10 : 12) {
Bogdan Timofte authored 6 days ago
36
                if shouldShowVoltageCard {
37
                    liveMetricCard(
38
                        title: "Voltage",
39
                        symbol: "bolt.fill",
40
                        color: .green,
41
                        value: "\(meter.voltage.format(decimalDigits: 3)) V",
42
                        range: metricRange(
43
                            min: meter.measurements.voltage.context.minValue,
44
                            max: meter.measurements.voltage.context.maxValue,
45
                            unit: "V"
46
                        )
Bogdan Timofte authored 2 weeks ago
47
                    )
Bogdan Timofte authored 6 days ago
48
                }
Bogdan Timofte authored 2 weeks ago
49

            
Bogdan Timofte authored 6 days ago
50
                if shouldShowCurrentCard {
51
                    liveMetricCard(
52
                        title: "Current",
53
                        symbol: "waveform.path.ecg",
54
                        color: .blue,
55
                        value: "\(meter.current.format(decimalDigits: 3)) A",
56
                        range: metricRange(
57
                            min: meter.measurements.current.context.minValue,
58
                            max: meter.measurements.current.context.maxValue,
59
                            unit: "A"
60
                        )
Bogdan Timofte authored 2 weeks ago
61
                    )
Bogdan Timofte authored 6 days ago
62
                }
Bogdan Timofte authored 2 weeks ago
63

            
Bogdan Timofte authored 6 days ago
64
                if shouldShowPowerCard {
65
                    liveMetricCard(
66
                        title: "Power",
67
                        symbol: "flame.fill",
68
                        color: .pink,
69
                        value: "\(meter.power.format(decimalDigits: 3)) W",
70
                        range: metricRange(
71
                            min: meter.measurements.power.context.minValue,
72
                            max: meter.measurements.power.context.maxValue,
73
                            unit: "W"
74
                        )
Bogdan Timofte authored 2 weeks ago
75
                    )
Bogdan Timofte authored 6 days ago
76
                }
Bogdan Timofte authored 2 weeks ago
77

            
Bogdan Timofte authored 6 days ago
78
                if shouldShowTemperatureCard {
79
                    liveMetricCard(
80
                        title: "Temperature",
81
                        symbol: "thermometer.medium",
82
                        color: .orange,
83
                        value: meter.primaryTemperatureDescription,
84
                        range: temperatureRange()
85
                    )
86
                }
Bogdan Timofte authored 2 weeks ago
87

            
Bogdan Timofte authored 6 days ago
88
                if shouldShowLoadCard {
89
                    liveMetricCard(
90
                        title: "Load",
91
                        customSymbol: AnyView(LoadResistanceIconView(color: .yellow)),
92
                        color: .yellow,
93
                        value: "\(meter.loadResistance.format(decimalDigits: 1)) \u{2126}",
94
                        detailText: "Measured resistance"
95
                    )
96
                }
Bogdan Timofte authored 2 weeks ago
97

            
Bogdan Timofte authored 2 weeks ago
98
                liveMetricCard(
99
                    title: "RSSI",
100
                    symbol: "dot.radiowaves.left.and.right",
101
                    color: .mint,
Bogdan Timofte authored 2 weeks ago
102
                    value: "\(meter.btSerial.averageRSSI) dBm",
Bogdan Timofte authored a week ago
103
                    range: MeterLiveMetricRange(
Bogdan Timofte authored 2 weeks ago
104
                        minLabel: "Min",
105
                        maxLabel: "Max",
106
                        minValue: "\(meter.btSerial.minRSSI) dBm",
107
                        maxValue: "\(meter.btSerial.maxRSSI) dBm"
108
                    ),
109
                    valueFont: .system(compactLayout ? .subheadline : .headline, design: .rounded).weight(.bold)
110
                )
Bogdan Timofte authored a week ago
111

            
Bogdan Timofte authored 6 days ago
112
                if meter.supportsChargerDetection && hasLiveMetrics {
Bogdan Timofte authored a week ago
113
                    liveMetricCard(
114
                        title: "Detected Charger",
115
                        symbol: "powerplug.fill",
116
                        color: .indigo,
117
                        value: meter.chargerTypeDescription,
118
                        detailText: "Source handshake",
119
                        valueFont: .system(compactLayout ? .subheadline : .headline, design: .rounded).weight(.bold),
120
                        valueLineLimit: 2,
121
                        valueMonospacedDigits: false,
122
                        valueMinimumScaleFactor: 0.72
123
                    )
124
                }
Bogdan Timofte authored 2 weeks ago
125
            }
126
        }
Bogdan Timofte authored 2 weeks ago
127
        .frame(maxWidth: .infinity, alignment: .topLeading)
Bogdan Timofte authored 2 weeks ago
128
    }
129

            
Bogdan Timofte authored 6 days ago
130
    private var hasLiveMetrics: Bool {
131
        meter.operationalState == .dataIsAvailable
132
    }
133

            
134
    private var shouldShowVoltageCard: Bool {
135
        hasLiveMetrics && meter.measurements.voltage.context.isValid && meter.voltage.isFinite
136
    }
137

            
138
    private var shouldShowCurrentCard: Bool {
139
        hasLiveMetrics && meter.measurements.current.context.isValid && meter.current.isFinite
140
    }
141

            
142
    private var shouldShowPowerCard: Bool {
143
        hasLiveMetrics && meter.measurements.power.context.isValid && meter.power.isFinite
144
    }
145

            
146
    private var shouldShowTemperatureCard: Bool {
147
        hasLiveMetrics && meter.displayedTemperatureValue.isFinite
148
    }
149

            
150
    private var shouldShowLoadCard: Bool {
151
        hasLiveMetrics && meter.loadResistance.isFinite && meter.loadResistance > 0
152
    }
153

            
Bogdan Timofte authored 2 weeks ago
154
    private var liveMetricColumns: [GridItem] {
155
        if compactLayout {
Bogdan Timofte authored 2 weeks ago
156
            return Array(repeating: GridItem(.flexible(), spacing: 10), count: 3)
Bogdan Timofte authored 2 weeks ago
157
        }
158

            
159
        return [GridItem(.flexible()), GridItem(.flexible())]
Bogdan Timofte authored 2 weeks ago
160
    }
Bogdan Timofte authored 2 weeks ago
161

            
162
    private var statusBadge: some View {
163
        Text(meter.operationalState == .dataIsAvailable ? "Live" : "Waiting")
164
            .font(.caption.weight(.semibold))
165
            .padding(.horizontal, 10)
166
            .padding(.vertical, 6)
167
            .foregroundColor(meter.operationalState == .dataIsAvailable ? .green : .secondary)
168
            .meterCard(
169
                tint: meter.operationalState == .dataIsAvailable ? .green : .secondary,
170
                fillOpacity: 0.12,
171
                strokeOpacity: 0.16,
172
                cornerRadius: 999
173
            )
174
    }
175

            
Bogdan Timofte authored 2 weeks ago
176
    private var showsCompactMetricRange: Bool {
177
        compactLayout && (availableSize?.height ?? 0) >= 380
178
    }
179

            
180
    private var shouldShowMetricRange: Bool {
181
        !compactLayout || showsCompactMetricRange
182
    }
183

            
Bogdan Timofte authored 2 weeks ago
184
    private func liveMetricCard(
185
        title: String,
Bogdan Timofte authored 2 weeks ago
186
        symbol: String? = nil,
187
        customSymbol: AnyView? = nil,
Bogdan Timofte authored 2 weeks ago
188
        color: Color,
189
        value: String,
Bogdan Timofte authored a week ago
190
        range: MeterLiveMetricRange? = nil,
Bogdan Timofte authored 2 weeks ago
191
        detailText: String? = nil,
192
        valueFont: Font? = nil,
193
        valueLineLimit: Int = 1,
194
        valueMonospacedDigits: Bool = true,
195
        valueMinimumScaleFactor: CGFloat = 0.85
Bogdan Timofte authored 2 weeks ago
196
    ) -> some View {
197
        VStack(alignment: .leading, spacing: 10) {
Bogdan Timofte authored 2 weeks ago
198
            HStack(spacing: compactLayout ? 8 : 10) {
Bogdan Timofte authored 2 weeks ago
199
                Group {
200
                    if let customSymbol {
201
                        customSymbol
202
                    } else if let symbol {
203
                        Image(systemName: symbol)
204
                            .font(.system(size: compactLayout ? 14 : 15, weight: .semibold))
205
                            .foregroundColor(color)
206
                    }
207
                }
Bogdan Timofte authored 2 weeks ago
208
                    .frame(width: compactLayout ? 30 : 34, height: compactLayout ? 30 : 34)
Bogdan Timofte authored 2 weeks ago
209
                .background(Circle().fill(color.opacity(0.12)))
Bogdan Timofte authored 2 weeks ago
210

            
Bogdan Timofte authored 2 weeks ago
211
                Text(title)
212
                    .font((compactLayout ? Font.caption : .subheadline).weight(.semibold))
213
                    .foregroundColor(.secondary)
214
                    .lineLimit(1)
215

            
216
                Spacer(minLength: 0)
217
            }
Bogdan Timofte authored 2 weeks ago
218

            
Bogdan Timofte authored 2 weeks ago
219
            Group {
220
                if valueMonospacedDigits {
221
                    Text(value)
222
                        .monospacedDigit()
223
                } else {
224
                    Text(value)
225
                }
226
            }
227
            .font(valueFont ?? .system(compactLayout ? .headline : .title3, design: .rounded).weight(.bold))
228
            .lineLimit(valueLineLimit)
229
            .minimumScaleFactor(valueMinimumScaleFactor)
Bogdan Timofte authored 2 weeks ago
230

            
Bogdan Timofte authored 2 weeks ago
231
            if shouldShowMetricRange {
232
                if let range {
233
                    metricRangeTable(range)
234
                } else if let detailText, !detailText.isEmpty {
235
                    Text(detailText)
236
                        .font(.caption)
237
                        .foregroundColor(.secondary)
238
                        .lineLimit(2)
239
                }
Bogdan Timofte authored 2 weeks ago
240
            }
241
        }
Bogdan Timofte authored 2 weeks ago
242
        .frame(
243
            maxWidth: .infinity,
Bogdan Timofte authored 2 weeks ago
244
            minHeight: compactLayout ? (shouldShowMetricRange ? 132 : 96) : 128,
Bogdan Timofte authored 2 weeks ago
245
            alignment: .leading
246
        )
247
        .padding(compactLayout ? 12 : 16)
Bogdan Timofte authored 2 weeks ago
248
        .meterCard(tint: color, fillOpacity: 0.10, strokeOpacity: 0.12)
249
    }
250

            
Bogdan Timofte authored a week ago
251
    private func metricRangeTable(_ range: MeterLiveMetricRange) -> some View {
Bogdan Timofte authored 2 weeks ago
252
        VStack(alignment: .leading, spacing: 4) {
253
            HStack(spacing: 12) {
254
                Text(range.minLabel)
255
                Spacer(minLength: 0)
256
                Text(range.maxLabel)
257
            }
258
            .font(.caption2.weight(.semibold))
259
            .foregroundColor(.secondary)
260

            
261
            HStack(spacing: 12) {
262
                Text(range.minValue)
263
                    .monospacedDigit()
264
                Spacer(minLength: 0)
265
                Text(range.maxValue)
266
                    .monospacedDigit()
267
            }
268
            .font(.caption.weight(.medium))
269
            .foregroundColor(.primary)
270
        }
271
    }
272

            
Bogdan Timofte authored a week ago
273
    private func metricRange(min: Double, max: Double, unit: String) -> MeterLiveMetricRange? {
Bogdan Timofte authored 2 weeks ago
274
        guard min.isFinite, max.isFinite else { return nil }
Bogdan Timofte authored 2 weeks ago
275

            
Bogdan Timofte authored a week ago
276
        return MeterLiveMetricRange(
Bogdan Timofte authored 2 weeks ago
277
            minLabel: "Min",
278
            maxLabel: "Max",
279
            minValue: "\(min.format(decimalDigits: 3)) \(unit)",
280
            maxValue: "\(max.format(decimalDigits: 3)) \(unit)"
281
        )
282
    }
283

            
Bogdan Timofte authored a week ago
284
    private func temperatureRange() -> MeterLiveMetricRange? {
Bogdan Timofte authored 2 weeks ago
285
        let value = meter.primaryTemperatureDescription
286
        guard !value.isEmpty else { return nil }
287

            
Bogdan Timofte authored a week ago
288
        return MeterLiveMetricRange(
Bogdan Timofte authored 2 weeks ago
289
            minLabel: "Min",
290
            maxLabel: "Max",
291
            minValue: value,
292
            maxValue: value
293
        )
Bogdan Timofte authored 2 weeks ago
294
    }
Bogdan Timofte authored a week ago
295

            
296
    private func meterHistoryText(for date: Date?) -> String {
297
        guard let date else {
298
            return "Never"
299
        }
300
        return date.format(as: "yyyy-MM-dd HH:mm")
301
    }
Bogdan Timofte authored 2 weeks ago
302
}