Newer Older
243 lines | 9.586kb
Bogdan Timofte authored a week ago
1
//
Bogdan Timofte authored a week ago
2
//  MeterHomeTabView.swift
Bogdan Timofte authored a week ago
3
//  USB Meter
4
//
5

            
6
import SwiftUI
7

            
Bogdan Timofte authored a week ago
8
struct MeterHomeTabView: View {
Bogdan Timofte authored a week ago
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)
Bogdan Timofte authored a week ago
32
                        MeterOverviewSectionView(meter: meter)
Bogdan Timofte authored a week ago
33
                    }
34
                    .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
35
                }
36
            } else {
37
                portraitFace {
38
                    VStack(alignment: .leading, spacing: 12) {
39
                        connectionCard(
Bogdan Timofte authored a week ago
40
                            compact: prefersCompactPortraitLayout,
Bogdan Timofte authored a week ago
41
                            showsActions: meter.operationalState == .dataIsAvailable
42
                        )
Bogdan Timofte authored a week ago
43
                        MeterOverviewSectionView(meter: meter)
Bogdan Timofte authored a week ago
44
                    }
45
                }
46
            }
47
        }
48
    }
49

            
Bogdan Timofte authored a week ago
50
    private var prefersCompactPortraitLayout: Bool {
Bogdan Timofte authored a week ago
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 {
Bogdan Timofte authored a week ago
64
        ScrollView {
65
            content()
66
                .frame(maxWidth: .infinity, alignment: .topLeading)
67
                .padding(.horizontal, pageHorizontalPadding)
68
                .padding(.vertical, pageVerticalPadding)
69
        }
70
        .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
Bogdan Timofte authored a week ago
71
    }
72

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

            
81
            connectionActionArea(compact: compact)
82

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

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

            
Bogdan Timofte authored a week ago
96
        return cardContent
97
            .frame(maxWidth: .infinity, alignment: .topLeading)
Bogdan Timofte authored a week ago
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) {
Bogdan Timofte authored a week ago
126
                    DataGroupsSheetView(visibility: $dataGroupsViewVisibility)
Bogdan Timofte authored a week ago
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) {
Bogdan Timofte authored a week ago
136
                        ChargeRecordSheetView(visibility: $recordingViewVisibility)
Bogdan Timofte authored a week ago
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) {
Bogdan Timofte authored a week ago
146
                    AppHistorySheetView(visibility: $measurementsViewVisibility)
Bogdan Timofte authored a week ago
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 {
Bogdan Timofte authored a week ago
169
        MeterConnectionActionView(
Bogdan Timofte authored a week ago
170
            operationalState: meter.operationalState,
Bogdan Timofte authored a week ago
171
            compact: compact
Bogdan Timofte authored a week ago
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 {
Bogdan Timofte authored a week ago
218
        MeterConnectionStatusBadgeView(text: statusText, color: statusColor)
Bogdan Timofte authored a week ago
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
}