Showing 9 changed files with 128 additions and 98 deletions
+28 -1
USB Meter/Model/AppData.swift
@@ -104,7 +104,7 @@ final class AppData : ObservableObject {
104 104
 
105 105
             return MeterSummary(
106 106
                 macAddress: macAddress,
107
-                displayName: liveMeter?.name ?? record?.customName ?? macAddress,
107
+                displayName: Self.friendlyDisplayName(liveMeter: liveMeter, record: record),
108 108
                 modelSummary: liveMeter?.deviceModelSummary ?? record?.modelName ?? "Meter",
109 109
                 advertisedName: liveMeter?.modelString ?? record?.advertisedName,
110 110
                 lastSeen: liveMeter?.lastSeen ?? record?.lastSeen,
@@ -113,6 +113,12 @@ final class AppData : ObservableObject {
113 113
             )
114 114
         }
115 115
         .sorted { lhs, rhs in
116
+            if lhs.meter != nil && rhs.meter == nil {
117
+                return true
118
+            }
119
+            if lhs.meter == nil && rhs.meter != nil {
120
+                return false
121
+            }
116 122
             let byName = lhs.displayName.localizedCaseInsensitiveCompare(rhs.displayName)
117 123
             if byName != .orderedSame {
118 124
                 return byName == .orderedAscending
@@ -167,3 +173,24 @@ extension AppData.MeterSummary {
167 173
         }
168 174
     }
169 175
 }
176
+
177
+private extension AppData {
178
+    static func friendlyDisplayName(liveMeter: Meter?, record: MeterNameStore.Record?) -> String {
179
+        if let liveName = liveMeter?.name.trimmingCharacters(in: .whitespacesAndNewlines), !liveName.isEmpty {
180
+            return liveName
181
+        }
182
+        if let customName = record?.customName {
183
+            return customName
184
+        }
185
+        if let advertisedName = record?.advertisedName {
186
+            return advertisedName
187
+        }
188
+        if let recordModel = record?.modelName {
189
+            return recordModel
190
+        }
191
+        if let liveModel = liveMeter?.deviceModelSummary {
192
+            return liveModel
193
+        }
194
+        return "Meter"
195
+    }
196
+}
+4 -21
USB Meter/Views/Meter/Tabs/Connection/Components/ConnectionHomeInfoPreviewView.swift
@@ -18,31 +18,14 @@ struct ConnectionHomeInfoPreviewView: View {
18 18
                 MeterInfoRow(label: "Name", value: meter.name)
19 19
                 MeterInfoRow(label: "Device Model", value: meter.deviceModelName)
20 20
                 MeterInfoRow(label: "Advertised Model", value: meter.modelString)
21
-                MeterInfoRow(label: "Working Voltage", value: meter.documentedWorkingVoltage)
22
-                MeterInfoRow(label: "Temperature Unit", value: meter.temperatureUnitDescription)
23
-                MeterInfoRow(label: "Last Seen", value: meterHistoryText(for: meter.lastSeen))
24
-                MeterInfoRow(label: "Last Connected", value: meterHistoryText(for: meter.lastConnectedAt))
25
-            }
26
-
27
-            MeterInfoCard(title: "Identifiers", tint: .blue) {
28 21
                 MeterInfoRow(label: "MAC", value: meter.btSerial.macAddress.description)
29 22
                 if meter.modelNumber != 0 {
30 23
                     MeterInfoRow(label: "Model Identifier", value: "\(meter.modelNumber)")
31 24
                 }
32
-            }
33
-
34
-            MeterInfoCard(title: "Screen Reporting", tint: .orange) {
35
-                if meter.reportsCurrentScreenIndex {
36
-                    MeterInfoRow(label: "Current Screen", value: meter.currentScreenDescription)
37
-                    Text("The active screen index is reported by the meter and mapped by the app to a known label.")
38
-                        .font(.footnote)
39
-                        .foregroundColor(.secondary)
40
-                } else {
41
-                    MeterInfoRow(label: "Current Screen", value: "Not Reported")
42
-                    Text("The current screen is not reported by the device payload, or we have not yet identified where and how the protocol announces it.")
43
-                        .font(.footnote)
44
-                        .foregroundColor(.secondary)
45
-                }
25
+                MeterInfoRow(label: "Working Voltage", value: meter.documentedWorkingVoltage)
26
+                MeterInfoRow(label: "Temperature Unit", value: meter.temperatureUnitDescription)
27
+                MeterInfoRow(label: "Last Seen", value: meterHistoryText(for: meter.lastSeen))
28
+                MeterInfoRow(label: "Last Connected", value: meterHistoryText(for: meter.lastConnectedAt))
46 29
             }
47 30
 
48 31
             MeterInfoCard(title: "Live Device Details", tint: .indigo) {
+2 -13
USB Meter/Views/Meter/Tabs/Connection/MeterConnectionTabView.swift
@@ -76,10 +76,6 @@ struct MeterConnectionTabView: View {
76 76
                 statusBadge
77 77
             }
78 78
 
79
-            if compact {
80
-                Spacer(minLength: 0)
81
-            }
82
-
83 79
             connectionActionArea(compact: compact)
84 80
 
85 81
             if showsActions {
@@ -95,15 +91,8 @@ struct MeterConnectionTabView: View {
95 91
         .padding(compact ? 16 : 20)
96 92
         .meterCard(tint: meter.color, fillOpacity: 0.22, strokeOpacity: 0.24)
97 93
 
98
-        return Group {
99
-            if compact {
100
-                cardContent
101
-                    .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
102
-            } else {
103
-                cardContent
104
-                    .frame(maxWidth: .infinity, alignment: .topLeading)
105
-            }
106
-        }
94
+        return cardContent
95
+            .frame(maxWidth: .infinity, alignment: .topLeading)
107 96
     }
108 97
 
109 98
     private func meterIdentity(compact: Bool) -> some View {
+9 -0
USB Meter/Views/Meter/Tabs/Settings/MeterSettingsTabView.swift
@@ -50,6 +50,15 @@ struct MeterSettingsTabView: View {
50 50
                         }
51 51
                     }
52 52
 
53
+                    if meter.operationalState == .dataIsAvailable && meter.model == .TC66C {
54
+                        settingsCard(title: "Screen Reporting", tint: .orange) {
55
+                            MeterInfoRow(label: "Current Screen", value: "Not Reported")
56
+                            Text("TC66 is the exception: it does not report the current screen in the payload, so the app keeps this note here instead of showing it on the home screen.")
57
+                                .font(.footnote)
58
+                                .foregroundColor(.secondary)
59
+                        }
60
+                    }
61
+
53 62
                     if meter.operationalState == .dataIsAvailable {
54 63
                         settingsCard(
55 64
                             title: meter.reportsCurrentScreenIndex ? "Screen Controls" : "Page Controls",
+17 -26
USB Meter/Views/MeterDetailView.swift
@@ -1,36 +1,36 @@
1 1
 import SwiftUI
2 2
 
3 3
 struct MeterDetailView: View {
4
-    let knownMeter: AppData.KnownMeterSummary
4
+    let meterSummary: AppData.MeterSummary
5 5
 
6 6
     var body: some View {
7 7
         ScrollView {
8 8
             VStack(spacing: 18) {
9 9
                 headerCard
10 10
                 statusCard
11
-                historyCard
11
+                identifiersCard
12 12
             }
13 13
             .padding()
14 14
         }
15 15
         .background(
16 16
             LinearGradient(
17
-                colors: [knownMeter.tint.opacity(0.18), Color.clear],
17
+                colors: [meterSummary.tint.opacity(0.18), Color.clear],
18 18
                 startPoint: .topLeading,
19 19
                 endPoint: .bottomTrailing
20 20
             )
21 21
             .ignoresSafeArea()
22 22
         )
23
-        .navigationTitle(knownMeter.displayName)
23
+        .navigationTitle(meterSummary.displayName)
24 24
     }
25 25
 
26 26
     private var headerCard: some View {
27 27
         VStack(alignment: .leading, spacing: 8) {
28
-            Text(knownMeter.displayName)
28
+            Text(meterSummary.displayName)
29 29
                 .font(.title2.weight(.semibold))
30
-            Text(knownMeter.modelSummary)
30
+            Text(meterSummary.modelSummary)
31 31
                 .font(.subheadline)
32 32
                 .foregroundColor(.secondary)
33
-            if let advertisedName = knownMeter.advertisedName {
33
+            if let advertisedName = meterSummary.advertisedName {
34 34
                 Text("Advertised as " + advertisedName)
35 35
                     .font(.caption2)
36 36
                     .foregroundColor(.secondary)
@@ -38,7 +38,7 @@ struct MeterDetailView: View {
38 38
         }
39 39
         .frame(maxWidth: .infinity, alignment: .leading)
40 40
         .padding(18)
41
-        .meterCard(tint: knownMeter.tint, fillOpacity: 0.22, strokeOpacity: 0.28, cornerRadius: 20)
41
+        .meterCard(tint: meterSummary.tint, fillOpacity: 0.22, strokeOpacity: 0.28, cornerRadius: 20)
42 42
     }
43 43
 
44 44
     private var statusCard: some View {
@@ -47,7 +47,7 @@ struct MeterDetailView: View {
47 47
                 .font(.headline)
48 48
             HStack(spacing: 8) {
49 49
                 Circle()
50
-                    .fill(knownMeter.tint)
50
+                    .fill(meterSummary.tint)
51 51
                     .frame(width: 10, height: 10)
52 52
                 Text("Offline")
53 53
                     .font(.caption.weight(.semibold))
@@ -59,16 +59,17 @@ struct MeterDetailView: View {
59 59
         }
60 60
         .frame(maxWidth: .infinity, alignment: .leading)
61 61
         .padding(18)
62
-        .meterCard(tint: knownMeter.tint, fillOpacity: 0.16, strokeOpacity: 0.22, cornerRadius: 18)
62
+        .meterCard(tint: meterSummary.tint, fillOpacity: 0.16, strokeOpacity: 0.22, cornerRadius: 18)
63 63
     }
64 64
 
65
-    private var historyCard: some View {
65
+    private var identifiersCard: some View {
66 66
         VStack(alignment: .leading, spacing: 10) {
67
-            Text("History")
67
+            Text("Identifiers")
68 68
                 .font(.headline)
69
-            infoRow(label: "Last Seen", value: lastSeenText)
70
-            Divider()
71
-            infoRow(label: "Last Connected", value: lastConnectedText)
69
+            infoRow(label: "MAC Address", value: meterSummary.macAddress)
70
+            if let advertisedName = meterSummary.advertisedName {
71
+                infoRow(label: "Advertised as", value: advertisedName)
72
+            }
72 73
         }
73 74
         .frame(maxWidth: .infinity, alignment: .leading)
74 75
         .padding(18)
@@ -84,22 +85,12 @@ struct MeterDetailView: View {
84 85
                 .font(.caption)
85 86
         }
86 87
     }
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 88
 }
98 89
 
99 90
 struct MeterDetailView_Previews: PreviewProvider {
100 91
     static var previews: some View {
101 92
         MeterDetailView(
102
-            knownMeter: AppData.KnownMeterSummary(
93
+            meterSummary: AppData.MeterSummary(
103 94
                 macAddress: "AA:BB:CC:DD:EE:FF",
104 95
                 displayName: "Desk Meter",
105 96
                 modelSummary: "UM25C",
+8 -6
USB Meter/Views/Sidebar/SidebarList/Components/SidebarLinkCardView.swift
@@ -7,7 +7,7 @@ import SwiftUI
7 7
 
8 8
 struct SidebarLinkCardView: View {
9 9
     let title: String
10
-    let subtitle: String
10
+    let subtitle: String?
11 11
     let symbol: String
12 12
     let tint: Color
13 13
 
@@ -19,12 +19,14 @@ struct SidebarLinkCardView: View {
19 19
                 .frame(width: 42, height: 42)
20 20
                 .background(Circle().fill(tint.opacity(0.18)))
21 21
 
22
-            VStack(alignment: .leading, spacing: 4) {
22
+            VStack(alignment: .leading, spacing: subtitle == nil ? 0 : 4) {
23 23
                 Text(title)
24 24
                     .font(.headline)
25
-                Text(subtitle)
26
-                    .font(.caption)
27
-                    .foregroundColor(.secondary)
25
+                if let subtitle {
26
+                    Text(subtitle)
27
+                        .font(.caption)
28
+                        .foregroundColor(.secondary)
29
+                }
28 30
             }
29 31
 
30 32
             Spacer()
@@ -36,4 +38,4 @@ struct SidebarLinkCardView: View {
36 38
         .padding(14)
37 39
         .meterCard(tint: tint, fillOpacity: 0.16, strokeOpacity: 0.22, cornerRadius: 18)
38 40
     }
39
-}
41
+}
+19 -19
USB Meter/Views/Sidebar/SidebarList/Sections/ContentSidebarHelpSectionView.swift
@@ -97,33 +97,33 @@ struct ContentSidebarHelpSectionView<BluetoothHelpDestination: View, DeviceHelpD
97 97
 
98 98
                 if activeReason == .cloudSyncUnavailable {
99 99
                     Button(action: onOpenSettings) {
100
-                        SidebarLinkCardView(
101
-                            title: "Open Settings",
102
-                            subtitle: "Check Apple ID, iCloud Drive, and any restrictions affecting Cloud sync.",
103
-                            symbol: "gearshape.fill",
104
-                            tint: .indigo
105
-                        )
100
+                SidebarLinkCardView(
101
+                    title: "Open Settings",
102
+                    subtitle: nil,
103
+                    symbol: "gearshape.fill",
104
+                    tint: .indigo
105
+                )
106 106
                     }
107 107
                     .buttonStyle(.plain)
108 108
                 }
109 109
 
110 110
                 NavigationLink(destination: bluetoothHelpDestination) {
111
-                    SidebarLinkCardView(
112
-                        title: "Bluetooth",
113
-                        subtitle: "Permissions, adapter state, and connection tips.",
114
-                        symbol: "bolt.horizontal.circle.fill",
115
-                        tint: bluetoothStatusTint
116
-                    )
111
+                SidebarLinkCardView(
112
+                    title: "Bluetooth",
113
+                    subtitle: nil,
114
+                    symbol: "bolt.horizontal.circle.fill",
115
+                    tint: bluetoothStatusTint
116
+                )
117 117
                 }
118 118
                 .buttonStyle(.plain)
119 119
 
120 120
                 NavigationLink(destination: deviceHelpDestination) {
121
-                    SidebarLinkCardView(
122
-                        title: "Device",
123
-                        subtitle: "Quick checks when a meter is not responding as expected.",
124
-                        symbol: "questionmark.circle.fill",
125
-                        tint: .orange
126
-                    )
121
+                SidebarLinkCardView(
122
+                    title: "Device",
123
+                    subtitle: nil,
124
+                    symbol: "questionmark.circle.fill",
125
+                    tint: .orange
126
+                )
127 127
                 }
128 128
                 .buttonStyle(.plain)
129 129
             }
@@ -138,4 +138,4 @@ struct ContentSidebarHelpSectionView<BluetoothHelpDestination: View, DeviceHelpD
138 138
     private var sectionSymbol: String {
139 139
         activeReason?.symbol ?? "questionmark.circle.fill"
140 140
     }
141
-}
141
+}
+9 -9
USB Meter/Views/Sidebar/SidebarList/Sections/SidebarHelpSectionView.swift
@@ -97,12 +97,12 @@ struct SidebarHelpSectionView<BluetoothHelpDestination: View, DeviceHelpDestinat
97 97
 
98 98
                 if activeReason == .cloudSyncUnavailable {
99 99
                     Button(action: onOpenSettings) {
100
-                        SidebarLinkCardView(
101
-                            title: "Open Settings",
102
-                            subtitle: "Check Apple ID, iCloud Drive, and any restrictions affecting Cloud sync.",
103
-                            symbol: "gearshape.fill",
104
-                            tint: .indigo
105
-                        )
100
+                    SidebarLinkCardView(
101
+                        title: "Open Settings",
102
+                        subtitle: nil,
103
+                        symbol: "gearshape.fill",
104
+                        tint: .indigo
105
+                    )
106 106
                     }
107 107
                     .buttonStyle(.plain)
108 108
                 }
@@ -110,7 +110,7 @@ struct SidebarHelpSectionView<BluetoothHelpDestination: View, DeviceHelpDestinat
110 110
                 NavigationLink(destination: bluetoothHelpDestination) {
111 111
                     SidebarLinkCardView(
112 112
                         title: "Bluetooth",
113
-                        subtitle: "Permissions, adapter state, and connection tips.",
113
+                        subtitle: nil,
114 114
                         symbol: "bolt.horizontal.circle.fill",
115 115
                         tint: bluetoothStatusTint
116 116
                     )
@@ -120,7 +120,7 @@ struct SidebarHelpSectionView<BluetoothHelpDestination: View, DeviceHelpDestinat
120 120
                 NavigationLink(destination: deviceHelpDestination) {
121 121
                     SidebarLinkCardView(
122 122
                         title: "Device",
123
-                        subtitle: "Quick checks when a meter is not responding as expected.",
123
+                        subtitle: nil,
124 124
                         symbol: "questionmark.circle.fill",
125 125
                         tint: .orange
126 126
                     )
@@ -138,4 +138,4 @@ struct SidebarHelpSectionView<BluetoothHelpDestination: View, DeviceHelpDestinat
138 138
     private var sectionSymbol: String {
139 139
         activeReason?.symbol ?? "questionmark.circle.fill"
140 140
     }
141
-}
141
+}
+32 -3
USB Meter/Views/Sidebar/SidebarList/Sections/SidebarUSBMetersSectionView.swift
@@ -67,9 +67,17 @@ struct SidebarUSBMetersSectionView: View {
67 67
     }
68 68
 
69 69
     private var usbSectionHeader: some View {
70
-        HStack {
71
-            Text("USB Meters")
72
-                .font(.headline)
70
+        HStack(alignment: .firstTextBaseline) {
71
+            VStack(alignment: .leading, spacing: 2) {
72
+                Text("USB & Known Meters")
73
+                    .font(.headline)
74
+                if meters.isEmpty == false {
75
+                    Text(sectionSubtitleText)
76
+                        .font(.caption)
77
+                        .foregroundColor(.secondary)
78
+                        .lineLimit(1)
79
+                }
80
+            }
73 81
             Spacer()
74 82
             Text("\(meters.count)")
75 83
                 .font(.caption.weight(.bold))
@@ -78,4 +86,25 @@ struct SidebarUSBMetersSectionView: View {
78 86
                 .meterCard(tint: .blue, fillOpacity: 0.18, strokeOpacity: 0.24, cornerRadius: 999)
79 87
         }
80 88
     }
89
+
90
+    private var sectionSubtitleText: String {
91
+        switch (liveMeterCount, offlineMeterCount) {
92
+        case let (live, offline) where live > 0 && offline > 0:
93
+            return "\(live) live • \(offline) stored"
94
+        case let (live, _) where live > 0:
95
+            return "\(live) live meter\(live == 1 ? "" : "s")"
96
+        case let (_, offline) where offline > 0:
97
+            return "\(offline) known meter\(offline == 1 ? "" : "s")"
98
+        default:
99
+            return ""
100
+        }
101
+    }
102
+
103
+    private var liveMeterCount: Int {
104
+        meters.filter { $0.meter != nil }.count
105
+    }
106
+
107
+    private var offlineMeterCount: Int {
108
+        max(0, meters.count - liveMeterCount)
109
+    }
81 110
 }