Showing 4 changed files with 89 additions and 267 deletions
+0 -3
USB Meter.xcodeproj/project.pbxproj
@@ -50,7 +50,6 @@
50 50
 		43CBF681240D153000255B8B /* CBManagerState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43CBF680240D153000255B8B /* CBManagerState.swift */; };
51 51
 		43ED78AE2420A0BE00974487 /* BluetoothSerial.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43ED78AD2420A0BE00974487 /* BluetoothSerial.swift */; };
52 52
 		43F7792B2465AE1600745DF4 /* UIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43F7792A2465AE1600745DF4 /* UIView.swift */; };
53
-		AAD5F9A72B1CAC0700F8E4F9 /* MeterDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAD5F9A32B1CAC0700F8E4F9 /* MeterDetailView.swift */; };
54 53
 		AAD5F9B12B1CAC7A00F8E4F9 /* SidebarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAD5F9B22B1CAC7A00F8E4F9 /* SidebarView.swift */; };
55 54
 		B0A000113C8F000100A10011 /* ChargerStandbyPowerStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0A000013C8F000100A10001 /* ChargerStandbyPowerStore.swift */; };
56 55
 		B0A000123C8F000100A10012 /* ChargerStandbyPowerWizardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0A000023C8F000100A10002 /* ChargerStandbyPowerWizardView.swift */; };
@@ -177,7 +176,6 @@
177 176
 		43F7792A2465AE1600745DF4 /* UIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIView.swift; sourceTree = "<group>"; };
178 177
 		56BA4CE53B6B2C4EBA42C81A /* MeterMappingDebugView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeterMappingDebugView.swift; sourceTree = "<group>"; };
179 178
 		7396C8BB36F4E7F8E0CD8FF8 /* MeterNameStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeterNameStore.swift; sourceTree = "<group>"; };
180
-		AAD5F9A32B1CAC0700F8E4F9 /* MeterDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeterDetailView.swift; sourceTree = "<group>"; };
181 179
 		AAD5F9B22B1CAC7A00F8E4F9 /* SidebarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarView.swift; sourceTree = "<group>"; };
182 180
 		B0A000013C8F000100A10001 /* ChargerStandbyPowerStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChargerStandbyPowerStore.swift; sourceTree = "<group>"; };
183 181
 		B0A000023C8F000100A10002 /* ChargerStandbyPowerWizardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChargerStandbyPowerWizardView.swift; sourceTree = "<group>"; };
@@ -942,7 +940,6 @@
942 940
 				430CB4FC245E07EB006525C2 /* ChevronView.swift in Sources */,
943 941
 				43554B3424444B0E004E66F5 /* Date.swift in Sources */,
944 942
 				4311E63A241384960080EA59 /* DeviceHelpView.swift in Sources */,
945
-				AAD5F9A72B1CAC0700F8E4F9 /* MeterDetailView.swift in Sources */,
946 943
 				E430FB6B7CB3E0D4189F6D7D /* MeterMappingDebugView.swift in Sources */,
947 944
 				439D996524234B98008DE3AA /* BluetoothRadio.swift in Sources */,
948 945
 				438695892463F062008855A9 /* Measurements.swift in Sources */,
+0 -258
USB Meter/Views/MeterDetailView.swift
@@ -1,258 +0,0 @@
1
-import SwiftUI
2
-
3
-/// Offline meter view - shows meter information when not connected.
4
-/// Uses same tab-based layout as MeterView but filtered to available operations.
5
-struct MeterDetailView: View {
6
-    @EnvironmentObject private var appData: AppData
7
-    @Environment(\.dismiss) private var dismiss
8
-    @State private var editorVisibility = false
9
-    @State private var deleteConfirmationVisibility = false
10
-
11
-    let meterSummary: AppData.MeterSummary
12
-
13
-    private let meterColor = Color.orange
14
-
15
-    var body: some View {
16
-        GeometryReader { proxy in
17
-            let landscape = proxy.size.width > proxy.size.height
18
-
19
-            VStack(spacing: 0) {
20
-                // Compact header matching online meters
21
-                HStack(spacing: 12) {
22
-                    Text(meterSummary.displayName.isEmpty ? "Meter" : meterSummary.displayName)
23
-                        .font(.headline)
24
-                        .lineLimit(1)
25
-
26
-                    Spacer()
27
-
28
-                    Button("Edit") {
29
-                        editorVisibility = true
30
-                    }
31
-                    .font(.body.weight(.semibold))
32
-
33
-                    Button(role: .destructive) {
34
-                        deleteConfirmationVisibility = true
35
-                    } label: {
36
-                        Image(systemName: "trash")
37
-                    }
38
-                    .font(.body.weight(.semibold))
39
-                }
40
-                .padding(.horizontal, 16)
41
-                .padding(.vertical, 10)
42
-                .background(
43
-                    Rectangle()
44
-                        .fill(.ultraThinMaterial)
45
-                        .ignoresSafeArea(edges: .top)
46
-                )
47
-                .overlay(alignment: .bottom) {
48
-                    Rectangle()
49
-                        .fill(Color.secondary.opacity(0.12))
50
-                        .frame(height: 1)
51
-                }
52
-
53
-                // Content
54
-                ScrollView {
55
-                    VStack(spacing: 18) {
56
-                        headerCard
57
-                        statusCard
58
-                        identifiersCard
59
-                    }
60
-                    .padding()
61
-                }
62
-                .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
63
-            }
64
-        }
65
-        .background(
66
-            LinearGradient(
67
-                colors: [meterColor.opacity(0.18), Color.clear],
68
-                startPoint: .topLeading,
69
-                endPoint: .bottomTrailing
70
-            )
71
-            .ignoresSafeArea()
72
-        )
73
-        .navigationBarHidden(true)
74
-        .sheet(isPresented: $editorVisibility) {
75
-            MeterEditorSheetView(existingMeterSummary: meterSummary)
76
-                .environmentObject(appData)
77
-        }
78
-        .alert("Delete Meter?", isPresented: $deleteConfirmationVisibility) {
79
-            Button("Delete", role: .destructive) {
80
-                if appData.deleteMeter(macAddress: meterSummary.macAddress) {
81
-                    dismiss()
82
-                }
83
-            }
84
-            Button("Cancel", role: .cancel) {}
85
-        } message: {
86
-            Text("This removes the stored meter entry and its saved metadata from the sidebar until the meter is discovered again.")
87
-        }
88
-    }
89
-
90
-    private var headerCard: some View {
91
-        VStack(alignment: .leading, spacing: 8) {
92
-            Text(meterSummary.displayName)
93
-                .font(.title2.weight(.semibold))
94
-            Text(meterSummary.modelSummary)
95
-                .font(.subheadline)
96
-                .foregroundColor(.secondary)
97
-            if let advertisedName = meterSummary.advertisedName {
98
-                Text("Advertised as " + advertisedName)
99
-                    .font(.caption2)
100
-                    .foregroundColor(.secondary)
101
-            }
102
-        }
103
-        .frame(maxWidth: .infinity, alignment: .leading)
104
-        .padding(18)
105
-        .meterCard(tint: meterColor, fillOpacity: 0.22, strokeOpacity: 0.28, cornerRadius: 20)
106
-    }
107
-
108
-    private var statusCard: some View {
109
-        VStack(alignment: .leading, spacing: 10) {
110
-            HStack(spacing: 8) {
111
-                Text("Status")
112
-                    .font(.headline)
113
-                ContextInfoButton(
114
-                    title: "Status",
115
-                    message: "This meter is offline. Bring it within Bluetooth range to connect and view live data."
116
-                )
117
-            }
118
-            HStack(spacing: 8) {
119
-                Circle()
120
-                    .fill(Color.red)
121
-                    .frame(width: 10, height: 10)
122
-                Text("Offline")
123
-                    .font(.caption.weight(.semibold))
124
-                    .foregroundColor(.secondary)
125
-            }
126
-        }
127
-        .frame(maxWidth: .infinity, alignment: .leading)
128
-        .padding(18)
129
-        .meterCard(tint: meterColor, fillOpacity: 0.16, strokeOpacity: 0.22, cornerRadius: 18)
130
-    }
131
-
132
-    private var identifiersCard: some View {
133
-        VStack(alignment: .leading, spacing: 10) {
134
-            Text("Identifiers")
135
-                .font(.headline)
136
-            infoRow(label: "MAC Address", value: meterSummary.macAddress)
137
-            if let advertisedName = meterSummary.advertisedName {
138
-                infoRow(label: "Advertised as", value: advertisedName)
139
-            }
140
-        }
141
-        .frame(maxWidth: .infinity, alignment: .leading)
142
-        .padding(18)
143
-        .meterCard(tint: .secondary, fillOpacity: 0.12, strokeOpacity: 0.18, cornerRadius: 18)
144
-    }
145
-
146
-    private func infoRow(label: String, value: String) -> some View {
147
-        HStack {
148
-            Text(label)
149
-            Spacer()
150
-            Text(value)
151
-                .foregroundColor(.secondary)
152
-                .font(.caption)
153
-        }
154
-    }
155
-}
156
-
157
-struct MeterDetailView_Previews: PreviewProvider {
158
-    static var previews: some View {
159
-        MeterDetailView(
160
-            meterSummary: AppData.MeterSummary(
161
-                macAddress: "AA:BB:CC:DD:EE:FF",
162
-                displayName: "Desk Meter",
163
-                modelSummary: "UM25C",
164
-                advertisedName: "UM25C-123",
165
-                lastSeen: Date(),
166
-                lastConnected: Date().addingTimeInterval(-3600),
167
-                meter: nil
168
-            )
169
-        )
170
-    }
171
-}
172
-
173
-struct MeterEditorSheetView: View {
174
-    @EnvironmentObject private var appData: AppData
175
-    @Environment(\.dismiss) private var dismiss
176
-
177
-    let existingMeterSummary: AppData.MeterSummary?
178
-
179
-    @State private var customName: String
180
-    @State private var macAddress: String
181
-    @State private var advertisedName: String
182
-    @State private var selectedModel: Model
183
-
184
-    init(existingMeterSummary: AppData.MeterSummary? = nil) {
185
-        self.existingMeterSummary = existingMeterSummary
186
-        _customName = State(initialValue: existingMeterSummary?.displayName ?? "")
187
-        _macAddress = State(initialValue: existingMeterSummary?.macAddress ?? "")
188
-        _advertisedName = State(initialValue: existingMeterSummary?.advertisedName ?? "")
189
-        _selectedModel = State(initialValue: Self.model(for: existingMeterSummary?.modelSummary))
190
-    }
191
-
192
-    var body: some View {
193
-        NavigationView {
194
-            Form {
195
-                Section(
196
-                    header: ContextInfoHeader(
197
-                        title: "Identity",
198
-                        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."
199
-                    )
200
-                ) {
201
-                    TextField("Display name", text: $customName)
202
-                    TextField("MAC Address", text: $macAddress)
203
-                        .textInputAutocapitalization(.characters)
204
-                        .disableAutocorrection(true)
205
-                        .disabled(existingMeterSummary != nil)
206
-
207
-                    Picker("Model", selection: $selectedModel) {
208
-                        ForEach(Model.allCases, id: \.self) { model in
209
-                            Text(model.canonicalName)
210
-                                .tag(model)
211
-                        }
212
-                    }
213
-
214
-                    TextField("Advertised name", text: $advertisedName)
215
-                }
216
-            }
217
-            .navigationTitle(existingMeterSummary == nil ? "New Meter" : "Edit Meter")
218
-            .navigationBarTitleDisplayMode(.inline)
219
-            .toolbar {
220
-                ToolbarItem(placement: .cancellationAction) {
221
-                    Button("Cancel") {
222
-                        dismiss()
223
-                    }
224
-                }
225
-                ToolbarItem(placement: .confirmationAction) {
226
-                    Button(existingMeterSummary == nil ? "Save" : "Update") {
227
-                        let normalizedMAC = AppData.normalizedMACAddress(macAddress)
228
-                        let didSave = appData.createKnownMeter(
229
-                            macAddress: normalizedMAC,
230
-                            customName: customName,
231
-                            modelName: selectedModel.canonicalName,
232
-                            advertisedName: advertisedName
233
-                        )
234
-                        if didSave {
235
-                            dismiss()
236
-                        }
237
-                    }
238
-                    .disabled(isSaveDisabled)
239
-                }
240
-            }
241
-        }
242
-        .navigationViewStyle(StackNavigationViewStyle())
243
-    }
244
-
245
-    private var isSaveDisabled: Bool {
246
-        AppData.isValidMACAddress(AppData.normalizedMACAddress(macAddress)) == false
247
-    }
248
-
249
-    private static func model(for summary: String?) -> Model {
250
-        if summary?.contains("UM34C") == true {
251
-            return .UM34C
252
-        }
253
-        if summary?.contains("TC66C") == true {
254
-            return .TC66C
255
-        }
256
-        return .UM25C
257
-    }
258
-}
+0 -6
USB Meter/Views/Sidebar/SidebarList/Sections/SidebarUSBMetersSectionView.swift
@@ -41,12 +41,6 @@ struct SidebarUSBMetersSectionView: View {
41 41
                             }
42 42
                             .buttonStyle(.plain)
43 43
                             .transition(.opacity.combined(with: .move(edge: .top)))
44
-                        } else {
45
-                            NavigationLink(destination: MeterDetailView(meterSummary: meterSummary)) {
46
-                                MeterCardView(meterSummary: meterSummary)
47
-                            }
48
-                            .buttonStyle(.plain)
49
-                            .transition(.opacity.combined(with: .move(edge: .top)))
50 44
                         }
51 45
                     }
52 46
                 }
+89 -0
USB Meter/Views/Sidebar/SidebarView.swift
@@ -202,3 +202,92 @@ struct SidebarView: View {
202 202
         UIApplication.shared.open(settingsURL, options: [:], completionHandler: nil)
203 203
     }
204 204
 }
205
+
206
+// MARK: - Meter Editor Sheet
207
+
208
+struct MeterEditorSheetView: View {
209
+    @EnvironmentObject private var appData: AppData
210
+    @Environment(\.dismiss) private var dismiss
211
+
212
+    let existingMeterSummary: AppData.MeterSummary?
213
+
214
+    @State private var customName: String
215
+    @State private var macAddress: String
216
+    @State private var advertisedName: String
217
+    @State private var selectedModel: Model
218
+
219
+    init(existingMeterSummary: AppData.MeterSummary? = nil) {
220
+        self.existingMeterSummary = existingMeterSummary
221
+        _customName = State(initialValue: existingMeterSummary?.displayName ?? "")
222
+        _macAddress = State(initialValue: existingMeterSummary?.macAddress ?? "")
223
+        _advertisedName = State(initialValue: existingMeterSummary?.advertisedName ?? "")
224
+        _selectedModel = State(initialValue: Self.model(for: existingMeterSummary?.modelSummary))
225
+    }
226
+
227
+    var body: some View {
228
+        NavigationView {
229
+            Form {
230
+                Section(
231
+                    header: ContextInfoHeader(
232
+                        title: "Identity",
233
+                        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."
234
+                    )
235
+                ) {
236
+                    TextField("Display name", text: $customName)
237
+                    TextField("MAC Address", text: $macAddress)
238
+                        .textInputAutocapitalization(.characters)
239
+                        .disableAutocorrection(true)
240
+                        .disabled(existingMeterSummary != nil)
241
+
242
+                    Picker("Model", selection: $selectedModel) {
243
+                        ForEach(Model.allCases, id: \.self) { model in
244
+                            Text(model.canonicalName)
245
+                                .tag(model)
246
+                        }
247
+                    }
248
+
249
+                    TextField("Advertised name", text: $advertisedName)
250
+                }
251
+            }
252
+            .navigationTitle(existingMeterSummary == nil ? "New Meter" : "Edit Meter")
253
+            .navigationBarTitleDisplayMode(.inline)
254
+            .toolbar {
255
+                ToolbarItem(placement: .cancellationAction) {
256
+                    Button("Cancel") {
257
+                        dismiss()
258
+                    }
259
+                }
260
+                ToolbarItem(placement: .confirmationAction) {
261
+                    Button(existingMeterSummary == nil ? "Save" : "Update") {
262
+                        let normalizedMAC = AppData.normalizedMACAddress(macAddress)
263
+                        let didSave = appData.createKnownMeter(
264
+                            macAddress: normalizedMAC,
265
+                            customName: customName,
266
+                            modelName: selectedModel.canonicalName,
267
+                            advertisedName: advertisedName
268
+                        )
269
+                        if didSave {
270
+                            dismiss()
271
+                        }
272
+                    }
273
+                    .disabled(isSaveDisabled)
274
+                }
275
+            }
276
+        }
277
+        .navigationViewStyle(StackNavigationViewStyle())
278
+    }
279
+
280
+    private var isSaveDisabled: Bool {
281
+        AppData.isValidMACAddress(AppData.normalizedMACAddress(macAddress)) == false
282
+    }
283
+
284
+    private static func model(for summary: String?) -> Model {
285
+        if summary?.contains("UM34C") == true {
286
+            return .UM34C
287
+        }
288
+        if summary?.contains("TC66C") == true {
289
+            return .TC66C
290
+        }
291
+        return .UM25C
292
+    }
293
+}