Newer Older
257 lines | 9.266kb
Bogdan Timofte authored 2 weeks ago
1
//
2
//  LiveView.swift
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

            
11
struct LiveView: View {
12

            
13
    @EnvironmentObject private var meter: Meter
Bogdan Timofte authored 2 weeks ago
14
    var compactLayout: Bool = false
15
    var availableSize: CGSize? = nil
Bogdan Timofte authored 2 weeks ago
16

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

            
Bogdan Timofte authored 2 weeks ago
26
            LazyVGrid(columns: liveMetricColumns, spacing: compactLayout ? 10 : 12) {
Bogdan Timofte authored 2 weeks ago
27
                liveMetricCard(
28
                    title: "Voltage",
29
                    symbol: "bolt.fill",
30
                    color: .green,
31
                    value: "\(meter.voltage.format(decimalDigits: 3)) V",
32
                    range: rangeText(
33
                        min: meter.measurements.voltage.context.minValue,
34
                        max: meter.measurements.voltage.context.maxValue,
35
                        unit: "V"
36
                    )
37
                )
38

            
39
                liveMetricCard(
40
                    title: "Current",
41
                    symbol: "waveform.path.ecg",
42
                    color: .blue,
43
                    value: "\(meter.current.format(decimalDigits: 3)) A",
44
                    range: rangeText(
45
                        min: meter.measurements.current.context.minValue,
46
                        max: meter.measurements.current.context.maxValue,
47
                        unit: "A"
48
                    )
49
                )
50

            
51
                liveMetricCard(
52
                    title: "Power",
53
                    symbol: "flame.fill",
54
                    color: .pink,
55
                    value: "\(meter.power.format(decimalDigits: 3)) W",
56
                    range: rangeText(
57
                        min: meter.measurements.power.context.minValue,
58
                        max: meter.measurements.power.context.maxValue,
59
                        unit: "W"
60
                    )
61
                )
62

            
63
                liveMetricCard(
64
                    title: "Temperature",
65
                    symbol: "thermometer.medium",
66
                    color: .orange,
67
                    value: meter.primaryTemperatureDescription,
68
                    range: meter.secondaryTemperatureDescription
69
                )
70
            }
71

            
Bogdan Timofte authored 2 weeks ago
72
            if compactLayout && usesExpandedCompactLayout {
73
                Spacer(minLength: 0)
74
            }
75

            
Bogdan Timofte authored 2 weeks ago
76
            if shouldShowSecondaryDetails {
Bogdan Timofte authored 2 weeks ago
77
                Group {
78
                    if compactLayout {
79
                        HStack(spacing: 12) {
80
                            if meter.loadResistance > 0 {
81
                                secondaryDetailChip(
82
                                    title: "Load",
83
                                    value: "\(meter.loadResistance.format(decimalDigits: 1)) \u{2126}",
84
                                    symbol: "cable.connector",
85
                                    color: .yellow
86
                                )
87
                            }
88

            
89
                            if shouldShowChargerType {
90
                                secondaryDetailChip(
91
                                    title: "Charger",
92
                                    value: meter.chargerTypeDescription,
93
                                    symbol: "bolt.badge.checkmark",
94
                                    color: .purple
95
                                )
96
                            }
97
                        }
98
                    } else {
99
                        VStack(alignment: .leading, spacing: 12) {
100
                            Text("Details")
101
                                .font(.subheadline.weight(.semibold))
102
                                .foregroundColor(.secondary)
Bogdan Timofte authored 2 weeks ago
103

            
Bogdan Timofte authored 2 weeks ago
104
                            if meter.loadResistance > 0 {
105
                                secondaryDetailRow(
106
                                    title: "Load",
107
                                    value: "\(meter.loadResistance.format(decimalDigits: 1)) \u{2126}",
108
                                    symbol: "cable.connector",
109
                                    color: .yellow
110
                                )
111
                            }
112

            
113
                            if shouldShowChargerType {
114
                                secondaryDetailRow(
115
                                    title: "Charger",
116
                                    value: meter.chargerTypeDescription,
117
                                    symbol: "bolt.badge.checkmark",
118
                                    color: .purple
119
                                )
120
                            }
121
                        }
Bogdan Timofte authored 2 weeks ago
122
                    }
Bogdan Timofte authored 2 weeks ago
123
                }
Bogdan Timofte authored 2 weeks ago
124
                .padding(compactLayout ? 14 : 18)
Bogdan Timofte authored 2 weeks ago
125
                .meterCard(tint: meter.color, fillOpacity: 0.06, strokeOpacity: 0.10)
Bogdan Timofte authored 2 weeks ago
126
            }
127
        }
Bogdan Timofte authored 2 weeks ago
128
        .frame(maxWidth: .infinity, maxHeight: compactLayout ? .infinity : nil, alignment: .topLeading)
129
    }
130

            
131
    private var liveMetricColumns: [GridItem] {
132
        if compactLayout {
133
            let count = usesExpandedCompactLayout ? 2 : 4
134
            return Array(repeating: GridItem(.flexible(), spacing: 10), count: count)
135
        }
136

            
137
        return [GridItem(.flexible()), GridItem(.flexible())]
Bogdan Timofte authored 2 weeks ago
138
    }
Bogdan Timofte authored 2 weeks ago
139

            
140
    private var statusBadge: some View {
141
        Text(meter.operationalState == .dataIsAvailable ? "Live" : "Waiting")
142
            .font(.caption.weight(.semibold))
143
            .padding(.horizontal, 10)
144
            .padding(.vertical, 6)
145
            .foregroundColor(meter.operationalState == .dataIsAvailable ? .green : .secondary)
146
            .meterCard(
147
                tint: meter.operationalState == .dataIsAvailable ? .green : .secondary,
148
                fillOpacity: 0.12,
149
                strokeOpacity: 0.16,
150
                cornerRadius: 999
151
            )
152
    }
153

            
154
    private var shouldShowSecondaryDetails: Bool {
155
        meter.loadResistance > 0 || shouldShowChargerType
156
    }
157

            
158
    private var shouldShowChargerType: Bool {
159
        meter.supportsChargerDetection && meter.chargerTypeDescription != "Unknown"
160
    }
161

            
Bogdan Timofte authored 2 weeks ago
162
    private var usesExpandedCompactLayout: Bool {
163
        compactLayout && (availableSize?.height ?? 0) >= 520
164
    }
165

            
Bogdan Timofte authored 2 weeks ago
166
    private func liveMetricCard(
167
        title: String,
168
        symbol: String,
169
        color: Color,
170
        value: String,
171
        range: String?
172
    ) -> some View {
173
        VStack(alignment: .leading, spacing: 10) {
174
            HStack {
175
                Image(systemName: symbol)
Bogdan Timofte authored 2 weeks ago
176
                    .font(.system(size: compactLayout ? 14 : 15, weight: .semibold))
Bogdan Timofte authored 2 weeks ago
177
                    .foregroundColor(color)
Bogdan Timofte authored 2 weeks ago
178
                    .frame(width: compactLayout ? 30 : 34, height: compactLayout ? 30 : 34)
Bogdan Timofte authored 2 weeks ago
179
                    .background(Circle().fill(color.opacity(0.12)))
180
                Spacer()
181
            }
182

            
183
            Text(title)
Bogdan Timofte authored 2 weeks ago
184
                .font((compactLayout ? Font.caption : .subheadline).weight(.semibold))
Bogdan Timofte authored 2 weeks ago
185
                .foregroundColor(.secondary)
186

            
187
            Text(value)
Bogdan Timofte authored 2 weeks ago
188
                .font(.system(compactLayout ? .headline : .title3, design: .rounded).weight(.bold))
Bogdan Timofte authored 2 weeks ago
189
                .monospacedDigit()
190

            
Bogdan Timofte authored 2 weeks ago
191
            if !compactLayout, let range, !range.isEmpty {
Bogdan Timofte authored 2 weeks ago
192
                Text(range)
193
                    .font(.caption)
194
                    .foregroundColor(.secondary)
195
                    .lineLimit(2)
196
            }
197
        }
Bogdan Timofte authored 2 weeks ago
198
        .frame(
199
            maxWidth: .infinity,
200
            minHeight: compactLayout ? (usesExpandedCompactLayout ? 128 : 96) : 128,
201
            alignment: .leading
202
        )
203
        .padding(compactLayout ? 12 : 16)
Bogdan Timofte authored 2 weeks ago
204
        .meterCard(tint: color, fillOpacity: 0.10, strokeOpacity: 0.12)
205
    }
206

            
207
    private func secondaryDetailRow(
208
        title: String,
209
        value: String,
210
        symbol: String,
211
        color: Color
212
    ) -> some View {
213
        HStack(spacing: 12) {
214
            Image(systemName: symbol)
215
                .foregroundColor(color)
216
                .frame(width: 28)
217
            Text(title)
218
                .foregroundColor(.secondary)
219
            Spacer()
220
            Text(value)
221
                .fontWeight(.semibold)
222
                .multilineTextAlignment(.trailing)
223
        }
224
        .font(.footnote)
225
    }
226

            
Bogdan Timofte authored 2 weeks ago
227
    private func secondaryDetailChip(
228
        title: String,
229
        value: String,
230
        symbol: String,
231
        color: Color
232
    ) -> some View {
233
        HStack(spacing: 10) {
234
            Image(systemName: symbol)
235
                .foregroundColor(color)
236
                .frame(width: 22, height: 22)
237
                .background(Circle().fill(color.opacity(0.12)))
238

            
239
            VStack(alignment: .leading, spacing: 2) {
240
                Text(title)
241
                    .foregroundColor(.secondary)
242
                Text(value)
243
                    .fontWeight(.semibold)
244
                    .lineLimit(1)
245
            }
246

            
247
            Spacer(minLength: 0)
248
        }
249
        .font(.caption)
250
        .frame(maxWidth: .infinity, alignment: .leading)
251
    }
252

            
Bogdan Timofte authored 2 weeks ago
253
    private func rangeText(min: Double, max: Double, unit: String) -> String? {
254
        guard min.isFinite, max.isFinite else { return nil }
255
        return "Min \(min.format(decimalDigits: 3)) \(unit)  Max \(max.format(decimalDigits: 3)) \(unit)"
256
    }
Bogdan Timofte authored 2 weeks ago
257
}