USB-Meter / USB Meter / Views / Meter / Tabs / Connection / MeterConnectionTabView.swift
Newer Older
245 lines | 9.743kb
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 {
72
        VStack(alignment: .leading, spacing: compact ? 12 : 18) {
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
        .frame(maxWidth: .infinity, maxHeight: compact ? .infinity : nil, alignment: .topLeading)
97
        .meterCard(tint: meter.color, fillOpacity: 0.22, strokeOpacity: 0.24)
98
    }
99

            
100
    private func meterIdentity(compact: Bool) -> some View {
101
        HStack(alignment: .firstTextBaseline, spacing: 8) {
102
            Text(meter.name)
103
                .font(.system(compact ? .title3 : .title2, design: .rounded).weight(.bold))
104
                .lineLimit(1)
105
                .minimumScaleFactor(0.8)
106

            
107
            Text(meter.deviceModelName)
108
                .font((compact ? Font.caption : .subheadline).weight(.semibold))
109
                .foregroundColor(.secondary)
110
                .lineLimit(1)
111
                .minimumScaleFactor(0.8)
112
        }
113
    }
114

            
115
    private func actionGrid(compact: Bool = false, embedded: Bool = false) -> some View {
116
        let currentActionHeight = compact ? CGFloat(86) : actionButtonHeight
117

            
118
        return GeometryReader { proxy in
119
            let buttonWidth = actionButtonWidth(for: proxy.size.width)
120
            let stripWidth = actionStripWidth(for: buttonWidth)
121
            let stripContent = HStack(spacing: 0) {
122
                meterSheetButton(icon: "square.grid.2x2.fill", title: meter.dataGroupsTitle, tint: .teal, width: buttonWidth, height: currentActionHeight, compact: compact) {
123
                    dataGroupsViewVisibility.toggle()
124
                }
125
                .sheet(isPresented: $dataGroupsViewVisibility) {
126
                    DataGroupsView(visibility: $dataGroupsViewVisibility)
127
                        .environmentObject(meter)
128
                }
129

            
130
                if meter.supportsRecordingView {
131
                    actionStripDivider(height: currentActionHeight)
132
                    meterSheetButton(icon: "gauge.with.dots.needle.50percent", title: "Charge Record", tint: .pink, width: buttonWidth, height: currentActionHeight, compact: compact) {
133
                        recordingViewVisibility.toggle()
134
                    }
135
                    .sheet(isPresented: $recordingViewVisibility) {
136
                        RecordingView(visibility: $recordingViewVisibility)
137
                            .environmentObject(meter)
138
                    }
139
                }
140

            
141
                actionStripDivider(height: currentActionHeight)
142
                meterSheetButton(icon: "clock.arrow.circlepath", title: "App History", tint: .blue, width: buttonWidth, height: currentActionHeight, compact: compact) {
143
                    measurementsViewVisibility.toggle()
144
                }
145
                .sheet(isPresented: $measurementsViewVisibility) {
146
                    MeasurementsView(visibility: $measurementsViewVisibility)
147
                        .environmentObject(meter.measurements)
148
                }
149
            }
150
            .padding(actionStripPadding)
151
            .frame(width: stripWidth)
152

            
153
            HStack {
154
                Spacer(minLength: 0)
155
                stripContent
156
                    .meterCard(
157
                        tint: embedded ? meter.color : Color.secondary,
158
                        fillOpacity: embedded ? 0.08 : 0.10,
159
                        strokeOpacity: embedded ? 0.14 : 0.16,
160
                        cornerRadius: embedded ? 24 : 22
161
                    )
162
                Spacer(minLength: 0)
163
            }
164
        }
165
        .frame(height: currentActionHeight + (actionStripPadding * 2))
166
    }
167

            
168
    private func connectionActionArea(compact: Bool = false) -> some View {
169
        ConnectionPrimaryActionView(
170
            operationalState: meter.operationalState,
171
            compact: compact,
172
            connectAction: { meter.connect() },
173
            disconnectAction: { meter.disconnect() }
174
        )
175
    }
176

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

            
197
    private var visibleActionButtonCount: CGFloat {
198
        meter.supportsRecordingView ? 3 : 2
199
    }
200

            
201
    private func actionButtonWidth(for availableWidth: CGFloat) -> CGFloat {
202
        let dividerWidth = actionDividerWidth * max(visibleActionButtonCount - 1, 0)
203
        let contentWidth = availableWidth - (actionStripPadding * 2) - dividerWidth
204
        let fittedWidth = floor(contentWidth / visibleActionButtonCount)
205
        return min(actionButtonMaxWidth, max(actionButtonMinWidth, fittedWidth))
206
    }
207

            
208
    private func actionStripWidth(for buttonWidth: CGFloat) -> CGFloat {
209
        let dividerWidth = actionDividerWidth * max(visibleActionButtonCount - 1, 0)
210
        return (buttonWidth * visibleActionButtonCount) + dividerWidth + (actionStripPadding * 2)
211
    }
212

            
213
    private func actionStripDivider(height: CGFloat) -> some View {
214
        Rectangle()
215
            .fill(Color.secondary.opacity(0.16))
216
            .frame(width: actionDividerWidth, height: max(44, height - 22))
217
    }
218

            
219
    private var statusBadge: some View {
220
        ConnectionStatusBadgeView(text: statusText, color: statusColor)
221
    }
222

            
223
    private var statusText: String {
224
        switch meter.operationalState {
225
        case .notPresent:
226
            return "Missing"
227
        case .peripheralNotConnected:
228
            return "Ready"
229
        case .peripheralConnectionPending:
230
            return "Connecting"
231
        case .peripheralConnected:
232
            return "Linked"
233
        case .peripheralReady:
234
            return "Preparing"
235
        case .comunicating:
236
            return "Syncing"
237
        case .dataIsAvailable:
238
            return "Live"
239
        }
240
    }
241

            
242
    private var statusColor: Color {
243
        Meter.operationalColor(for: meter.operationalState)
244
    }
245
}