USB-Meter / USB Meter / Views / MeterDetailView.swift
Newer Older
226 lines | 8.39kb
Bogdan Timofte authored 2 months ago
1
import SwiftUI
2

            
3
struct MeterDetailView: View {
Bogdan Timofte authored a month ago
4
    @EnvironmentObject private var appData: AppData
5
    @Environment(\.dismiss) private var dismiss
6
    @State private var editorVisibility = false
7
    @State private var deleteConfirmationVisibility = false
8

            
Bogdan Timofte authored 2 months ago
9
    let meterSummary: AppData.MeterSummary
Bogdan Timofte authored 2 months ago
10

            
11
    var body: some View {
12
        ScrollView {
13
            VStack(spacing: 18) {
14
                headerCard
15
                statusCard
Bogdan Timofte authored 2 months ago
16
                identifiersCard
Bogdan Timofte authored 2 months ago
17
            }
18
            .padding()
19
        }
20
        .background(
21
            LinearGradient(
Bogdan Timofte authored 2 months ago
22
                colors: [meterSummary.tint.opacity(0.18), Color.clear],
Bogdan Timofte authored 2 months ago
23
                startPoint: .topLeading,
24
                endPoint: .bottomTrailing
25
            )
26
            .ignoresSafeArea()
27
        )
Bogdan Timofte authored 2 months ago
28
        .navigationTitle(meterSummary.displayName)
Bogdan Timofte authored a month ago
29
        .toolbar {
30
            ToolbarItemGroup(placement: .primaryAction) {
31
                Button("Edit") {
32
                    editorVisibility = true
33
                }
34
                Button(role: .destructive) {
35
                    deleteConfirmationVisibility = true
36
                } label: {
37
                    Image(systemName: "trash")
38
                }
39
            }
40
        }
41
        .sheet(isPresented: $editorVisibility) {
42
            MeterEditorSheetView(existingMeterSummary: meterSummary)
43
                .environmentObject(appData)
44
        }
45
        .alert("Delete Meter?", isPresented: $deleteConfirmationVisibility) {
46
            Button("Delete", role: .destructive) {
47
                if appData.deleteMeter(macAddress: meterSummary.macAddress) {
48
                    dismiss()
49
                }
50
            }
51
            Button("Cancel", role: .cancel) {}
52
        } message: {
53
            Text("This removes the stored meter entry and its saved metadata from the sidebar until the meter is discovered again.")
54
        }
Bogdan Timofte authored 2 months ago
55
    }
56

            
57
    private var headerCard: some View {
58
        VStack(alignment: .leading, spacing: 8) {
Bogdan Timofte authored 2 months ago
59
            Text(meterSummary.displayName)
Bogdan Timofte authored 2 months ago
60
                .font(.title2.weight(.semibold))
Bogdan Timofte authored 2 months ago
61
            Text(meterSummary.modelSummary)
Bogdan Timofte authored 2 months ago
62
                .font(.subheadline)
63
                .foregroundColor(.secondary)
Bogdan Timofte authored 2 months ago
64
            if let advertisedName = meterSummary.advertisedName {
Bogdan Timofte authored 2 months ago
65
                Text("Advertised as " + advertisedName)
66
                    .font(.caption2)
67
                    .foregroundColor(.secondary)
68
            }
69
        }
70
        .frame(maxWidth: .infinity, alignment: .leading)
71
        .padding(18)
Bogdan Timofte authored 2 months ago
72
        .meterCard(tint: meterSummary.tint, fillOpacity: 0.22, strokeOpacity: 0.28, cornerRadius: 20)
Bogdan Timofte authored 2 months ago
73
    }
74

            
75
    private var statusCard: some View {
76
        VStack(alignment: .leading, spacing: 10) {
Bogdan Timofte authored a month ago
77
            HStack(spacing: 8) {
78
                Text("Status")
79
                    .font(.headline)
80
                ContextInfoButton(
81
                    title: "Status",
82
                    message: "The meter is not currently connected. Bring it within Bluetooth range or wake it up to open live diagnostics."
83
                )
84
            }
Bogdan Timofte authored 2 months ago
85
            HStack(spacing: 8) {
86
                Circle()
Bogdan Timofte authored 2 months ago
87
                    .fill(meterSummary.tint)
Bogdan Timofte authored 2 months ago
88
                    .frame(width: 10, height: 10)
89
                Text("Offline")
90
                    .font(.caption.weight(.semibold))
91
                    .foregroundColor(.secondary)
92
            }
93
        }
94
        .frame(maxWidth: .infinity, alignment: .leading)
95
        .padding(18)
Bogdan Timofte authored 2 months ago
96
        .meterCard(tint: meterSummary.tint, fillOpacity: 0.16, strokeOpacity: 0.22, cornerRadius: 18)
Bogdan Timofte authored 2 months ago
97
    }
98

            
Bogdan Timofte authored 2 months ago
99
    private var identifiersCard: some View {
Bogdan Timofte authored 2 months ago
100
        VStack(alignment: .leading, spacing: 10) {
Bogdan Timofte authored 2 months ago
101
            Text("Identifiers")
Bogdan Timofte authored 2 months ago
102
                .font(.headline)
Bogdan Timofte authored 2 months ago
103
            infoRow(label: "MAC Address", value: meterSummary.macAddress)
104
            if let advertisedName = meterSummary.advertisedName {
105
                infoRow(label: "Advertised as", value: advertisedName)
106
            }
Bogdan Timofte authored 2 months ago
107
        }
108
        .frame(maxWidth: .infinity, alignment: .leading)
109
        .padding(18)
110
        .meterCard(tint: .secondary, fillOpacity: 0.12, strokeOpacity: 0.18, cornerRadius: 18)
111
    }
112

            
113
    private func infoRow(label: String, value: String) -> some View {
114
        HStack {
115
            Text(label)
116
            Spacer()
117
            Text(value)
118
                .foregroundColor(.secondary)
119
                .font(.caption)
120
        }
121
    }
122
}
123

            
124
struct MeterDetailView_Previews: PreviewProvider {
125
    static var previews: some View {
126
        MeterDetailView(
Bogdan Timofte authored 2 months ago
127
            meterSummary: AppData.MeterSummary(
Bogdan Timofte authored 2 months ago
128
                macAddress: "AA:BB:CC:DD:EE:FF",
129
                displayName: "Desk Meter",
130
                modelSummary: "UM25C",
131
                advertisedName: "UM25C-123",
132
                lastSeen: Date(),
133
                lastConnected: Date().addingTimeInterval(-3600),
134
                meter: nil
135
            )
136
        )
Bogdan Timofte authored a month ago
137
        .environmentObject(appData)
138
    }
139
}
140

            
141
struct MeterEditorSheetView: View {
142
    @EnvironmentObject private var appData: AppData
143
    @Environment(\.dismiss) private var dismiss
144

            
145
    let existingMeterSummary: AppData.MeterSummary?
146

            
147
    @State private var customName: String
148
    @State private var macAddress: String
149
    @State private var advertisedName: String
150
    @State private var selectedModel: Model
151

            
152
    init(existingMeterSummary: AppData.MeterSummary? = nil) {
153
        self.existingMeterSummary = existingMeterSummary
154
        _customName = State(initialValue: existingMeterSummary?.displayName ?? "")
155
        _macAddress = State(initialValue: existingMeterSummary?.macAddress ?? "")
156
        _advertisedName = State(initialValue: existingMeterSummary?.advertisedName ?? "")
157
        _selectedModel = State(initialValue: Self.model(for: existingMeterSummary?.modelSummary))
158
    }
159

            
160
    var body: some View {
161
        NavigationView {
162
            Form {
Bogdan Timofte authored a month ago
163
                Section(
164
                    header: ContextInfoHeader(
165
                        title: "Identity",
166
                        message: "Use the real BLE MAC address format `AA:BB:CC:DD:EE:FF`. Saved meters remain visible in the sidebar even when they are currently offline."
167
                    )
168
                ) {
Bogdan Timofte authored a month ago
169
                    TextField("Display name", text: $customName)
170
                    TextField("MAC Address", text: $macAddress)
171
                        .textInputAutocapitalization(.characters)
172
                        .disableAutocorrection(true)
173
                        .disabled(existingMeterSummary != nil)
174

            
175
                    Picker("Model", selection: $selectedModel) {
176
                        ForEach(Model.allCases, id: \.self) { model in
177
                            Text(model.canonicalName)
178
                                .tag(model)
179
                        }
180
                    }
181

            
182
                    TextField("Advertised name", text: $advertisedName)
183
                }
184
            }
185
            .navigationTitle(existingMeterSummary == nil ? "New Meter" : "Edit Meter")
186
            .navigationBarTitleDisplayMode(.inline)
187
            .toolbar {
188
                ToolbarItem(placement: .cancellationAction) {
189
                    Button("Cancel") {
190
                        dismiss()
191
                    }
192
                }
193
                ToolbarItem(placement: .confirmationAction) {
194
                    Button(existingMeterSummary == nil ? "Save" : "Update") {
195
                        let normalizedMAC = AppData.normalizedMACAddress(macAddress)
196
                        let didSave = appData.createKnownMeter(
197
                            macAddress: normalizedMAC,
198
                            customName: customName,
199
                            modelName: selectedModel.canonicalName,
200
                            advertisedName: advertisedName
201
                        )
202
                        if didSave {
203
                            dismiss()
204
                        }
205
                    }
206
                    .disabled(isSaveDisabled)
207
                }
208
            }
209
        }
210
        .navigationViewStyle(StackNavigationViewStyle())
211
    }
212

            
213
    private var isSaveDisabled: Bool {
214
        AppData.isValidMACAddress(AppData.normalizedMACAddress(macAddress)) == false
215
    }
216

            
217
    private static func model(for summary: String?) -> Model {
218
        if summary?.contains("UM34C") == true {
219
            return .UM34C
220
        }
221
        if summary?.contains("TC66C") == true {
222
            return .TC66C
223
        }
224
        return .UM25C
Bogdan Timofte authored 2 months ago
225
    }
226
}