Showing 5 changed files with 364 additions and 546 deletions
+228 -14
USB Meter.xcodeproj/project.pbxproj
@@ -3,15 +3,15 @@
3 3
 	archiveVersion = 1;
4 4
 	classes = {
5 5
 	};
6
-	objectVersion = 54;
6
+	objectVersion = 70;
7 7
 	objects = {
8 8
 
9 9
 /* Begin PBXBuildFile section */
10
+		3407A133FADB8858DC2A1FED /* MeterNameStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7396C8BB36F4E7F8E0CD8FF8 /* MeterNameStore.swift */; };
10 11
 		4308CF8624176CAB0002E80B /* DataGroupRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4308CF8524176CAB0002E80B /* DataGroupRowView.swift */; };
11 12
 		4308CF882417770D0002E80B /* DataGroupsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4308CF872417770D0002E80B /* DataGroupsView.swift */; };
12 13
 		430CB4FC245E07EB006525C2 /* ChevronView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 430CB4FB245E07EB006525C2 /* ChevronView.swift */; };
13 14
 		4311E63A241384960080EA59 /* DeviceHelpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4311E639241384960080EA59 /* DeviceHelpView.swift */; };
14
-		E430FB6B7CB3E0D4189F6D7D /* MeterMappingDebugView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BA4CE53B6B2C4EBA42C81A /* MeterMappingDebugView.swift */; };
15 15
 		4327461B24619CED0009BE4B /* MeterRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4327461A24619CED0009BE4B /* MeterRowView.swift */; };
16 16
 		432EA6442445A559006FC905 /* ChartContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 432EA6432445A559006FC905 /* ChartContext.swift */; };
17 17
 		4347F01D28D717C1007EE7B1 /* CryptoSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 4347F01C28D717C1007EE7B1 /* CryptoSwift */; };
@@ -28,9 +28,25 @@
28 28
 		4383B460240EB2D000DAAEBF /* Meter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4383B45F240EB2D000DAAEBF /* Meter.swift */; };
29 29
 		4383B462240EB5E400DAAEBF /* AppData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4383B461240EB5E400DAAEBF /* AppData.swift */; };
30 30
 		4383B465240EB6B200DAAEBF /* UserDefault.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4383B464240EB6B200DAAEBF /* UserDefault.swift */; };
31
-		3407A133FADB8858DC2A1FED /* MeterNameStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7396C8BB36F4E7F8E0CD8FF8 /* MeterNameStore.swift */; };
32 31
 		4383B468240F845500DAAEBF /* MacAdress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4383B467240F845500DAAEBF /* MacAdress.swift */; };
33 32
 		4383B46A240FE4A600DAAEBF /* MeterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4383B469240FE4A600DAAEBF /* MeterView.swift */; };
33
+		D28F11013C8E4A7A00A10011 /* MeterConnectionTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28F11023C8E4A7A00A10012 /* MeterConnectionTabView.swift */; };
34
+		D28F11033C8E4A7A00A10013 /* MeterLiveTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28F11043C8E4A7A00A10014 /* MeterLiveTabView.swift */; };
35
+		D28F11053C8E4A7A00A10015 /* MeterChartTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28F11063C8E4A7A00A10016 /* MeterChartTabView.swift */; };
36
+		D28F11073C8E4A7A00A10017 /* MeterSettingsTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28F11083C8E4A7A00A10018 /* MeterSettingsTabView.swift */; };
37
+		D28F11113C8E4A7A00A10021 /* MeterInfoCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28F11123C8E4A7A00A10022 /* MeterInfoCard.swift */; };
38
+		D28F11133C8E4A7A00A10023 /* MeterInfoRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28F11143C8E4A7A00A10024 /* MeterInfoRow.swift */; };
39
+		D28F11153C8E4A7A00A10025 /* EditNameView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28F11163C8E4A7A00A10026 /* EditNameView.swift */; };
40
+		D28F11173C8E4A7A00A10027 /* EditScreenTimeoutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28F11183C8E4A7A00A10028 /* EditScreenTimeoutView.swift */; };
41
+		D28F11193C8E4A7A00A10029 /* EditScreenBrightnessView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28F111A3C8E4A7A00A1002A /* EditScreenBrightnessView.swift */; };
42
+		D28F11213C8E4A7A00A10031 /* LiveMetricRange.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28F11223C8E4A7A00A10032 /* LiveMetricRange.swift */; };
43
+		D28F11233C8E4A7A00A10033 /* LoadResistanceSymbolView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28F11243C8E4A7A00A10034 /* LoadResistanceSymbolView.swift */; };
44
+		D28F11313C8E4A7A00A10041 /* ControlActionButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28F11323C8E4A7A00A10042 /* ControlActionButtonView.swift */; };
45
+		D28F11333C8E4A7A00A10043 /* ControlCurrentScreenCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28F11343C8E4A7A00A10044 /* ControlCurrentScreenCardView.swift */; };
46
+		D28F11353C8E4A7A00A10045 /* RecordingMetricsTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28F11363C8E4A7A00A10046 /* RecordingMetricsTableView.swift */; };
47
+		D28F11393C8E4A7A00A10049 /* ConnectionStatusBadgeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28F113A3C8E4A7A00A1004A /* ConnectionStatusBadgeView.swift */; };
48
+		D28F113B3C8E4A7A00A1004B /* ConnectionPrimaryActionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28F113C3C8E4A7A00A1004C /* ConnectionPrimaryActionView.swift */; };
49
+		D28F113D3C8E4A7A00A1004D /* ConnectionHomeInfoPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28F113E3C8E4A7A00A1004E /* ConnectionHomeInfoPreviewView.swift */; };
34 50
 		438695892463F062008855A9 /* Measurements.swift in Sources */ = {isa = PBXBuildFile; fileRef = 438695882463F062008855A9 /* Measurements.swift */; };
35 51
 		4386958B2F6A1001008855A9 /* UMProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4386958A2F6A1001008855A9 /* UMProtocol.swift */; };
36 52
 		4386958D2F6A1002008855A9 /* TC66Protocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4386958C2F6A1002008855A9 /* TC66Protocol.swift */; };
@@ -48,9 +64,11 @@
48 64
 		43CBF66F240BF3ED00255B8B /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 43CBF66D240BF3ED00255B8B /* LaunchScreen.storyboard */; };
49 65
 		43CBF677240C043E00255B8B /* BluetoothManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43CBF676240C043E00255B8B /* BluetoothManager.swift */; };
50 66
 		43CBF681240D153000255B8B /* CBManagerState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43CBF680240D153000255B8B /* CBManagerState.swift */; };
51
-		43DFBE402441A37B004A47EA /* BorderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43DFBE3F2441A37B004A47EA /* BorderView.swift */; };
52 67
 		43ED78AE2420A0BE00974487 /* BluetoothSerial.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43ED78AD2420A0BE00974487 /* BluetoothSerial.swift */; };
53 68
 		43F7792B2465AE1600745DF4 /* UIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43F7792A2465AE1600745DF4 /* UIView.swift */; };
69
+		AAD5F9A72B1CAC0700F8E4F9 /* MeterDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAD5F9A32B1CAC0700F8E4F9 /* MeterDetailView.swift */; };
70
+		AAD5F9B12B1CAC7A00F8E4F9 /* SidebarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAD5F9B22B1CAC7A00F8E4F9 /* SidebarView.swift */; };
71
+		E430FB6B7CB3E0D4189F6D7D /* MeterMappingDebugView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BA4CE53B6B2C4EBA42C81A /* MeterMappingDebugView.swift */; };
54 72
 /* End PBXBuildFile section */
55 73
 
56 74
 /* Begin PBXFileReference section */
@@ -89,7 +107,6 @@
89 107
 		4308CF872417770D0002E80B /* DataGroupsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataGroupsView.swift; sourceTree = "<group>"; };
90 108
 		430CB4FB245E07EB006525C2 /* ChevronView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChevronView.swift; sourceTree = "<group>"; };
91 109
 		4311E639241384960080EA59 /* DeviceHelpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceHelpView.swift; sourceTree = "<group>"; };
92
-		56BA4CE53B6B2C4EBA42C81A /* MeterMappingDebugView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeterMappingDebugView.swift; sourceTree = "<group>"; };
93 110
 		4327461A24619CED0009BE4B /* MeterRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeterRowView.swift; sourceTree = "<group>"; };
94 111
 		432EA6432445A559006FC905 /* ChartContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChartContext.swift; sourceTree = "<group>"; };
95 112
 		4351E7BA24685ACD00E798A3 /* CGPoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGPoint.swift; sourceTree = "<group>"; };
@@ -105,9 +122,25 @@
105 122
 		4383B45F240EB2D000DAAEBF /* Meter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Meter.swift; sourceTree = "<group>"; };
106 123
 		4383B461240EB5E400DAAEBF /* AppData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppData.swift; sourceTree = "<group>"; };
107 124
 		4383B464240EB6B200DAAEBF /* UserDefault.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefault.swift; sourceTree = "<group>"; };
108
-		7396C8BB36F4E7F8E0CD8FF8 /* MeterNameStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeterNameStore.swift; sourceTree = "<group>"; };
109 125
 		4383B467240F845500DAAEBF /* MacAdress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacAdress.swift; sourceTree = "<group>"; };
110 126
 		4383B469240FE4A600DAAEBF /* MeterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeterView.swift; sourceTree = "<group>"; };
127
+		D28F11023C8E4A7A00A10012 /* MeterConnectionTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeterConnectionTabView.swift; sourceTree = "<group>"; };
128
+		D28F11043C8E4A7A00A10014 /* MeterLiveTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeterLiveTabView.swift; sourceTree = "<group>"; };
129
+		D28F11063C8E4A7A00A10016 /* MeterChartTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeterChartTabView.swift; sourceTree = "<group>"; };
130
+		D28F11083C8E4A7A00A10018 /* MeterSettingsTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeterSettingsTabView.swift; sourceTree = "<group>"; };
131
+		D28F11123C8E4A7A00A10022 /* MeterInfoCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeterInfoCard.swift; sourceTree = "<group>"; };
132
+		D28F11143C8E4A7A00A10024 /* MeterInfoRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeterInfoRow.swift; sourceTree = "<group>"; };
133
+		D28F11163C8E4A7A00A10026 /* EditNameView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditNameView.swift; sourceTree = "<group>"; };
134
+		D28F11183C8E4A7A00A10028 /* EditScreenTimeoutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditScreenTimeoutView.swift; sourceTree = "<group>"; };
135
+		D28F111A3C8E4A7A00A1002A /* EditScreenBrightnessView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditScreenBrightnessView.swift; sourceTree = "<group>"; };
136
+		D28F11223C8E4A7A00A10032 /* LiveMetricRange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveMetricRange.swift; sourceTree = "<group>"; };
137
+		D28F11243C8E4A7A00A10034 /* LoadResistanceSymbolView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadResistanceSymbolView.swift; sourceTree = "<group>"; };
138
+		D28F11323C8E4A7A00A10042 /* ControlActionButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlActionButtonView.swift; sourceTree = "<group>"; };
139
+		D28F11343C8E4A7A00A10044 /* ControlCurrentScreenCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlCurrentScreenCardView.swift; sourceTree = "<group>"; };
140
+		D28F11363C8E4A7A00A10046 /* RecordingMetricsTableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordingMetricsTableView.swift; sourceTree = "<group>"; };
141
+		D28F113A3C8E4A7A00A1004A /* ConnectionStatusBadgeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionStatusBadgeView.swift; sourceTree = "<group>"; };
142
+		D28F113C3C8E4A7A00A1004C /* ConnectionPrimaryActionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionPrimaryActionView.swift; sourceTree = "<group>"; };
143
+		D28F113E3C8E4A7A00A1004E /* ConnectionHomeInfoPreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionHomeInfoPreviewView.swift; sourceTree = "<group>"; };
111 144
 		438695882463F062008855A9 /* Measurements.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Measurements.swift; sourceTree = "<group>"; };
112 145
 		4386958A2F6A1001008855A9 /* UMProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UMProtocol.swift; sourceTree = "<group>"; };
113 146
 		4386958C2F6A1002008855A9 /* TC66Protocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TC66Protocol.swift; sourceTree = "<group>"; };
@@ -128,11 +161,18 @@
128 161
 		43CBF676240C043E00255B8B /* BluetoothManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothManager.swift; sourceTree = "<group>"; };
129 162
 		43CBF67A240C0D8A00255B8B /* USB Meter.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "USB Meter.entitlements"; sourceTree = "<group>"; };
130 163
 		43CBF680240D153000255B8B /* CBManagerState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CBManagerState.swift; sourceTree = "<group>"; };
131
-		43DFBE3F2441A37B004A47EA /* BorderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BorderView.swift; sourceTree = "<group>"; };
132 164
 		43ED78AD2420A0BE00974487 /* BluetoothSerial.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothSerial.swift; sourceTree = "<group>"; };
133 165
 		43F7792A2465AE1600745DF4 /* UIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIView.swift; sourceTree = "<group>"; };
166
+		56BA4CE53B6B2C4EBA42C81A /* MeterMappingDebugView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeterMappingDebugView.swift; sourceTree = "<group>"; };
167
+		7396C8BB36F4E7F8E0CD8FF8 /* MeterNameStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeterNameStore.swift; sourceTree = "<group>"; };
168
+		AAD5F9A32B1CAC0700F8E4F9 /* MeterDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeterDetailView.swift; sourceTree = "<group>"; };
169
+		AAD5F9B22B1CAC7A00F8E4F9 /* SidebarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarView.swift; sourceTree = "<group>"; };
134 170
 /* End PBXFileReference section */
135 171
 
172
+/* Begin PBXFileSystemSynchronizedRootGroup section */
173
+		43BE08E12F78F49500250EEC /* SidebarList */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = SidebarList; sourceTree = "<group>"; };
174
+/* End PBXFileSystemSynchronizedRootGroup section */
175
+
136 176
 /* Begin PBXFrameworksBuildPhase section */
137 177
 		43CBF659240BF3EB00255B8B /* Frameworks */ = {
138 178
 			isa = PBXFrameworksBuildPhase;
@@ -273,16 +313,160 @@
273 313
 			path = Measurements;
274 314
 			sourceTree = "<group>";
275 315
 		};
276
-		437D47CF2415F8CF00B7768E /* Meter */ = {
316
+		D28F11253C8E4A7A00A10035 /* Components */ = {
317
+			isa = PBXGroup;
318
+			children = (
319
+				D28F11223C8E4A7A00A10032 /* LiveMetricRange.swift */,
320
+				D28F11243C8E4A7A00A10034 /* LoadResistanceSymbolView.swift */,
321
+			);
322
+			path = Components;
323
+			sourceTree = "<group>";
324
+		};
325
+		D28F11263C8E4A7A00A10036 /* Live */ = {
277 326
 			isa = PBXGroup;
278 327
 			children = (
279
-				4383B469240FE4A600DAAEBF /* MeterView.swift */,
280 328
 				437D47D02415F91B00B7768E /* LiveView.swift */,
329
+				D28F11253C8E4A7A00A10035 /* Components */,
330
+			);
331
+			path = Live;
332
+			sourceTree = "<group>";
333
+		};
334
+		D28F11373C8E4A7A00A10047 /* Components */ = {
335
+			isa = PBXGroup;
336
+			children = (
337
+				D28F11323C8E4A7A00A10042 /* ControlActionButtonView.swift */,
338
+				D28F11343C8E4A7A00A10044 /* ControlCurrentScreenCardView.swift */,
339
+			);
340
+			path = Components;
341
+			sourceTree = "<group>";
342
+		};
343
+		D28F11383C8E4A7A00A10048 /* Components */ = {
344
+			isa = PBXGroup;
345
+			children = (
346
+				D28F11363C8E4A7A00A10046 /* RecordingMetricsTableView.swift */,
347
+			);
348
+			path = Components;
349
+			sourceTree = "<group>";
350
+		};
351
+		D28F11273C8E4A7A00A10037 /* Recording */ = {
352
+			isa = PBXGroup;
353
+			children = (
281 354
 				437D47D42415FD8C00B7768E /* RecordingView.swift */,
355
+				D28F11383C8E4A7A00A10048 /* Components */,
356
+			);
357
+			path = Recording;
358
+			sourceTree = "<group>";
359
+		};
360
+		D28F11283C8E4A7A00A10038 /* Control */ = {
361
+			isa = PBXGroup;
362
+			children = (
282 363
 				437D47D62415FDF300B7768E /* ControlView.swift */,
283
-				4308CF89241777130002E80B /* Data Groups */,
364
+				D28F11373C8E4A7A00A10047 /* Components */,
365
+			);
366
+			path = Control;
367
+			sourceTree = "<group>";
368
+		};
369
+		D28F10013C8E4A7A00A10001 /* Screens */ = {
370
+			isa = PBXGroup;
371
+			children = (
372
+				D28F11263C8E4A7A00A10036 /* Live */,
373
+				D28F11273C8E4A7A00A10037 /* Recording */,
374
+				D28F11283C8E4A7A00A10038 /* Control */,
375
+			);
376
+			path = Screens;
377
+			sourceTree = "<group>";
378
+		};
379
+		D28F111B3C8E4A7A00A1002B /* Components */ = {
380
+			isa = PBXGroup;
381
+			children = (
382
+				D28F11123C8E4A7A00A10022 /* MeterInfoCard.swift */,
383
+				D28F11143C8E4A7A00A10024 /* MeterInfoRow.swift */,
384
+				D28F113A3C8E4A7A00A1004A /* ConnectionStatusBadgeView.swift */,
385
+				D28F113C3C8E4A7A00A1004C /* ConnectionPrimaryActionView.swift */,
386
+				D28F113E3C8E4A7A00A1004E /* ConnectionHomeInfoPreviewView.swift */,
387
+			);
388
+			path = Components;
389
+			sourceTree = "<group>";
390
+		};
391
+		D28F111C3C8E4A7A00A1002C /* Components */ = {
392
+			isa = PBXGroup;
393
+			children = (
394
+				D28F11163C8E4A7A00A10026 /* EditNameView.swift */,
395
+				D28F11183C8E4A7A00A10028 /* EditScreenTimeoutView.swift */,
396
+				D28F111A3C8E4A7A00A1002A /* EditScreenBrightnessView.swift */,
397
+			);
398
+			path = Components;
399
+			sourceTree = "<group>";
400
+		};
401
+		D28F110A3C8E4A7A00A1001A /* Connection */ = {
402
+			isa = PBXGroup;
403
+			children = (
404
+				D28F11023C8E4A7A00A10012 /* MeterConnectionTabView.swift */,
405
+				D28F111B3C8E4A7A00A1002B /* Components */,
406
+			);
407
+			path = Connection;
408
+			sourceTree = "<group>";
409
+		};
410
+		D28F110B3C8E4A7A00A1001B /* Live */ = {
411
+			isa = PBXGroup;
412
+			children = (
413
+				D28F11043C8E4A7A00A10014 /* MeterLiveTabView.swift */,
414
+			);
415
+			path = Live;
416
+			sourceTree = "<group>";
417
+		};
418
+		D28F110C3C8E4A7A00A1001C /* Chart */ = {
419
+			isa = PBXGroup;
420
+			children = (
421
+				D28F11063C8E4A7A00A10016 /* MeterChartTabView.swift */,
422
+			);
423
+			path = Chart;
424
+			sourceTree = "<group>";
425
+		};
426
+		D28F110D3C8E4A7A00A1001D /* Settings */ = {
427
+			isa = PBXGroup;
428
+			children = (
429
+				D28F11083C8E4A7A00A10018 /* MeterSettingsTabView.swift */,
430
+				D28F111C3C8E4A7A00A1002C /* Components */,
431
+			);
432
+			path = Settings;
433
+			sourceTree = "<group>";
434
+		};
435
+		D28F11093C8E4A7A00A10019 /* Tabs */ = {
436
+			isa = PBXGroup;
437
+			children = (
438
+				D28F110A3C8E4A7A00A1001A /* Connection */,
439
+				D28F110B3C8E4A7A00A1001B /* Live */,
440
+				D28F110C3C8E4A7A00A1001C /* Chart */,
441
+				D28F110D3C8E4A7A00A1001D /* Settings */,
442
+			);
443
+			path = Tabs;
444
+			sourceTree = "<group>";
445
+		};
446
+		D28F10033C8E4A7A00A10003 /* Generic */ = {
447
+			isa = PBXGroup;
448
+			children = (
284 449
 				4360A34C241CBB3800B464F9 /* RSSIView.swift */,
285 450
 				430CB4FB245E07EB006525C2 /* ChevronView.swift */,
451
+			);
452
+			path = Generic;
453
+			sourceTree = "<group>";
454
+		};
455
+		D28F10023C8E4A7A00A10002 /* Components */ = {
456
+			isa = PBXGroup;
457
+			children = (
458
+				D28F10033C8E4A7A00A10003 /* Generic */,
459
+			);
460
+			path = Components;
461
+			sourceTree = "<group>";
462
+		};
463
+		437D47CF2415F8CF00B7768E /* Meter */ = {
464
+			isa = PBXGroup;
465
+			children = (
466
+				4383B469240FE4A600DAAEBF /* MeterView.swift */,
467
+				D28F11093C8E4A7A00A10019 /* Tabs */,
468
+				D28F10013C8E4A7A00A10001 /* Screens */,
469
+				4308CF89241777130002E80B /* Data Groups */,
286 470
 				43554B3024444983004E66F5 /* Measurements */,
287 471
 			);
288 472
 			path = Meter;
@@ -371,11 +555,13 @@
371 555
 			isa = PBXGroup;
372 556
 			children = (
373 557
 				43CBF666240BF3EB00255B8B /* ContentView.swift */,
558
+				AAD5F9BB2B1CC10000F8E4F9 /* Sidebar */,
374 559
 				56BA4CE53B6B2C4EBA42C81A /* MeterMappingDebugView.swift */,
375 560
 				4327461A24619CED0009BE4B /* MeterRowView.swift */,
376 561
 				437D47CF2415F8CF00B7768E /* Meter */,
562
+				D28F10023C8E4A7A00A10002 /* Components */,
377 563
 				4311E639241384960080EA59 /* DeviceHelpView.swift */,
378
-				43DFBE3F2441A37B004A47EA /* BorderView.swift */,
564
+				AAD5F9A32B1CAC0700F8E4F9 /* MeterDetailView.swift */,
379 565
 			);
380 566
 			path = Views;
381 567
 			sourceTree = "<group>";
@@ -396,6 +582,15 @@
396 582
 			path = Extensions;
397 583
 			sourceTree = "<group>";
398 584
 		};
585
+		AAD5F9BB2B1CC10000F8E4F9 /* Sidebar */ = {
586
+			isa = PBXGroup;
587
+			children = (
588
+				AAD5F9B22B1CAC7A00F8E4F9 /* SidebarView.swift */,
589
+				43BE08E12F78F49500250EEC /* SidebarList */,
590
+			);
591
+			path = Sidebar;
592
+			sourceTree = "<group>";
593
+		};
399 594
 /* End PBXGroup section */
400 595
 
401 596
 /* Begin PBXNativeTarget section */
@@ -411,6 +606,9 @@
411 606
 			);
412 607
 			dependencies = (
413 608
 			);
609
+			fileSystemSynchronizedGroups = (
610
+				43BE08E12F78F49500250EEC /* SidebarList */,
611
+			);
414 612
 			name = "USB Meter";
415 613
 			packageProductDependencies = (
416 614
 				4347F01C28D717C1007EE7B1 /* CryptoSwift */,
@@ -485,17 +683,33 @@
485 683
 				4383B468240F845500DAAEBF /* MacAdress.swift in Sources */,
486 684
 				43CBF681240D153000255B8B /* CBManagerState.swift in Sources */,
487 685
 				4383B46A240FE4A600DAAEBF /* MeterView.swift in Sources */,
686
+				D28F11013C8E4A7A00A10011 /* MeterConnectionTabView.swift in Sources */,
687
+				D28F11033C8E4A7A00A10013 /* MeterLiveTabView.swift in Sources */,
688
+				D28F11053C8E4A7A00A10015 /* MeterChartTabView.swift in Sources */,
689
+				D28F11073C8E4A7A00A10017 /* MeterSettingsTabView.swift in Sources */,
690
+				D28F11113C8E4A7A00A10021 /* MeterInfoCard.swift in Sources */,
691
+				D28F11133C8E4A7A00A10023 /* MeterInfoRow.swift in Sources */,
692
+				D28F11153C8E4A7A00A10025 /* EditNameView.swift in Sources */,
693
+				D28F11173C8E4A7A00A10027 /* EditScreenTimeoutView.swift in Sources */,
694
+				D28F11193C8E4A7A00A10029 /* EditScreenBrightnessView.swift in Sources */,
695
+				D28F11213C8E4A7A00A10031 /* LiveMetricRange.swift in Sources */,
696
+				D28F11233C8E4A7A00A10033 /* LoadResistanceSymbolView.swift in Sources */,
697
+				D28F11313C8E4A7A00A10041 /* ControlActionButtonView.swift in Sources */,
698
+				D28F11333C8E4A7A00A10043 /* ControlCurrentScreenCardView.swift in Sources */,
699
+				D28F11353C8E4A7A00A10045 /* RecordingMetricsTableView.swift in Sources */,
700
+				D28F11393C8E4A7A00A10049 /* ConnectionStatusBadgeView.swift in Sources */,
701
+				D28F113B3C8E4A7A00A1004B /* ConnectionPrimaryActionView.swift in Sources */,
702
+				D28F113D3C8E4A7A00A1004D /* ConnectionHomeInfoPreviewView.swift in Sources */,
488 703
 				4360A34D241CBB3800B464F9 /* RSSIView.swift in Sources */,
489 704
 				437D47D12415F91B00B7768E /* LiveView.swift in Sources */,
490
-				4360A34F241D5CF100B464F9 /* MeterSettingsView.swift in Sources */,
491 705
 				4383B465240EB6B200DAAEBF /* UserDefault.swift in Sources */,
492 706
 				3407A133FADB8858DC2A1FED /* MeterNameStore.swift in Sources */,
493 707
 				43CBF677240C043E00255B8B /* BluetoothManager.swift in Sources */,
494 708
 				43CBF660240BF3EB00255B8B /* AppDelegate.swift in Sources */,
495 709
 				438B9555246D2D7500E61AE7 /* Path.swift in Sources */,
496 710
 				4383B460240EB2D000DAAEBF /* Meter.swift in Sources */,
711
+				AAD5F9B12B1CAC7A00F8E4F9 /* SidebarView.swift in Sources */,
497 712
 				43CBF667240BF3EB00255B8B /* ContentView.swift in Sources */,
498
-				43DFBE402441A37B004A47EA /* BorderView.swift in Sources */,
499 713
 				437F0AB72463108F005DEBEC /* MeasurementChartView.swift in Sources */,
500 714
 				437D47D32415FB7E00B7768E /* Decimal.swift in Sources */,
501 715
 				43874C7F2414F3F400525397 /* Float.swift in Sources */,
@@ -515,6 +729,7 @@
515 729
 				430CB4FC245E07EB006525C2 /* ChevronView.swift in Sources */,
516 730
 				43554B3424444B0E004E66F5 /* Date.swift in Sources */,
517 731
 				4311E63A241384960080EA59 /* DeviceHelpView.swift in Sources */,
732
+				AAD5F9A72B1CAC0700F8E4F9 /* MeterDetailView.swift in Sources */,
518 733
 				E430FB6B7CB3E0D4189F6D7D /* MeterMappingDebugView.swift in Sources */,
519 734
 				439D996524234B98008DE3AA /* BluetoothRadio.swift in Sources */,
520 735
 				438695892463F062008855A9 /* Measurements.swift in Sources */,
@@ -748,7 +963,6 @@
748 963
 			productName = CryptoSwift;
749 964
 		};
750 965
 /* End XCSwiftPackageProductDependency section */
751
-
752 966
 	};
753 967
 	rootObject = 43CBF654240BF3EB00255B8B /* Project object */;
754 968
 }
+21 -6
USB Meter/Model/AppData.swift
@@ -11,7 +11,7 @@ import Combine
11 11
 import CoreBluetooth
12 12
 
13 13
 final class AppData : ObservableObject {
14
-    struct KnownMeterSummary: Identifiable {
14
+    struct MeterSummary: Identifiable {
15 15
         let macAddress: String
16 16
         let displayName: String
17 17
         let modelSummary: String
@@ -73,8 +73,8 @@ final class AppData : ObservableObject {
73 73
         meterStore.upsert(macAddress: macAddress, name: nil, temperatureUnitRawValue: preference.rawValue)
74 74
     }
75 75
 
76
-    func registerKnownMeter(macAddress: String, modelName: String?, advertisedName: String?) {
77
-        meterStore.registerKnownMeter(macAddress: macAddress, modelName: modelName, advertisedName: advertisedName)
76
+    func registerMeter(macAddress: String, modelName: String?, advertisedName: String?) {
77
+        meterStore.registerMeter(macAddress: macAddress, modelName: modelName, advertisedName: advertisedName)
78 78
     }
79 79
 
80 80
     func noteMeterSeen(at date: Date, macAddress: String) {
@@ -93,7 +93,7 @@ final class AppData : ObservableObject {
93 93
         meterStore.lastConnected(for: macAddress)
94 94
     }
95 95
 
96
-    var knownMeters: [KnownMeterSummary] {
96
+    var meterSummaries: [MeterSummary] {
97 97
         let liveMetersByMAC = Dictionary(uniqueKeysWithValues: meters.values.map { ($0.btSerial.macAddress.description, $0) })
98 98
         let recordsByMAC = Dictionary(uniqueKeysWithValues: meterStore.allRecords().map { ($0.macAddress, $0) })
99 99
         let macAddresses = Set(recordsByMAC.keys).union(liveMetersByMAC.keys)
@@ -102,10 +102,10 @@ final class AppData : ObservableObject {
102 102
             let liveMeter = liveMetersByMAC[macAddress]
103 103
             let record = recordsByMAC[macAddress]
104 104
 
105
-            return KnownMeterSummary(
105
+            return MeterSummary(
106 106
                 macAddress: macAddress,
107 107
                 displayName: liveMeter?.name ?? record?.customName ?? macAddress,
108
-                modelSummary: liveMeter?.deviceModelSummary ?? record?.modelName ?? "Known meter",
108
+                modelSummary: liveMeter?.deviceModelSummary ?? record?.modelName ?? "Meter",
109 109
                 advertisedName: liveMeter?.modelString ?? record?.advertisedName,
110 110
                 lastSeen: liveMeter?.lastSeen ?? record?.lastSeen,
111 111
                 lastConnected: liveMeter?.lastConnectedAt ?? record?.lastConnected,
@@ -152,3 +152,18 @@ final class AppData : ObservableObject {
152 152
         }
153 153
     }
154 154
 }
155
+
156
+extension AppData.MeterSummary {
157
+    var tint: Color {
158
+        switch modelSummary {
159
+        case "UM25C":
160
+            return .blue
161
+        case "UM34C":
162
+            return .yellow
163
+        case "TC66C":
164
+            return Model.TC66C.color
165
+        default:
166
+            return .secondary
167
+        }
168
+    }
169
+}
+2 -523
USB Meter/Views/ContentView.swift
@@ -9,533 +9,12 @@
9 9
 //MARK: Bluetooth Icon: https://upload.wikimedia.org/wikipedia/commons/d/da/Bluetooth.svg
10 10
 
11 11
 import SwiftUI
12
-import Combine
13 12
 
14 13
 struct ContentView: View {
15
-    private enum HelpAutoReason: String {
16
-        case bluetoothPermission
17
-        case cloudSyncUnavailable
18
-        case noDevicesDetected
19
-
20
-        var tint: Color {
21
-            switch self {
22
-            case .bluetoothPermission:
23
-                return .orange
24
-            case .cloudSyncUnavailable:
25
-                return .indigo
26
-            case .noDevicesDetected:
27
-                return .yellow
28
-            }
29
-        }
30
-
31
-        var symbol: String {
32
-            switch self {
33
-            case .bluetoothPermission:
34
-                return "bolt.horizontal.circle.fill"
35
-            case .cloudSyncUnavailable:
36
-                return "icloud.slash.fill"
37
-            case .noDevicesDetected:
38
-                return "magnifyingglass.circle.fill"
39
-            }
40
-        }
41
-
42
-        var badgeTitle: String {
43
-            switch self {
44
-            case .bluetoothPermission:
45
-                return "Required"
46
-            case .cloudSyncUnavailable:
47
-                return "Sync Off"
48
-            case .noDevicesDetected:
49
-                return "Suggested"
50
-            }
51
-        }
52
-    }
53
-
54
-    @EnvironmentObject private var appData: AppData
55
-    @State private var isHelpExpanded = false
56
-    @State private var dismissedAutoHelpReason: HelpAutoReason?
57
-    @State private var now = Date()
58
-    private let helpRefreshTimer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
59
-    private let noDevicesHelpDelay: TimeInterval = 12
60
-
61 14
     var body: some View {
62 15
         NavigationView {
63
-            ScrollView {
64
-                VStack(alignment: .leading, spacing: 18) {
65
-                    headerCard
66
-                    helpSection
67
-                    devicesSection
68
-                    debugSection
69
-                }
70
-                .padding()
71
-            }
72
-            .background(
73
-                LinearGradient(
74
-                    colors: [
75
-                        appData.bluetoothManager.managerState.color.opacity(0.18),
76
-                        Color.clear
77
-                    ],
78
-                    startPoint: .topLeading,
79
-                    endPoint: .bottomTrailing
80
-                )
81
-                .ignoresSafeArea()
82
-            )
83
-            .navigationBarTitle(Text("USB Meters"), displayMode: .inline)
84
-        }
85
-        .onAppear {
86
-            appData.bluetoothManager.start()
87
-            now = Date()
88
-        }
89
-        .onReceive(helpRefreshTimer) { currentDate in
90
-            now = currentDate
91
-        }
92
-        .onChange(of: activeHelpAutoReason) { newReason in
93
-            if newReason == nil {
94
-                dismissedAutoHelpReason = nil
95
-            }
96
-        }
97
-    }
98
-
99
-    private var headerCard: some View {
100
-        VStack(alignment: .leading, spacing: 10) {
101
-            Text("USB Meters")
102
-                .font(.system(.title2, design: .rounded).weight(.bold))
103
-            Text("Browse nearby supported meters and jump into live diagnostics, charge records, and device controls.")
104
-                .font(.footnote)
105
-                .foregroundColor(.secondary)
106
-            HStack {
107
-                Label("Bluetooth", systemImage: "bolt.horizontal.circle.fill")
108
-                    .font(.footnote.weight(.semibold))
109
-                    .foregroundColor(appData.bluetoothManager.managerState.color)
110
-                Spacer()
111
-                Text(bluetoothStatusText)
112
-                    .font(.caption.weight(.semibold))
113
-                    .foregroundColor(.secondary)
114
-            }
115
-        }
116
-        .padding(18)
117
-        .meterCard(tint: appData.bluetoothManager.managerState.color, fillOpacity: 0.22, strokeOpacity: 0.26)
118
-    }
119
-
120
-    private var helpSection: some View {
121
-        VStack(alignment: .leading, spacing: 12) {
122
-            Button(action: toggleHelpSection) {
123
-                HStack(spacing: 14) {
124
-                    Image(systemName: helpSectionSymbol)
125
-                        .font(.system(size: 18, weight: .semibold))
126
-                        .foregroundColor(helpSectionTint)
127
-                        .frame(width: 42, height: 42)
128
-                        .background(Circle().fill(helpSectionTint.opacity(0.18)))
129
-
130
-                    VStack(alignment: .leading, spacing: 4) {
131
-                        Text("Help")
132
-                            .font(.headline)
133
-                        Text(helpSectionSummary)
134
-                            .font(.caption)
135
-                            .foregroundColor(.secondary)
136
-                    }
137
-
138
-                    Spacer()
139
-
140
-                    if let activeHelpAutoReason {
141
-                        Text(activeHelpAutoReason.badgeTitle)
142
-                            .font(.caption2.weight(.bold))
143
-                            .foregroundColor(activeHelpAutoReason.tint)
144
-                            .padding(.horizontal, 10)
145
-                            .padding(.vertical, 6)
146
-                            .background(
147
-                                Capsule(style: .continuous)
148
-                                    .fill(activeHelpAutoReason.tint.opacity(0.12))
149
-                            )
150
-                            .overlay(
151
-                                Capsule(style: .continuous)
152
-                                    .stroke(activeHelpAutoReason.tint.opacity(0.22), lineWidth: 1)
153
-                            )
154
-                    }
155
-
156
-                    Image(systemName: helpIsExpanded ? "chevron.up" : "chevron.down")
157
-                        .font(.footnote.weight(.bold))
158
-                        .foregroundColor(.secondary)
159
-                }
160
-                .padding(14)
161
-                .meterCard(tint: helpSectionTint, fillOpacity: 0.16, strokeOpacity: 0.22, cornerRadius: 18)
162
-            }
163
-            .buttonStyle(.plain)
164
-
165
-            if helpIsExpanded {
166
-                if let activeHelpAutoReason {
167
-                    helpNoticeCard(for: activeHelpAutoReason)
168
-                }
169
-
170
-                if activeHelpAutoReason == .cloudSyncUnavailable {
171
-                    Button(action: openSettings) {
172
-                        sidebarLinkCard(
173
-                            title: "Open Settings",
174
-                            subtitle: "Check Apple ID, iCloud Drive, and any restrictions affecting Cloud sync.",
175
-                            symbol: "gearshape.fill",
176
-                            tint: .indigo
177
-                        )
178
-                    }
179
-                    .buttonStyle(.plain)
180
-                }
181
-
182
-                NavigationLink(destination: appData.bluetoothManager.managerState.helpView) {
183
-                    sidebarLinkCard(
184
-                        title: "Bluetooth",
185
-                        subtitle: "Permissions, adapter state, and connection tips.",
186
-                        symbol: "bolt.horizontal.circle.fill",
187
-                        tint: appData.bluetoothManager.managerState.color
188
-                    )
189
-                }
190
-                .buttonStyle(.plain)
191
-
192
-                NavigationLink(destination: DeviceHelpView()) {
193
-                    sidebarLinkCard(
194
-                        title: "Device",
195
-                        subtitle: "Quick checks when a meter is not responding as expected.",
196
-                        symbol: "questionmark.circle.fill",
197
-                        tint: .orange
198
-                    )
199
-                }
200
-                .buttonStyle(.plain)
201
-            }
202
-        }
203
-        .animation(.easeInOut(duration: 0.22), value: helpIsExpanded)
204
-    }
205
-
206
-    private var devicesSection: some View {
207
-        VStack(alignment: .leading, spacing: 12) {
208
-            HStack {
209
-                Text("Known Meters")
210
-                    .font(.headline)
211
-                Spacer()
212
-                Text("\(appData.knownMeters.count)")
213
-                    .font(.caption.weight(.bold))
214
-                    .padding(.horizontal, 10)
215
-                    .padding(.vertical, 6)
216
-                    .meterCard(tint: .blue, fillOpacity: 0.18, strokeOpacity: 0.24, cornerRadius: 999)
217
-            }
218
-
219
-            if appData.knownMeters.isEmpty {
220
-                Text(devicesEmptyStateText)
221
-                    .font(.footnote)
222
-                    .foregroundColor(.secondary)
223
-                    .frame(maxWidth: .infinity, alignment: .leading)
224
-                    .padding(18)
225
-                    .meterCard(
226
-                        tint: isWaitingForFirstDiscovery ? .blue : .secondary,
227
-                        fillOpacity: 0.14,
228
-                        strokeOpacity: 0.20
229
-                    )
230
-            } else {
231
-                ForEach(appData.knownMeters) { knownMeter in
232
-                    if let meter = knownMeter.meter {
233
-                        NavigationLink(destination: MeterView().environmentObject(meter)) {
234
-                            MeterRowView()
235
-                                .environmentObject(meter)
236
-                        }
237
-                        .buttonStyle(.plain)
238
-                    } else {
239
-                        knownMeterCard(for: knownMeter)
240
-                    }
241
-                }
242
-            }
243
-        }
244
-    }
245
-
246
-    private var debugSection: some View {
247
-        VStack(alignment: .leading, spacing: 12) {
248
-            Text("Debug")
249
-                .font(.headline)
250
-            debugLink
251
-        }
252
-    }
253
-
254
-    private var debugLink: some View {
255
-        NavigationLink(destination: MeterMappingDebugView()) {
256
-            sidebarLinkCard(
257
-                title: "Meter Sync Debug",
258
-                subtitle: "Inspect meter name sync data and iCloud KVS visibility as seen by this device.",
259
-                symbol: "list.bullet.rectangle",
260
-                tint: .purple
261
-            )
262
-        }
263
-        .buttonStyle(.plain)
264
-    }
265
-
266
-    private var bluetoothStatusText: String {
267
-        switch appData.bluetoothManager.managerState {
268
-        case .poweredOff:
269
-            return "Off"
270
-        case .poweredOn:
271
-            return "On"
272
-        case .resetting:
273
-            return "Resetting"
274
-        case .unauthorized:
275
-            return "Unauthorized"
276
-        case .unknown:
277
-            return "Unknown"
278
-        case .unsupported:
279
-            return "Unsupported"
280
-        @unknown default:
281
-            return "Other"
282
-        }
283
-    }
284
-
285
-    private var helpIsExpanded: Bool {
286
-        isHelpExpanded || shouldAutoExpandHelp
287
-    }
288
-
289
-    private var shouldAutoExpandHelp: Bool {
290
-        guard let activeHelpAutoReason else {
291
-            return false
292
-        }
293
-        return dismissedAutoHelpReason != activeHelpAutoReason
294
-    }
295
-
296
-    private var activeHelpAutoReason: HelpAutoReason? {
297
-        if appData.bluetoothManager.managerState == .unauthorized {
298
-            return .bluetoothPermission
299
-        }
300
-        if shouldPromptForCloudSync {
301
-            return .cloudSyncUnavailable
302
-        }
303
-        if hasWaitedLongEnoughForDevices {
304
-            return .noDevicesDetected
305
-        }
306
-        return nil
307
-    }
308
-
309
-    private var shouldPromptForCloudSync: Bool {
310
-        switch appData.cloudAvailability {
311
-        case .noAccount, .error:
312
-            return true
313
-        case .unknown, .available:
314
-            return false
315
-        }
316
-    }
317
-
318
-    private var hasWaitedLongEnoughForDevices: Bool {
319
-        guard appData.bluetoothManager.managerState == .poweredOn else {
320
-            return false
321
-        }
322
-        guard appData.meters.isEmpty else {
323
-            return false
324
-        }
325
-        guard let scanStartedAt = appData.bluetoothManager.scanStartedAt else {
326
-            return false
327
-        }
328
-        return now.timeIntervalSince(scanStartedAt) >= noDevicesHelpDelay
329
-    }
330
-
331
-    private var isWaitingForFirstDiscovery: Bool {
332
-        guard appData.bluetoothManager.managerState == .poweredOn else {
333
-            return false
334
-        }
335
-        guard appData.meters.isEmpty else {
336
-            return false
337
-        }
338
-        guard let scanStartedAt = appData.bluetoothManager.scanStartedAt else {
339
-            return false
340
-        }
341
-        return now.timeIntervalSince(scanStartedAt) < noDevicesHelpDelay
342
-    }
343
-
344
-    private var devicesEmptyStateText: String {
345
-        if isWaitingForFirstDiscovery {
346
-            return "Scanning for nearby supported meters..."
347
-        }
348
-        return "No known meters yet. Nearby supported meters will appear here and remain available after they disappear."
349
-    }
350
-
351
-    private var helpSectionTint: Color {
352
-        activeHelpAutoReason?.tint ?? .secondary
353
-    }
354
-
355
-    private var helpSectionSymbol: String {
356
-        activeHelpAutoReason?.symbol ?? "questionmark.circle.fill"
357
-    }
358
-
359
-    private var helpSectionSummary: String {
360
-        switch activeHelpAutoReason {
361
-        case .bluetoothPermission:
362
-            return "Bluetooth permission is needed before scanning can begin."
363
-        case .cloudSyncUnavailable:
364
-            return appData.cloudAvailability.helpMessage
365
-        case .noDevicesDetected:
366
-            return "No supported devices were found after \(Int(noDevicesHelpDelay)) seconds."
367
-        case nil:
368
-            return "Connection tips and quick checks when discovery needs help."
369
-        }
370
-    }
371
-
372
-    private func toggleHelpSection() {
373
-        withAnimation(.easeInOut(duration: 0.22)) {
374
-            if shouldAutoExpandHelp {
375
-                dismissedAutoHelpReason = activeHelpAutoReason
376
-                isHelpExpanded = false
377
-            } else {
378
-                isHelpExpanded.toggle()
379
-            }
380
-        }
381
-    }
382
-
383
-    private func openSettings() {
384
-        guard let settingsURL = URL(string: UIApplication.openSettingsURLString) else {
385
-            return
386
-        }
387
-        UIApplication.shared.open(settingsURL, options: [:], completionHandler: nil)
388
-    }
389
-
390
-    private func helpNoticeCard(for reason: HelpAutoReason) -> some View {
391
-        VStack(alignment: .leading, spacing: 8) {
392
-            Text(helpNoticeTitle(for: reason))
393
-                .font(.subheadline.weight(.semibold))
394
-            Text(helpNoticeDetail(for: reason))
395
-                .font(.caption)
396
-                .foregroundColor(.secondary)
397
-        }
398
-        .frame(maxWidth: .infinity, alignment: .leading)
399
-        .padding(14)
400
-        .meterCard(tint: reason.tint, fillOpacity: 0.14, strokeOpacity: 0.20, cornerRadius: 18)
401
-    }
402
-
403
-    private func helpNoticeTitle(for reason: HelpAutoReason) -> String {
404
-        switch reason {
405
-        case .bluetoothPermission:
406
-            return "Bluetooth access needs attention"
407
-        case .cloudSyncUnavailable:
408
-            return appData.cloudAvailability.helpTitle
409
-        case .noDevicesDetected:
410
-            return "No supported meters found yet"
411
-        }
412
-    }
413
-
414
-    private func helpNoticeDetail(for reason: HelpAutoReason) -> String {
415
-        switch reason {
416
-        case .bluetoothPermission:
417
-            return "Open Bluetooth help to review the permission state and jump into Settings if access is blocked."
418
-        case .cloudSyncUnavailable:
419
-            return appData.cloudAvailability.helpMessage
420
-        case .noDevicesDetected:
421
-            return "Open Device help for quick checks like meter power, Bluetooth availability on the meter, or an existing connection to another phone."
422
-        }
423
-    }
424
-
425
-    private func sidebarLinkCard(
426
-        title: String,
427
-        subtitle: String,
428
-        symbol: String,
429
-        tint: Color
430
-    ) -> some View {
431
-        HStack(spacing: 14) {
432
-            Image(systemName: symbol)
433
-                .font(.system(size: 18, weight: .semibold))
434
-                .foregroundColor(tint)
435
-                .frame(width: 42, height: 42)
436
-                .background(Circle().fill(tint.opacity(0.18)))
437
-
438
-            VStack(alignment: .leading, spacing: 4) {
439
-                Text(title)
440
-                    .font(.headline)
441
-                Text(subtitle)
442
-                    .font(.caption)
443
-                    .foregroundColor(.secondary)
444
-            }
445
-
446
-            Spacer()
447
-
448
-            Image(systemName: "chevron.right")
449
-                .font(.footnote.weight(.bold))
450
-                .foregroundColor(.secondary)
451
-        }
452
-        .padding(14)
453
-        .meterCard(tint: tint, fillOpacity: 0.16, strokeOpacity: 0.22, cornerRadius: 18)
454
-    }
455
-
456
-    private func knownMeterCard(for knownMeter: AppData.KnownMeterSummary) -> some View {
457
-        HStack(spacing: 14) {
458
-            Image(systemName: "sensor.tag.radiowaves.forward.fill")
459
-                .font(.system(size: 18, weight: .semibold))
460
-                .foregroundColor(knownMeterTint(for: knownMeter))
461
-                .frame(width: 42, height: 42)
462
-                .background(
463
-                    Circle()
464
-                        .fill(knownMeterTint(for: knownMeter).opacity(0.18))
465
-                )
466
-                .overlay(alignment: .bottomTrailing) {
467
-                    Circle()
468
-                        .fill(Color.red)
469
-                        .frame(width: 12, height: 12)
470
-                        .overlay(
471
-                            Circle()
472
-                                .stroke(Color(uiColor: .systemBackground), lineWidth: 2)
473
-                        )
474
-                }
475
-
476
-            VStack(alignment: .leading, spacing: 4) {
477
-                Text(knownMeter.displayName)
478
-                    .font(.headline)
479
-                Text(knownMeter.modelSummary)
480
-                    .font(.caption)
481
-                    .foregroundColor(.secondary)
482
-                if let advertisedName = knownMeter.advertisedName, advertisedName != knownMeter.modelSummary {
483
-                    Text("Advertised as \(advertisedName)")
484
-                        .font(.caption2)
485
-                        .foregroundColor(.secondary)
486
-                }
487
-            }
488
-
489
-            Spacer()
490
-
491
-            VStack(alignment: .trailing, spacing: 4) {
492
-                HStack(spacing: 6) {
493
-                    Circle()
494
-                        .fill(Color.red)
495
-                        .frame(width: 8, height: 8)
496
-                    Text("Missing")
497
-                        .font(.caption.weight(.semibold))
498
-                        .foregroundColor(.secondary)
499
-                }
500
-                .padding(.horizontal, 10)
501
-                .padding(.vertical, 6)
502
-                .background(
503
-                    Capsule(style: .continuous)
504
-                        .fill(Color.red.opacity(0.12))
505
-                )
506
-                .overlay(
507
-                    Capsule(style: .continuous)
508
-                        .stroke(Color.red.opacity(0.22), lineWidth: 1)
509
-                )
510
-                Text(knownMeter.macAddress)
511
-                    .font(.caption2)
512
-                    .foregroundColor(.secondary)
513
-                if let lastSeen = knownMeter.lastSeen {
514
-                    Text("Seen \(lastSeen.format(as: "yyyy-MM-dd HH:mm"))")
515
-                        .font(.caption2)
516
-                        .foregroundColor(.secondary)
517
-                }
518
-            }
519
-        }
520
-        .padding(14)
521
-        .meterCard(
522
-            tint: knownMeterTint(for: knownMeter),
523
-            fillOpacity: 0.16,
524
-            strokeOpacity: 0.22,
525
-            cornerRadius: 18
526
-        )
527
-    }
528
-
529
-    private func knownMeterTint(for knownMeter: AppData.KnownMeterSummary) -> Color {
530
-        switch knownMeter.modelSummary {
531
-        case "UM25C":
532
-            return .blue
533
-        case "UM34C":
534
-            return .yellow
535
-        case "TC66C":
536
-            return Model.TC66C.color
537
-        default:
538
-            return .secondary
16
+            SidebarView()
17
+                .navigationBarTitle(Text("USB Meters"), displayMode: .inline)
539 18
         }
540 19
     }
541 20
 }
+113 -0
USB Meter/Views/MeterDetailView.swift
@@ -0,0 +1,113 @@
1
+import SwiftUI
2
+
3
+struct MeterDetailView: View {
4
+    let knownMeter: AppData.KnownMeterSummary
5
+
6
+    var body: some View {
7
+        ScrollView {
8
+            VStack(spacing: 18) {
9
+                headerCard
10
+                statusCard
11
+                historyCard
12
+            }
13
+            .padding()
14
+        }
15
+        .background(
16
+            LinearGradient(
17
+                colors: [knownMeter.tint.opacity(0.18), Color.clear],
18
+                startPoint: .topLeading,
19
+                endPoint: .bottomTrailing
20
+            )
21
+            .ignoresSafeArea()
22
+        )
23
+        .navigationTitle(knownMeter.displayName)
24
+    }
25
+
26
+    private var headerCard: some View {
27
+        VStack(alignment: .leading, spacing: 8) {
28
+            Text(knownMeter.displayName)
29
+                .font(.title2.weight(.semibold))
30
+            Text(knownMeter.modelSummary)
31
+                .font(.subheadline)
32
+                .foregroundColor(.secondary)
33
+            if let advertisedName = knownMeter.advertisedName {
34
+                Text("Advertised as " + advertisedName)
35
+                    .font(.caption2)
36
+                    .foregroundColor(.secondary)
37
+            }
38
+        }
39
+        .frame(maxWidth: .infinity, alignment: .leading)
40
+        .padding(18)
41
+        .meterCard(tint: knownMeter.tint, fillOpacity: 0.22, strokeOpacity: 0.28, cornerRadius: 20)
42
+    }
43
+
44
+    private var statusCard: some View {
45
+        VStack(alignment: .leading, spacing: 10) {
46
+            Text("Status")
47
+                .font(.headline)
48
+            HStack(spacing: 8) {
49
+                Circle()
50
+                    .fill(knownMeter.tint)
51
+                    .frame(width: 10, height: 10)
52
+                Text("Offline")
53
+                    .font(.caption.weight(.semibold))
54
+                    .foregroundColor(.secondary)
55
+            }
56
+            Text("The meter is not currently connected. Bring it within Bluetooth range or wake it up to open live diagnostics.")
57
+                .font(.caption)
58
+                .foregroundColor(.secondary)
59
+        }
60
+        .frame(maxWidth: .infinity, alignment: .leading)
61
+        .padding(18)
62
+        .meterCard(tint: knownMeter.tint, fillOpacity: 0.16, strokeOpacity: 0.22, cornerRadius: 18)
63
+    }
64
+
65
+    private var historyCard: some View {
66
+        VStack(alignment: .leading, spacing: 10) {
67
+            Text("History")
68
+                .font(.headline)
69
+            infoRow(label: "Last Seen", value: lastSeenText)
70
+            Divider()
71
+            infoRow(label: "Last Connected", value: lastConnectedText)
72
+        }
73
+        .frame(maxWidth: .infinity, alignment: .leading)
74
+        .padding(18)
75
+        .meterCard(tint: .secondary, fillOpacity: 0.12, strokeOpacity: 0.18, cornerRadius: 18)
76
+    }
77
+
78
+    private func infoRow(label: String, value: String) -> some View {
79
+        HStack {
80
+            Text(label)
81
+            Spacer()
82
+            Text(value)
83
+                .foregroundColor(.secondary)
84
+                .font(.caption)
85
+        }
86
+    }
87
+
88
+    private var lastSeenText: String {
89
+        guard let date = knownMeter.lastSeen else { return "—" }
90
+        return date.format(as: "yyyy-MM-dd HH:mm")
91
+    }
92
+
93
+    private var lastConnectedText: String {
94
+        guard let date = knownMeter.lastConnected else { return "—" }
95
+        return date.format(as: "yyyy-MM-dd HH:mm")
96
+    }
97
+}
98
+
99
+struct MeterDetailView_Previews: PreviewProvider {
100
+    static var previews: some View {
101
+        MeterDetailView(
102
+            knownMeter: AppData.KnownMeterSummary(
103
+                macAddress: "AA:BB:CC:DD:EE:FF",
104
+                displayName: "Desk Meter",
105
+                modelSummary: "UM25C",
106
+                advertisedName: "UM25C-123",
107
+                lastSeen: Date(),
108
+                lastConnected: Date().addingTimeInterval(-3600),
109
+                meter: nil
110
+            )
111
+        )
112
+    }
113
+}
+0 -3
USB Meter/Views/MeterRowView.swift
@@ -61,9 +61,6 @@ struct MeterRowView: View {
61 61
                     Capsule(style: .continuous)
62 62
                         .stroke(connectivityTint.opacity(0.22), lineWidth: 1)
63 63
                 )
64
-                Text(meter.btSerial.macAddress.description)
65
-                    .font(.caption2)
66
-                    .foregroundColor(.secondary)
67 64
             }
68 65
         }
69 66
         .padding(14)