Newer Older
267 lines | 9.895kb
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
            }
33

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

            
47
                liveMetricCard(
48
                    title: "Current",
49
                    symbol: "waveform.path.ecg",
50
                    color: .blue,
51
                    value: "\(meter.current.format(decimalDigits: 3)) A",
Bogdan Timofte authored 2 weeks ago
52
                    range: metricRange(
Bogdan Timofte authored 2 weeks ago
53
                        min: meter.measurements.current.context.minValue,
54
                        max: meter.measurements.current.context.maxValue,
55
                        unit: "A"
56
                    )
57
                )
58

            
59
                liveMetricCard(
60
                    title: "Power",
61
                    symbol: "flame.fill",
62
                    color: .pink,
63
                    value: "\(meter.power.format(decimalDigits: 3)) W",
Bogdan Timofte authored 2 weeks ago
64
                    range: metricRange(
Bogdan Timofte authored 2 weeks ago
65
                        min: meter.measurements.power.context.minValue,
66
                        max: meter.measurements.power.context.maxValue,
67
                        unit: "W"
68
                    )
69
                )
70

            
71
                liveMetricCard(
72
                    title: "Temperature",
73
                    symbol: "thermometer.medium",
74
                    color: .orange,
75
                    value: meter.primaryTemperatureDescription,
Bogdan Timofte authored 2 weeks ago
76
                    range: temperatureRange()
Bogdan Timofte authored 2 weeks ago
77
                )
78

            
Bogdan Timofte authored 2 weeks ago
79
                liveMetricCard(
80
                    title: "Load",
Bogdan Timofte authored a week ago
81
                    customSymbol: AnyView(LoadResistanceIconView(color: .yellow)),
Bogdan Timofte authored 2 weeks ago
82
                    color: .yellow,
83
                    value: "\(meter.loadResistance.format(decimalDigits: 1)) \u{2126}",
84
                    detailText: "Measured resistance"
85
                )
Bogdan Timofte authored 2 weeks ago
86

            
Bogdan Timofte authored 2 weeks ago
87
                liveMetricCard(
88
                    title: "RSSI",
89
                    symbol: "dot.radiowaves.left.and.right",
90
                    color: .mint,
Bogdan Timofte authored 2 weeks ago
91
                    value: "\(meter.btSerial.averageRSSI) dBm",
Bogdan Timofte authored a week ago
92
                    range: MeterLiveMetricRange(
Bogdan Timofte authored 2 weeks ago
93
                        minLabel: "Min",
94
                        maxLabel: "Max",
95
                        minValue: "\(meter.btSerial.minRSSI) dBm",
96
                        maxValue: "\(meter.btSerial.maxRSSI) dBm"
97
                    ),
98
                    valueFont: .system(compactLayout ? .subheadline : .headline, design: .rounded).weight(.bold)
99
                )
Bogdan Timofte authored a week ago
100

            
101
                if meter.supportsChargerDetection {
102
                    liveMetricCard(
103
                        title: "Detected Charger",
104
                        symbol: "powerplug.fill",
105
                        color: .indigo,
106
                        value: meter.chargerTypeDescription,
107
                        detailText: "Source handshake",
108
                        valueFont: .system(compactLayout ? .subheadline : .headline, design: .rounded).weight(.bold),
109
                        valueLineLimit: 2,
110
                        valueMonospacedDigits: false,
111
                        valueMinimumScaleFactor: 0.72
112
                    )
113
                }
Bogdan Timofte authored 2 weeks ago
114
            }
115
        }
Bogdan Timofte authored 2 weeks ago
116
        .frame(maxWidth: .infinity, alignment: .topLeading)
Bogdan Timofte authored 2 weeks ago
117
    }
118

            
119
    private var liveMetricColumns: [GridItem] {
120
        if compactLayout {
Bogdan Timofte authored 2 weeks ago
121
            return Array(repeating: GridItem(.flexible(), spacing: 10), count: 3)
Bogdan Timofte authored 2 weeks ago
122
        }
123

            
124
        return [GridItem(.flexible()), GridItem(.flexible())]
Bogdan Timofte authored 2 weeks ago
125
    }
Bogdan Timofte authored 2 weeks ago
126

            
127
    private var statusBadge: some View {
128
        Text(meter.operationalState == .dataIsAvailable ? "Live" : "Waiting")
129
            .font(.caption.weight(.semibold))
130
            .padding(.horizontal, 10)
131
            .padding(.vertical, 6)
132
            .foregroundColor(meter.operationalState == .dataIsAvailable ? .green : .secondary)
133
            .meterCard(
134
                tint: meter.operationalState == .dataIsAvailable ? .green : .secondary,
135
                fillOpacity: 0.12,
136
                strokeOpacity: 0.16,
137
                cornerRadius: 999
138
            )
139
    }
140

            
Bogdan Timofte authored 2 weeks ago
141
    private var showsCompactMetricRange: Bool {
142
        compactLayout && (availableSize?.height ?? 0) >= 380
143
    }
144

            
145
    private var shouldShowMetricRange: Bool {
146
        !compactLayout || showsCompactMetricRange
147
    }
148

            
Bogdan Timofte authored 2 weeks ago
149
    private func liveMetricCard(
150
        title: String,
Bogdan Timofte authored 2 weeks ago
151
        symbol: String? = nil,
152
        customSymbol: AnyView? = nil,
Bogdan Timofte authored 2 weeks ago
153
        color: Color,
154
        value: String,
Bogdan Timofte authored a week ago
155
        range: MeterLiveMetricRange? = nil,
Bogdan Timofte authored 2 weeks ago
156
        detailText: String? = nil,
157
        valueFont: Font? = nil,
158
        valueLineLimit: Int = 1,
159
        valueMonospacedDigits: Bool = true,
160
        valueMinimumScaleFactor: CGFloat = 0.85
Bogdan Timofte authored 2 weeks ago
161
    ) -> some View {
162
        VStack(alignment: .leading, spacing: 10) {
Bogdan Timofte authored 2 weeks ago
163
            HStack(spacing: compactLayout ? 8 : 10) {
Bogdan Timofte authored 2 weeks ago
164
                Group {
165
                    if let customSymbol {
166
                        customSymbol
167
                    } else if let symbol {
168
                        Image(systemName: symbol)
169
                            .font(.system(size: compactLayout ? 14 : 15, weight: .semibold))
170
                            .foregroundColor(color)
171
                    }
172
                }
Bogdan Timofte authored 2 weeks ago
173
                    .frame(width: compactLayout ? 30 : 34, height: compactLayout ? 30 : 34)
Bogdan Timofte authored 2 weeks ago
174
                .background(Circle().fill(color.opacity(0.12)))
Bogdan Timofte authored 2 weeks ago
175

            
Bogdan Timofte authored 2 weeks ago
176
                Text(title)
177
                    .font((compactLayout ? Font.caption : .subheadline).weight(.semibold))
178
                    .foregroundColor(.secondary)
179
                    .lineLimit(1)
180

            
181
                Spacer(minLength: 0)
182
            }
Bogdan Timofte authored 2 weeks ago
183

            
Bogdan Timofte authored 2 weeks ago
184
            Group {
185
                if valueMonospacedDigits {
186
                    Text(value)
187
                        .monospacedDigit()
188
                } else {
189
                    Text(value)
190
                }
191
            }
192
            .font(valueFont ?? .system(compactLayout ? .headline : .title3, design: .rounded).weight(.bold))
193
            .lineLimit(valueLineLimit)
194
            .minimumScaleFactor(valueMinimumScaleFactor)
Bogdan Timofte authored 2 weeks ago
195

            
Bogdan Timofte authored 2 weeks ago
196
            if shouldShowMetricRange {
197
                if let range {
198
                    metricRangeTable(range)
199
                } else if let detailText, !detailText.isEmpty {
200
                    Text(detailText)
201
                        .font(.caption)
202
                        .foregroundColor(.secondary)
203
                        .lineLimit(2)
204
                }
Bogdan Timofte authored 2 weeks ago
205
            }
206
        }
Bogdan Timofte authored 2 weeks ago
207
        .frame(
208
            maxWidth: .infinity,
Bogdan Timofte authored 2 weeks ago
209
            minHeight: compactLayout ? (shouldShowMetricRange ? 132 : 96) : 128,
Bogdan Timofte authored 2 weeks ago
210
            alignment: .leading
211
        )
212
        .padding(compactLayout ? 12 : 16)
Bogdan Timofte authored 2 weeks ago
213
        .meterCard(tint: color, fillOpacity: 0.10, strokeOpacity: 0.12)
214
    }
215

            
Bogdan Timofte authored a week ago
216
    private func metricRangeTable(_ range: MeterLiveMetricRange) -> some View {
Bogdan Timofte authored 2 weeks ago
217
        VStack(alignment: .leading, spacing: 4) {
218
            HStack(spacing: 12) {
219
                Text(range.minLabel)
220
                Spacer(minLength: 0)
221
                Text(range.maxLabel)
222
            }
223
            .font(.caption2.weight(.semibold))
224
            .foregroundColor(.secondary)
225

            
226
            HStack(spacing: 12) {
227
                Text(range.minValue)
228
                    .monospacedDigit()
229
                Spacer(minLength: 0)
230
                Text(range.maxValue)
231
                    .monospacedDigit()
232
            }
233
            .font(.caption.weight(.medium))
234
            .foregroundColor(.primary)
235
        }
236
    }
237

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

            
Bogdan Timofte authored a week ago
241
        return MeterLiveMetricRange(
Bogdan Timofte authored 2 weeks ago
242
            minLabel: "Min",
243
            maxLabel: "Max",
244
            minValue: "\(min.format(decimalDigits: 3)) \(unit)",
245
            maxValue: "\(max.format(decimalDigits: 3)) \(unit)"
246
        )
247
    }
248

            
Bogdan Timofte authored a week ago
249
    private func temperatureRange() -> MeterLiveMetricRange? {
Bogdan Timofte authored 2 weeks ago
250
        let value = meter.primaryTemperatureDescription
251
        guard !value.isEmpty else { return nil }
252

            
Bogdan Timofte authored a week ago
253
        return MeterLiveMetricRange(
Bogdan Timofte authored 2 weeks ago
254
            minLabel: "Min",
255
            maxLabel: "Max",
256
            minValue: value,
257
            maxValue: value
258
        )
Bogdan Timofte authored 2 weeks ago
259
    }
Bogdan Timofte authored a week ago
260

            
261
    private func meterHistoryText(for date: Date?) -> String {
262
        guard let date else {
263
            return "Never"
264
        }
265
        return date.format(as: "yyyy-MM-dd HH:mm")
266
    }
Bogdan Timofte authored 2 weeks ago
267
}