USB-Meter / USB Meter / Views / Meter / Tabs / Connection / MeterConnectionTabView.swift
Newer Older
243 lines | 9.678kb
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
            connectionActionArea(compact: compact)
80

            
81
            if showsActions {
82
                VStack(spacing: compact ? 10 : 12) {
83
                    Rectangle()
84
                        .fill(Color.secondary.opacity(0.12))
85
                        .frame(height: 1)
86

            
87
                    actionGrid(compact: compact, embedded: true)
88
                }
89
            }
90
        }
91
        .padding(compact ? 16 : 20)
92
        .meterCard(tint: meter.color, fillOpacity: 0.22, strokeOpacity: 0.24)
Bogdan Timofte authored a week ago
93

            
Bogdan Timofte authored a week ago
94
        return cardContent
95
            .frame(maxWidth: .infinity, alignment: .topLeading)
Bogdan Timofte authored a week ago
96
    }
97

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

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

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

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

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

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

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

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

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

            
195
    private var visibleActionButtonCount: CGFloat {
196
        meter.supportsRecordingView ? 3 : 2
197
    }
198

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

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

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

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

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

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