USB-Meter / USB Meter / Views / Meter / Tabs / Connection / MeterConnectionTabView.swift
Newer Older
254 lines | 9.979kb
Bogdan Timofte authored a week ago
1
//
2
//  MeterConnectionTabView.swift
3
//  USB Meter
4
//
5

            
6
import SwiftUI
7

            
8
struct MeterConnectionTabView: View {
9
    @EnvironmentObject private var meter: Meter
10

            
11
    let size: CGSize
12
    let isLandscape: Bool
13

            
14
    @State private var dataGroupsViewVisibility = false
15
    @State private var recordingViewVisibility = false
16
    @State private var measurementsViewVisibility = false
17

            
18
    private let actionStripPadding: CGFloat = 10
19
    private let actionDividerWidth: CGFloat = 1
20
    private let actionButtonMaxWidth: CGFloat = 156
21
    private let actionButtonMinWidth: CGFloat = 88
22
    private let actionButtonHeight: CGFloat = 108
23
    private let pageHorizontalPadding: CGFloat = 12
24
    private let pageVerticalPadding: CGFloat = 12
25

            
26
    var body: some View {
27
        Group {
28
            if isLandscape {
29
                landscapeFace {
30
                    VStack(alignment: .leading, spacing: 12) {
31
                        connectionCard(compact: true, showsActions: meter.operationalState == .dataIsAvailable)
32
                        ConnectionHomeInfoPreviewView(meter: meter)
33
                    }
34
                    .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
35
                }
36
            } else {
37
                portraitFace {
38
                    VStack(alignment: .leading, spacing: 12) {
39
                        connectionCard(
40
                            compact: prefersCompactPortraitConnection,
41
                            showsActions: meter.operationalState == .dataIsAvailable
42
                        )
43
                        ConnectionHomeInfoPreviewView(meter: meter)
44
                    }
45
                }
46
            }
47
        }
48
    }
49

            
50
    private var prefersCompactPortraitConnection: Bool {
51
        size.height < 760 || size.width < 380
52
    }
53

            
54
    private func portraitFace<Content: View>(@ViewBuilder content: () -> Content) -> some View {
55
        ScrollView {
56
            content()
57
                .frame(maxWidth: .infinity, alignment: .topLeading)
58
                .padding(.horizontal, pageHorizontalPadding)
59
                .padding(.vertical, pageVerticalPadding)
60
        }
61
    }
62

            
63
    private func landscapeFace<Content: View>(@ViewBuilder content: () -> Content) -> some View {
64
        content()
65
            .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
66
            .padding(.horizontal, pageHorizontalPadding)
67
            .padding(.vertical, pageVerticalPadding)
68
            .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
69
    }
70

            
71
    private func connectionCard(compact: Bool = false, showsActions: Bool = false) -> some View {
Bogdan Timofte authored a week ago
72
        let cardContent = VStack(alignment: .leading, spacing: compact ? 12 : 18) {
Bogdan Timofte authored a week ago
73
            HStack(alignment: .top) {
74
                meterIdentity(compact: compact)
75
                Spacer()
76
                statusBadge
77
            }
78

            
79
            if compact {
80
                Spacer(minLength: 0)
81
            }
82

            
83
            connectionActionArea(compact: compact)
84

            
85
            if showsActions {
86
                VStack(spacing: compact ? 10 : 12) {
87
                    Rectangle()
88
                        .fill(Color.secondary.opacity(0.12))
89
                        .frame(height: 1)
90

            
91
                    actionGrid(compact: compact, embedded: true)
92
                }
93
            }
94
        }
95
        .padding(compact ? 16 : 20)
96
        .meterCard(tint: meter.color, fillOpacity: 0.22, strokeOpacity: 0.24)
Bogdan Timofte authored a week ago
97

            
98
        return Group {
99
            if compact {
100
                cardContent
101
                    .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
102
            } else {
103
                cardContent
104
                    .frame(maxWidth: .infinity, alignment: .topLeading)
105
            }
106
        }
Bogdan Timofte authored a week ago
107
    }
108

            
109
    private func meterIdentity(compact: Bool) -> some View {
110
        HStack(alignment: .firstTextBaseline, spacing: 8) {
111
            Text(meter.name)
112
                .font(.system(compact ? .title3 : .title2, design: .rounded).weight(.bold))
113
                .lineLimit(1)
114
                .minimumScaleFactor(0.8)
115

            
116
            Text(meter.deviceModelName)
117
                .font((compact ? Font.caption : .subheadline).weight(.semibold))
118
                .foregroundColor(.secondary)
119
                .lineLimit(1)
120
                .minimumScaleFactor(0.8)
121
        }
122
    }
123

            
124
    private func actionGrid(compact: Bool = false, embedded: Bool = false) -> some View {
125
        let currentActionHeight = compact ? CGFloat(86) : actionButtonHeight
126

            
127
        return GeometryReader { proxy in
128
            let buttonWidth = actionButtonWidth(for: proxy.size.width)
129
            let stripWidth = actionStripWidth(for: buttonWidth)
130
            let stripContent = HStack(spacing: 0) {
131
                meterSheetButton(icon: "square.grid.2x2.fill", title: meter.dataGroupsTitle, tint: .teal, width: buttonWidth, height: currentActionHeight, compact: compact) {
132
                    dataGroupsViewVisibility.toggle()
133
                }
134
                .sheet(isPresented: $dataGroupsViewVisibility) {
135
                    DataGroupsView(visibility: $dataGroupsViewVisibility)
136
                        .environmentObject(meter)
137
                }
138

            
139
                if meter.supportsRecordingView {
140
                    actionStripDivider(height: currentActionHeight)
141
                    meterSheetButton(icon: "gauge.with.dots.needle.50percent", title: "Charge Record", tint: .pink, width: buttonWidth, height: currentActionHeight, compact: compact) {
142
                        recordingViewVisibility.toggle()
143
                    }
144
                    .sheet(isPresented: $recordingViewVisibility) {
145
                        RecordingView(visibility: $recordingViewVisibility)
146
                            .environmentObject(meter)
147
                    }
148
                }
149

            
150
                actionStripDivider(height: currentActionHeight)
151
                meterSheetButton(icon: "clock.arrow.circlepath", title: "App History", tint: .blue, width: buttonWidth, height: currentActionHeight, compact: compact) {
152
                    measurementsViewVisibility.toggle()
153
                }
154
                .sheet(isPresented: $measurementsViewVisibility) {
155
                    MeasurementsView(visibility: $measurementsViewVisibility)
156
                        .environmentObject(meter.measurements)
157
                }
158
            }
159
            .padding(actionStripPadding)
160
            .frame(width: stripWidth)
161

            
162
            HStack {
163
                Spacer(minLength: 0)
164
                stripContent
165
                    .meterCard(
166
                        tint: embedded ? meter.color : Color.secondary,
167
                        fillOpacity: embedded ? 0.08 : 0.10,
168
                        strokeOpacity: embedded ? 0.14 : 0.16,
169
                        cornerRadius: embedded ? 24 : 22
170
                    )
171
                Spacer(minLength: 0)
172
            }
173
        }
174
        .frame(height: currentActionHeight + (actionStripPadding * 2))
175
    }
176

            
177
    private func connectionActionArea(compact: Bool = false) -> some View {
178
        ConnectionPrimaryActionView(
179
            operationalState: meter.operationalState,
180
            compact: compact,
181
            connectAction: { meter.connect() },
182
            disconnectAction: { meter.disconnect() }
183
        )
184
    }
185

            
186
    private func meterSheetButton(icon: String, title: String, tint: Color, width: CGFloat, height: CGFloat, compact: Bool = false, action: @escaping () -> Void) -> some View {
187
        Button(action: action) {
188
            VStack(spacing: compact ? 8 : 10) {
189
                Image(systemName: icon)
190
                    .font(.system(size: compact ? 18 : 20, weight: .semibold))
191
                    .frame(width: compact ? 34 : 40, height: compact ? 34 : 40)
192
                    .background(Circle().fill(tint.opacity(0.14)))
193
                Text(title)
194
                    .font((compact ? Font.caption : .footnote).weight(.semibold))
195
                    .multilineTextAlignment(.center)
196
                    .lineLimit(2)
197
                    .minimumScaleFactor(0.9)
198
            }
199
            .foregroundColor(tint)
200
            .frame(width: width, height: height)
201
            .contentShape(Rectangle())
202
        }
203
        .buttonStyle(.plain)
204
    }
205

            
206
    private var visibleActionButtonCount: CGFloat {
207
        meter.supportsRecordingView ? 3 : 2
208
    }
209

            
210
    private func actionButtonWidth(for availableWidth: CGFloat) -> CGFloat {
211
        let dividerWidth = actionDividerWidth * max(visibleActionButtonCount - 1, 0)
212
        let contentWidth = availableWidth - (actionStripPadding * 2) - dividerWidth
213
        let fittedWidth = floor(contentWidth / visibleActionButtonCount)
214
        return min(actionButtonMaxWidth, max(actionButtonMinWidth, fittedWidth))
215
    }
216

            
217
    private func actionStripWidth(for buttonWidth: CGFloat) -> CGFloat {
218
        let dividerWidth = actionDividerWidth * max(visibleActionButtonCount - 1, 0)
219
        return (buttonWidth * visibleActionButtonCount) + dividerWidth + (actionStripPadding * 2)
220
    }
221

            
222
    private func actionStripDivider(height: CGFloat) -> some View {
223
        Rectangle()
224
            .fill(Color.secondary.opacity(0.16))
225
            .frame(width: actionDividerWidth, height: max(44, height - 22))
226
    }
227

            
228
    private var statusBadge: some View {
229
        ConnectionStatusBadgeView(text: statusText, color: statusColor)
230
    }
231

            
232
    private var statusText: String {
233
        switch meter.operationalState {
234
        case .notPresent:
235
            return "Missing"
236
        case .peripheralNotConnected:
237
            return "Ready"
238
        case .peripheralConnectionPending:
239
            return "Connecting"
240
        case .peripheralConnected:
241
            return "Linked"
242
        case .peripheralReady:
243
            return "Preparing"
244
        case .comunicating:
245
            return "Syncing"
246
        case .dataIsAvailable:
247
            return "Live"
248
        }
249
    }
250

            
251
    private var statusColor: Color {
252
        Meter.operationalColor(for: meter.operationalState)
253
    }
254
}