Showing 16 changed files with 1064 additions and 371 deletions
+73 -19
USB Meter/Extensions/CBManagerState.swift
@@ -79,59 +79,113 @@ extension CBManagerState {
79 79
     
80 80
     private struct poweredOffHelperView: View {
81 81
         var body: some View {
82
-            Text("Bluetooth is turned off on this device. Tou can turn it on in Settings->Bluetooth")
82
+            BluetoothHelpCard(
83
+                title: "Bluetooth Off",
84
+                detail: "Bluetooth is turned off on this device. You can enable it in Settings > Bluetooth.",
85
+                tint: .red
86
+            )
83 87
         }
84 88
     }
85 89
     private struct poweredOnHelperView: View {
86 90
         var body: some View {
87
-            Text("Bluetooth is up an running")
91
+            BluetoothHelpCard(
92
+                title: "Bluetooth Ready",
93
+                detail: "Bluetooth is powered on and ready for scanning.",
94
+                tint: .blue
95
+            )
88 96
         }
89 97
     }
90 98
     
91 99
     private struct resettingHelperView: View {
92 100
         var body: some View {
93
-            VStack {
94
-                Text("Bluetooth is resetting")
95
-                Text("Maybe wait for a while...")
96
-            }
101
+            BluetoothHelpCard(
102
+                title: "Bluetooth Resetting",
103
+                detail: "The Bluetooth stack is temporarily resetting. Wait a moment and try again.",
104
+                tint: .green
105
+            )
97 106
         }
98 107
     }
99 108
     
100 109
     private struct unauthorizedHelperView: View {
101 110
         var body: some View {
102
-            HStack {
103
-                Text("This application does not have permission to access Bluetooth. You can give it in ")
111
+            VStack(alignment: .leading, spacing: 12) {
112
+                Text("Bluetooth Access Needed")
113
+                    .font(.headline)
114
+                Text("This application does not have permission to access Bluetooth. You can enable it in Settings.")
115
+                    .font(.footnote)
116
+                    .foregroundColor(.secondary)
104 117
                 Button(action: { UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!, options: [:], completionHandler: nil) }) {
105 118
                     Text("Settings")
106 119
                 }
120
+                .padding(.horizontal, 14)
121
+                .padding(.vertical, 10)
122
+                .meterCard(tint: .orange, fillOpacity: 0.16, strokeOpacity: 0.22, cornerRadius: 14)
123
+                .buttonStyle(.plain)
107 124
             }
125
+            .frame(maxWidth: .infinity, alignment: .leading)
126
+            .padding(20)
127
+            .meterCard(tint: .orange, fillOpacity: 0.18, strokeOpacity: 0.24)
128
+            .padding()
108 129
         }
109 130
     }
110 131
     
111 132
     private struct unknownHelperView: View {
112 133
         var body: some View {
113
-            VStack {
114
-            Text("Bluetooth state is unknown!")
115
-            Text("There is no help available for this situation.")
116
-            }
134
+            BluetoothHelpCard(
135
+                title: "Unknown Bluetooth State",
136
+                detail: "Bluetooth is reporting an unknown state. Wait a moment and check again.",
137
+                tint: .secondary
138
+            )
117 139
         }
118 140
     }
119 141
     
120 142
     private struct unsupportedHelperView: View {
121 143
         var body: some View {
122
-            VStack {
123
-            Text("Your device does not have required capabilities to establish Bluetooth connections with USB Meters")
124
-            Text("There is no help available for this situation.")
125
-            }
144
+            BluetoothHelpCard(
145
+                title: "Bluetooth Unsupported",
146
+                detail: "This device does not support the Bluetooth capabilities required by these USB meters.",
147
+                tint: .gray
148
+            )
126 149
         }
127 150
     }
128 151
     
129 152
     private struct defaultHelperView: View {
130 153
         var body: some View {
131
-            VStack {
132
-                Text("Unknown Bluetooth state.")
133
-                Text("You may contact develloper.")
154
+            BluetoothHelpCard(
155
+                title: "Other Bluetooth State",
156
+                detail: "Bluetooth is in an unexpected state. Try again, then contact the developer if it persists.",
157
+                tint: .yellow
158
+            )
159
+        }
160
+    }
161
+
162
+    private struct BluetoothHelpCard: View {
163
+        let title: String
164
+        let detail: String
165
+        let tint: Color
166
+
167
+        var body: some View {
168
+            ScrollView {
169
+                VStack(alignment: .leading, spacing: 10) {
170
+                    Text(title)
171
+                        .font(.system(.title3, design: .rounded).weight(.bold))
172
+                    Text(detail)
173
+                        .font(.footnote)
174
+                        .foregroundColor(.secondary)
175
+                }
176
+                .frame(maxWidth: .infinity, alignment: .leading)
177
+                .padding(20)
178
+                .meterCard(tint: tint, fillOpacity: 0.18, strokeOpacity: 0.24)
179
+                .padding()
134 180
             }
181
+            .background(
182
+                LinearGradient(
183
+                    colors: [tint.opacity(0.14), Color.clear],
184
+                    startPoint: .topLeading,
185
+                    endPoint: .bottomTrailing
186
+                )
187
+                .ignoresSafeArea()
188
+            )
135 189
         }
136 190
     }
137 191
 }
+51 -5
USB Meter/Extensions/UIView.swift
@@ -42,6 +42,35 @@ struct RoundedRectangleBorder: ViewModifier {
42 42
     }
43 43
 }
44 44
 
45
+struct MeterCardStyle: ViewModifier {
46
+    var tint: Color
47
+    var fillOpacity: Double
48
+    var strokeOpacity: Double
49
+    var cornerRadius: CGFloat
50
+
51
+    func body(content: Content) -> some View {
52
+        content
53
+            .background(
54
+                RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)
55
+                    .fill(
56
+                        LinearGradient(
57
+                            colors: [
58
+                                tint.opacity(fillOpacity),
59
+                                Color.secondary.opacity(fillOpacity * 0.75)
60
+                            ],
61
+                            startPoint: .topLeading,
62
+                            endPoint: .bottomTrailing
63
+                        )
64
+                    )
65
+            )
66
+            .overlay(
67
+                RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)
68
+                    .stroke(tint.opacity(strokeOpacity), lineWidth: 1.25)
69
+            )
70
+            .shadow(color: tint.opacity(fillOpacity * 0.9), radius: 14, x: 0, y: 8)
71
+    }
72
+}
73
+
45 74
 extension View {
46 75
     func withRoundedRectangleBackground( cornerRadius: CGFloat, foregroundColor: Color, opacity: Double, blurRadius: CGFloat = 0 ) -> some View {
47 76
         self.modifier(RoundedRectangleBackground(cornerRadius: cornerRadius, foregroundColor: foregroundColor, opacity: opacity, blurRadius: blurRadius))
@@ -50,16 +79,33 @@ extension View {
50 79
     func withRoundedRectangleBorder( cornerRadius: CGFloat, foregroundColor: Color, lineWidth: CGFloat, blurRadius: CGFloat = 0 ) -> some View {
51 80
         self.modifier(RoundedRectangleBorder(cornerRadius: cornerRadius, foregroundColor: foregroundColor, lineWidth: lineWidth, blurRadius: blurRadius ))
52 81
     }
82
+
83
+    func meterCard(
84
+        tint: Color = .primary,
85
+        fillOpacity: Double = 0.14,
86
+        strokeOpacity: Double = 0.22,
87
+        cornerRadius: CGFloat = 22
88
+    ) -> some View {
89
+        self.modifier(
90
+            MeterCardStyle(
91
+                tint: tint,
92
+                fillOpacity: fillOpacity,
93
+                strokeOpacity: strokeOpacity,
94
+                cornerRadius: cornerRadius
95
+            )
96
+        )
97
+    }
53 98
 }
54 99
 
55 100
 // MARK: Local
56 101
 extension Button {
57 102
     func asEnableFeatureButton(state: Bool) -> some View {
58 103
         self
59
-            .foregroundColor( state ? .primary : .blue )
60
-            .padding(5)
61
-            .withRoundedRectangleBackground(cornerRadius: 15, foregroundColor: state ? .blue : .clear, opacity: 0.5)
62
-            .withRoundedRectangleBorder(cornerRadius: 15, foregroundColor: .blue, lineWidth: 1, blurRadius: 0.1)
104
+            .foregroundColor(state ? .white : .blue)
105
+            .padding(.horizontal, 12)
106
+            .padding(.vertical, 8)
107
+            .frame(minWidth: 84)
108
+            .withRoundedRectangleBackground(cornerRadius: 15, foregroundColor: state ? .blue : .blue, opacity: state ? 0.88 : 0.12)
109
+            .withRoundedRectangleBorder(cornerRadius: 15, foregroundColor: .blue, lineWidth: 1.5, blurRadius: 0.1)
63 110
     }
64 111
 }
65
-
+4 -0
USB Meter/Model/Meter.swift
@@ -207,6 +207,10 @@ class Meter : NSObject, ObservableObject, Identifiable {
207 207
         capabilities.dataGroupsTitle
208 208
     }
209 209
 
210
+    var documentedWorkingVoltage: String {
211
+        capabilities.documentedWorkingVoltage
212
+    }
213
+
210 214
     var chargerTypeDescription: String {
211 215
         capabilities.chargerTypeDescription(for: chargerTypeIndex)
212 216
     }
+3 -0
USB Meter/Model/MeterCapabilities.swift
@@ -20,6 +20,7 @@ struct MeterCapabilities {
20 20
     let supportsChargerDetection: Bool
21 21
     let primaryTemperatureUnitSymbol: String?
22 22
     let dataGroupsTitle: String
23
+    let documentedWorkingVoltage: String
23 24
     let chargerTypeDescriptions: [UInt16: String]
24 25
     let screenDescriptions: [UInt16: String]
25 26
     let dataGroupsHint: String?
@@ -52,6 +53,7 @@ extension MeterCapabilities {
52 53
         supportsChargerDetection: true,
53 54
         primaryTemperatureUnitSymbol: "℃",
54 55
         dataGroupsTitle: "Data Groups",
56
+        documentedWorkingVoltage: "4-24 V",
55 57
         chargerTypeDescriptions: [
56 58
             1: "QC2",
57 59
             2: "QC3",
@@ -87,6 +89,7 @@ extension MeterCapabilities {
87 89
         supportsChargerDetection: false,
88 90
         primaryTemperatureUnitSymbol: nil,
89 91
         dataGroupsTitle: "Memory Totals",
92
+        documentedWorkingVoltage: "3.5-24 V",
90 93
         chargerTypeDescriptions: [:],
91 94
         screenDescriptions: [:],
92 95
         dataGroupsHint: "The device exposes two read-only memories with charge and energy totals. The active memory is not reported.",
+153 -24
USB Meter/Views/ContentView.swift
@@ -16,36 +16,165 @@ struct ContentView: View {
16 16
     
17 17
     var body: some View {
18 18
         NavigationView {
19
-            List {
20
-                Section(header: Text("Help")) {
21
-                //VStack {
22
-                    NavigationLink(destination: appData.bluetoothManager.managerState.helpView ) {
23
-                        Text("Bluetooth")
24
-                            .foregroundColor(appData.bluetoothManager.managerState.color)
25
-                    }
26
-                    NavigationLink(destination: DeviceHelpView()) {
27
-                        Text("Device")
28
-                            .foregroundColor(appData.bluetoothManager.managerState.color)
29
-                    }
30
-                }
31
-                Section(header: Text("Discovered Devices")) {
32
-                    ForEach( [Meter](appData.meters.values), id: \.self ) { meter in
33
-                        NavigationLink(destination: MeterView()
34
-                            .environmentObject(meter) ) {
35
-                                MeterRowView()
36
-                                .environmentObject( meter )
37
-                        }
38
-                    }
19
+            ScrollView {
20
+                VStack(alignment: .leading, spacing: 18) {
21
+                    headerCard
22
+                    helpSection
23
+                    devicesSection
39 24
                 }
25
+                .padding()
40 26
             }
27
+            .background(
28
+                LinearGradient(
29
+                    colors: [
30
+                        appData.bluetoothManager.managerState.color.opacity(0.18),
31
+                        Color.clear
32
+                    ],
33
+                    startPoint: .topLeading,
34
+                    endPoint: .bottomTrailing
35
+                )
36
+                .ignoresSafeArea()
37
+            )
41 38
             .navigationBarTitle(Text("USB Meters"), displayMode: .inline)
42
-            .navigationBarItems(trailing:
43
-                Button("Help") {
44
-                    print("Help tapped!")
45
-            })
46 39
         }
47 40
         .onAppear {
48 41
             appData.bluetoothManager.start()
49 42
         }
50 43
     }
44
+
45
+    private var headerCard: some View {
46
+        VStack(alignment: .leading, spacing: 10) {
47
+            Text("USB Meters")
48
+                .font(.system(.title2, design: .rounded).weight(.bold))
49
+            Text("Browse nearby supported meters and jump into live diagnostics, charge records, and device controls.")
50
+                .font(.footnote)
51
+                .foregroundColor(.secondary)
52
+            HStack {
53
+                Label("Bluetooth", systemImage: "bolt.horizontal.circle.fill")
54
+                    .font(.footnote.weight(.semibold))
55
+                    .foregroundColor(appData.bluetoothManager.managerState.color)
56
+                Spacer()
57
+                Text(bluetoothStatusText)
58
+                    .font(.caption.weight(.semibold))
59
+                    .foregroundColor(.secondary)
60
+            }
61
+        }
62
+        .padding(18)
63
+        .meterCard(tint: appData.bluetoothManager.managerState.color, fillOpacity: 0.22, strokeOpacity: 0.26)
64
+    }
65
+
66
+    private var helpSection: some View {
67
+        VStack(alignment: .leading, spacing: 12) {
68
+            Text("Help")
69
+                .font(.headline)
70
+
71
+            NavigationLink(destination: appData.bluetoothManager.managerState.helpView) {
72
+                sidebarLinkCard(
73
+                    title: "Bluetooth",
74
+                    subtitle: "Permissions, adapter state, and connection tips.",
75
+                    symbol: "bolt.horizontal.circle.fill",
76
+                    tint: appData.bluetoothManager.managerState.color
77
+                )
78
+            }
79
+            .buttonStyle(.plain)
80
+
81
+            NavigationLink(destination: DeviceHelpView()) {
82
+                sidebarLinkCard(
83
+                    title: "Device",
84
+                    subtitle: "Quick checks when a meter is not responding as expected.",
85
+                    symbol: "questionmark.circle.fill",
86
+                    tint: .orange
87
+                )
88
+            }
89
+            .buttonStyle(.plain)
90
+        }
91
+    }
92
+
93
+    private var devicesSection: some View {
94
+        VStack(alignment: .leading, spacing: 12) {
95
+            HStack {
96
+                Text("Discovered Devices")
97
+                    .font(.headline)
98
+                Spacer()
99
+                Text("\(appData.meters.count)")
100
+                    .font(.caption.weight(.bold))
101
+                    .padding(.horizontal, 10)
102
+                    .padding(.vertical, 6)
103
+                    .meterCard(tint: .blue, fillOpacity: 0.18, strokeOpacity: 0.24, cornerRadius: 999)
104
+            }
105
+
106
+            if appData.meters.isEmpty {
107
+                Text("No supported meters are visible right now.")
108
+                    .font(.footnote)
109
+                    .foregroundColor(.secondary)
110
+                    .frame(maxWidth: .infinity, alignment: .leading)
111
+                    .padding(18)
112
+                    .meterCard(tint: .secondary, fillOpacity: 0.14, strokeOpacity: 0.20)
113
+            } else {
114
+                ForEach(discoveredMeters, id: \.self) { meter in
115
+                    NavigationLink(destination: MeterView().environmentObject(meter)) {
116
+                        MeterRowView()
117
+                            .environmentObject(meter)
118
+                    }
119
+                    .buttonStyle(.plain)
120
+                }
121
+            }
122
+        }
123
+    }
124
+
125
+    private var discoveredMeters: [Meter] {
126
+        Array(appData.meters.values).sorted { lhs, rhs in
127
+            lhs.name.localizedCaseInsensitiveCompare(rhs.name) == .orderedAscending
128
+        }
129
+    }
130
+
131
+    private var bluetoothStatusText: String {
132
+        switch appData.bluetoothManager.managerState {
133
+        case .poweredOff:
134
+            return "Off"
135
+        case .poweredOn:
136
+            return "On"
137
+        case .resetting:
138
+            return "Resetting"
139
+        case .unauthorized:
140
+            return "Unauthorized"
141
+        case .unknown:
142
+            return "Unknown"
143
+        case .unsupported:
144
+            return "Unsupported"
145
+        @unknown default:
146
+            return "Other"
147
+        }
148
+    }
149
+
150
+    private func sidebarLinkCard(
151
+        title: String,
152
+        subtitle: String,
153
+        symbol: String,
154
+        tint: Color
155
+    ) -> some View {
156
+        HStack(spacing: 14) {
157
+            Image(systemName: symbol)
158
+                .font(.system(size: 18, weight: .semibold))
159
+                .foregroundColor(tint)
160
+                .frame(width: 42, height: 42)
161
+                .background(Circle().fill(tint.opacity(0.18)))
162
+
163
+            VStack(alignment: .leading, spacing: 4) {
164
+                Text(title)
165
+                    .font(.headline)
166
+                Text(subtitle)
167
+                    .font(.caption)
168
+                    .foregroundColor(.secondary)
169
+            }
170
+
171
+            Spacer()
172
+
173
+            Image(systemName: "chevron.right")
174
+                .font(.footnote.weight(.bold))
175
+                .foregroundColor(.secondary)
176
+        }
177
+        .padding(14)
178
+        .meterCard(tint: tint, fillOpacity: 0.16, strokeOpacity: 0.22, cornerRadius: 18)
179
+    }
51 180
 }
+44 -4
USB Meter/Views/DeviceHelpView.swift
@@ -11,10 +11,50 @@ import CoreBluetooth
11 11
 
12 12
 struct DeviceHelpView: View {
13 13
     var body: some View {
14
-        List {
15
-            Text("Device ON?")
16
-            Text("Device has Bluetooth enabled?")
17
-            Text("Device allerady connected?")
14
+        ScrollView {
15
+            VStack(alignment: .leading, spacing: 14) {
16
+                Text("Device Checklist")
17
+                    .font(.system(.title3, design: .rounded).weight(.bold))
18
+                    .frame(maxWidth: .infinity, alignment: .leading)
19
+                    .padding(18)
20
+                    .meterCard(tint: .orange, fillOpacity: 0.18, strokeOpacity: 0.24)
21
+
22
+                helpCard(
23
+                    title: "Power",
24
+                    body: "Make sure the meter itself is powered on before trying to connect."
25
+                )
26
+                helpCard(
27
+                    title: "Bluetooth",
28
+                    body: "Confirm Bluetooth is enabled on the device and not disabled from the meter menu."
29
+                )
30
+                helpCard(
31
+                    title: "Existing Connection",
32
+                    body: "If the meter is already connected elsewhere, disconnect it first and then retry from the app."
33
+                )
34
+            }
35
+            .padding()
36
+        }
37
+        .background(
38
+            LinearGradient(
39
+                colors: [.orange.opacity(0.14), Color.clear],
40
+                startPoint: .topLeading,
41
+                endPoint: .bottomTrailing
42
+            )
43
+            .ignoresSafeArea()
44
+        )
45
+        .navigationTitle("Device Help")
46
+    }
47
+
48
+    private func helpCard(title: String, body: String) -> some View {
49
+        VStack(alignment: .leading, spacing: 6) {
50
+            Text(title)
51
+                .font(.headline)
52
+            Text(body)
53
+                .font(.footnote)
54
+                .foregroundColor(.secondary)
18 55
         }
56
+        .frame(maxWidth: .infinity, alignment: .leading)
57
+        .padding(18)
58
+        .meterCard(tint: .orange, fillOpacity: 0.14, strokeOpacity: 0.20)
19 59
     }
20 60
 }
+67 -13
USB Meter/Views/Meter/ControlView.swift
@@ -13,24 +13,78 @@ struct ControlView: View {
13 13
     @EnvironmentObject private var meter: Meter
14 14
 
15 15
     var body: some View {
16
-        HStack {
17
-            VStack {
18
-                HStack {
19
-                    Button(action: { self.meter.rotateScreen() }, label: { Image(systemName: "arrow.clockwise") })
20
-                }
21
-                HStack {
22
-                    Button(action: { self.meter.previousScreen() }, label: { Image(systemName: "arrowtriangle.left") })
16
+        VStack(alignment: .leading, spacing: 14) {
17
+            HStack {
18
+                Text("Controls")
19
+                    .font(.headline)
20
+                Spacer()
21
+                Text(meter.reportsCurrentScreenIndex ? "Device Screen" : "Page Controls")
22
+                    .font(.caption.weight(.semibold))
23
+                    .foregroundColor(.secondary)
24
+            }
25
+
26
+            HStack(spacing: 12) {
27
+                controlButton(
28
+                    title: "Prev",
29
+                    symbol: "chevron.left",
30
+                    tint: .indigo,
31
+                    action: { meter.previousScreen() }
32
+                )
33
+
34
+                VStack(spacing: 6) {
23 35
                     Text(meter.currentScreenDescription)
24
-                    Button(action: { self.meter.nextScreen() }, label: { Image(systemName: "arrowtriangle.right") })
25
-                }
26
-                if !meter.reportsCurrentScreenIndex {
27
-                    Text("This protocol supports page navigation, but not current-page reporting.")
28
-                        .font(.footnote)
29
-                        .foregroundColor(.secondary)
36
+                        .font(.subheadline.weight(.semibold))
30 37
                         .multilineTextAlignment(.center)
38
+                    if !meter.reportsCurrentScreenIndex {
39
+                        Text("The protocol allows navigation, but it does not report the active page back to the app.")
40
+                            .font(.caption)
41
+                            .foregroundColor(.secondary)
42
+                            .multilineTextAlignment(.center)
43
+                    }
31 44
                 }
45
+                .frame(maxWidth: .infinity, minHeight: 92)
46
+                .padding(.horizontal, 12)
47
+                .meterCard(tint: meter.color, fillOpacity: 0.06, strokeOpacity: 0.10)
48
+
49
+                controlButton(
50
+                    title: "Next",
51
+                    symbol: "chevron.right",
52
+                    tint: .indigo,
53
+                    action: { meter.nextScreen() }
54
+                )
55
+            }
56
+
57
+            controlButton(
58
+                title: "Rotate Screen",
59
+                symbol: "rotate.right.fill",
60
+                tint: .orange,
61
+                compact: false,
62
+                action: { meter.rotateScreen() }
63
+            )
64
+        }
65
+    }
66
+
67
+    private func controlButton(
68
+        title: String,
69
+        symbol: String,
70
+        tint: Color,
71
+        compact: Bool = true,
72
+        action: @escaping () -> Void
73
+    ) -> some View {
74
+        Button(action: action) {
75
+            VStack(spacing: 10) {
76
+                Image(systemName: symbol)
77
+                    .font(.system(size: compact ? 18 : 20, weight: .semibold))
78
+                Text(title)
79
+                    .font(.footnote.weight(.semibold))
80
+                    .multilineTextAlignment(.center)
32 81
             }
82
+            .foregroundColor(tint)
83
+            .frame(maxWidth: .infinity, minHeight: compact ? 92 : 68)
84
+            .padding(.horizontal, 8)
85
+            .meterCard(tint: tint, fillOpacity: 0.10, strokeOpacity: 0.14)
33 86
         }
87
+        .buttonStyle(.plain)
34 88
     }
35 89
 }
36 90
 
+37 -18
USB Meter/Views/Meter/Data Groups/DataGroupRowView.swift
@@ -21,38 +21,57 @@ struct DataGroupRowView: View {
21 21
     @EnvironmentObject private var usbMeter: Meter
22 22
     
23 23
     var body: some View {
24
-        HStack (spacing: 1) {
25
-            ZStack {
24
+        HStack(spacing: 8) {
25
+            cell(width: width) {
26 26
                 if showsCommands {
27
-                    Button(action: { self.usbMeter.selectDataGroup(id: self.id) }, label: { Image(systemName: "\(id).circle") })
27
+                    Button(action: { usbMeter.selectDataGroup(id: id) }) {
28
+                        Label(usbMeter.dataGroupLabel(for: id), systemImage: "\(id).circle")
29
+                            .labelStyle(.titleAndIcon)
30
+                    }
31
+                    .buttonStyle(.plain)
28 32
                 } else {
29 33
                     Text(usbMeter.dataGroupLabel(for: id))
34
+                        .fontWeight(.semibold)
30 35
                 }
31
-                Rectangle().opacity( opacity )
32
-            }.frame(width: width)
36
+            }
33 37
             
34
-            ZStack {
38
+            cell(width: width) {
35 39
                 Text("\(usbMeter.dataGroupRecords[Int(id)]!.ah.format(decimalDigits: 3))")
36
-                Rectangle().opacity( opacity )
37
-            }.frame(width: width)
40
+                    .monospacedDigit()
41
+            }
38 42
             
39 43
             if showsEnergy {
40
-                ZStack {
44
+                cell(width: width) {
41 45
                     Text("\(usbMeter.dataGroupRecords[Int(id)]!.wh.format(decimalDigits: 3))")
42
-                    Rectangle().opacity( opacity )
43
-                }.frame(width: width)
46
+                        .monospacedDigit()
47
+                }
44 48
             }
45 49
             
46 50
             if showsCommands {
47
-                ZStack {
48
-                    Button(action: { self.usbMeter.clear(group: self.id) }, label: { Image(systemName: "bin.xmark") })
49
-                    Rectangle().opacity( opacity )
50
-                }.frame(width: width)
51
+                cell(width: width) {
52
+                    Button(action: { usbMeter.clear(group: id) }) {
53
+                        Label("Clear", systemImage: "trash")
54
+                            .labelStyle(.iconOnly)
55
+                            .foregroundColor(.red)
56
+                    }
57
+                    .buttonStyle(.plain)
58
+                }
51 59
             }
52 60
         }
53
-        .overlay(
54
-            RoundedRectangle(cornerRadius: 10)
55
-                .stroke(highlightsSelection && usbMeter.selectedDataGroup == id ? Color.accentColor : Color.clear, lineWidth: 3)
61
+        .padding(10)
62
+        .meterCard(
63
+            tint: highlightsSelection && usbMeter.selectedDataGroup == id ? .accentColor : .secondary,
64
+            fillOpacity: highlightsSelection && usbMeter.selectedDataGroup == id ? 0.22 : 0.08 + opacity * 0.25,
65
+            strokeOpacity: highlightsSelection && usbMeter.selectedDataGroup == id ? 0.32 : 0.16,
66
+            cornerRadius: 16
56 67
         )
57 68
     }
69
+
70
+    private func cell<Content: View>(width: CGFloat, @ViewBuilder content: () -> Content) -> some View {
71
+        content()
72
+            .font(.footnote.weight(.semibold))
73
+            .frame(width: width)
74
+            .frame(minHeight: 40)
75
+            .multilineTextAlignment(.center)
76
+    }
58 77
 }
+57 -55
USB Meter/Views/Meter/Data Groups/DataGroupsView.swift
@@ -5,7 +5,6 @@
5 5
 //  Created by Bogdan Timofte on 10/03/2020.
6 6
 //  Copyright © 2020 Bogdan Timofte. All rights reserved.
7 7
 //
8
-//  MARK: GeometryReader: https://stackoverflow.com/questions/57577462/get-width-of-a-view-using-in-swiftui/57591483 Pe iPhone dimensiunea shetului pare mai mare decat cea afisata fara GeometryReader
9 8
 
10 9
 import SwiftUI
11 10
 
@@ -15,67 +14,70 @@ struct DataGroupsView: View {
15 14
     @EnvironmentObject private var usbMeter: Meter
16 15
     
17 16
     var body: some View {
18
-        GeometryReader { box in
19
-            let columnTitles = [usbMeter.supportsDataGroupCommands ? "Group" : "Memory", "Ah"]
20
-                + (usbMeter.showsDataGroupEnergy ? ["Wh"] : [])
21
-                + (usbMeter.supportsDataGroupCommands ? ["Clear"] : [])
22
-            let columnWidth = (box.size.width - 25) / CGFloat(columnTitles.count)
23
-            let tableReservedHeight: CGFloat = usbMeter.dataGroupsHint == nil ? 100 : 140
24
-            let rowCount = CGFloat(usbMeter.availableDataGroupIDs.count + 1)
25
-            let rowHeight = (box.size.height - tableReservedHeight) / rowCount
17
+        NavigationView {
18
+            GeometryReader { box in
19
+                let columnTitles = [usbMeter.supportsDataGroupCommands ? "Group" : "Memory", "Ah"]
20
+                    + (usbMeter.showsDataGroupEnergy ? ["Wh"] : [])
21
+                    + (usbMeter.supportsDataGroupCommands ? ["Clear"] : [])
22
+                let columnWidth = (box.size.width - 60) / CGFloat(columnTitles.count)
26 23
 
27
-            VStack (spacing: 1) {
28
-                HStack {
29
-                    Text(usbMeter.dataGroupsTitle)
30
-                        .bold()
31
-                    Spacer()
32
-                    Button(action: {self.visibility.toggle()}) {
33
-                        Text("ⓧ")
34
-                            .foregroundColor(.red)
35
-                    }
36
-                }
37
-                .font(.title)
38
-                
39
-                if let hint = usbMeter.dataGroupsHint {
40
-                    Text(hint)
41
-                        .font(.footnote)
42
-                        .foregroundColor(.secondary)
43
-                        .multilineTextAlignment(.center)
44
-                        .padding(.vertical, 8)
45
-                } else {
46
-                    Spacer(minLength: 8)
47
-                }
48
-                
49
-                HStack (spacing: 1) {
50
-                    ForEach(columnTitles, id: \.self ) { text in
51
-                        self.THView(text: text, width: columnWidth)
24
+                ScrollView {
25
+                    VStack(alignment: .leading, spacing: 14) {
26
+                        VStack(alignment: .leading, spacing: 8) {
27
+                            Text(usbMeter.dataGroupsTitle)
28
+                                .font(.system(.title3, design: .rounded).weight(.bold))
29
+                            if let hint = usbMeter.dataGroupsHint {
30
+                                Text(hint)
31
+                                    .font(.footnote)
32
+                                    .foregroundColor(.secondary)
33
+                            }
34
+                        }
35
+                        .padding(18)
36
+                        .meterCard(tint: .teal, fillOpacity: 0.18, strokeOpacity: 0.24)
37
+
38
+                        HStack(spacing: 8) {
39
+                            ForEach(columnTitles, id: \.self ) { text in
40
+                                headerCell(text: text, width: columnWidth)
41
+                            }
42
+                        }
43
+
44
+                        VStack(spacing: 10) {
45
+                            ForEach(usbMeter.availableDataGroupIDs, id: \.self) { groupId in
46
+                                DataGroupRowView(
47
+                                    id: groupId,
48
+                                    width: columnWidth,
49
+                                    opacity: groupId.isMultiple(of: 2) ? 0.1 : 0.2,
50
+                                    showsCommands: usbMeter.supportsDataGroupCommands,
51
+                                    showsEnergy: usbMeter.showsDataGroupEnergy,
52
+                                    highlightsSelection: usbMeter.highlightsActiveDataGroup
53
+                                )
54
+                            }
55
+                        }
52 56
                     }
57
+                    .padding()
53 58
                 }
54
-                .frame(height: rowHeight)
55
-                ForEach(usbMeter.availableDataGroupIDs, id: \.self) { groupId in
56
-                    DataGroupRowView(
57
-                        id: groupId,
58
-                        width: columnWidth,
59
-                        opacity: groupId.isMultiple(of: 2) ? 0.1 : 0.2,
60
-                        showsCommands: usbMeter.supportsDataGroupCommands,
61
-                        showsEnergy: usbMeter.showsDataGroupEnergy,
62
-                        highlightsSelection: usbMeter.highlightsActiveDataGroup
59
+                .background(
60
+                    LinearGradient(
61
+                        colors: [.teal.opacity(0.14), Color.clear],
62
+                        startPoint: .topLeading,
63
+                        endPoint: .bottomTrailing
63 64
                     )
64
-                }
65
-                .frame(height: rowHeight)
65
+                    .ignoresSafeArea()
66
+                )
66 67
             }
67
-            .padding()
68
+            .navigationTitle(usbMeter.dataGroupsTitle)
69
+            .navigationBarTitleDisplayMode(.inline)
70
+            .navigationBarItems(trailing: Button("Done") { visibility.toggle() })
68 71
         }
72
+        .navigationViewStyle(StackNavigationViewStyle())
69 73
     }
70 74
     
71
-    fileprivate func THView(text: String, width: CGFloat) -> some View {
72
-        return ZStack {
73
-            Rectangle()
74
-                .opacity(0.4)
75
-            Text(text)
76
-                .bold()
77
-        }
78
-        .frame(width: width)
75
+    fileprivate func headerCell(text: String, width: CGFloat) -> some View {
76
+        Text(text)
77
+            .font(.footnote.weight(.bold))
78
+            .frame(width: width)
79
+            .frame(minHeight: 38)
80
+            .foregroundColor(.secondary)
81
+            .meterCard(tint: .secondary, fillOpacity: 0.14, strokeOpacity: 0.20, cornerRadius: 14)
79 82
     }
80
-    
81 83
 }
+160 -43
USB Meter/Views/Meter/LiveView.swift
@@ -13,54 +13,171 @@ struct LiveView: View {
13 13
     @EnvironmentObject private var meter: Meter
14 14
 
15 15
     var body: some View {
16
-        VStack {
17
-            Text("Live Data")
18
-                .font(.headline)
16
+        VStack(alignment: .leading, spacing: 16) {
19 17
             HStack {
20
-                VStack (alignment: .leading) {
21
-                    Text("Voltage:")
22
-                    Text("Current:")
23
-                    Text("Power:")
24
-                    Text("Load")
25
-                    Text("Temperature:")
26
-                    if meter.secondaryTemperatureDescription != nil {
27
-                        Text("")
28
-                    }
29
-                    Text("USB Data+:")
30
-                    Text("USB Data-:")
31
-                    if meter.supportsChargerDetection {
32
-                        Text("Charger:")
33
-                    }
34
-                }
35
-                VStack (alignment: .trailing) {
36
-                    HStack {
37
-                        Text("\(meter.measurements.voltage.context.minValue.format(decimalDigits: 3))V")
38
-                        Text("\(meter.voltage.format(decimalDigits: 3))V")
39
-                        Text("\(meter.measurements.voltage.context.maxValue.format(decimalDigits: 3))V")
40
-                    }
41
-                    HStack {
42
-                        Text("\(meter.measurements.current.context.minValue.format(decimalDigits: 3))A")
43
-                        Text("\(meter.current.format(decimalDigits: 3))A")
44
-                        Text("\(meter.measurements.current.context.maxValue.format(decimalDigits: 3))A")
45
-                    }
46
-                    HStack {
47
-                        Text("\(meter.measurements.power.context.minValue.format(decimalDigits: 3))W")
48
-                        Text("\(meter.power.format(decimalDigits: 3))W")
49
-                        Text("\(meter.measurements.power.context.maxValue.format(decimalDigits: 3))W")
50
-                    }
51
-                    Text("\(meter.loadResistance.format(decimalDigits: 1))Ω")
52
-                    Text(meter.primaryTemperatureDescription)
53
-                    if let secondaryTemperatureDescription = meter.secondaryTemperatureDescription {
54
-                        Text(secondaryTemperatureDescription)
18
+                Text("Live Data")
19
+                    .font(.headline)
20
+                Spacer()
21
+                statusBadge
22
+            }
23
+
24
+            LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 12) {
25
+                liveMetricCard(
26
+                    title: "Voltage",
27
+                    symbol: "bolt.fill",
28
+                    color: .green,
29
+                    value: "\(meter.voltage.format(decimalDigits: 3)) V",
30
+                    range: rangeText(
31
+                        min: meter.measurements.voltage.context.minValue,
32
+                        max: meter.measurements.voltage.context.maxValue,
33
+                        unit: "V"
34
+                    )
35
+                )
36
+
37
+                liveMetricCard(
38
+                    title: "Current",
39
+                    symbol: "waveform.path.ecg",
40
+                    color: .blue,
41
+                    value: "\(meter.current.format(decimalDigits: 3)) A",
42
+                    range: rangeText(
43
+                        min: meter.measurements.current.context.minValue,
44
+                        max: meter.measurements.current.context.maxValue,
45
+                        unit: "A"
46
+                    )
47
+                )
48
+
49
+                liveMetricCard(
50
+                    title: "Power",
51
+                    symbol: "flame.fill",
52
+                    color: .pink,
53
+                    value: "\(meter.power.format(decimalDigits: 3)) W",
54
+                    range: rangeText(
55
+                        min: meter.measurements.power.context.minValue,
56
+                        max: meter.measurements.power.context.maxValue,
57
+                        unit: "W"
58
+                    )
59
+                )
60
+
61
+                liveMetricCard(
62
+                    title: "Temperature",
63
+                    symbol: "thermometer.medium",
64
+                    color: .orange,
65
+                    value: meter.primaryTemperatureDescription,
66
+                    range: meter.secondaryTemperatureDescription
67
+                )
68
+            }
69
+
70
+            if shouldShowSecondaryDetails {
71
+                VStack(alignment: .leading, spacing: 12) {
72
+                    Text("Details")
73
+                        .font(.subheadline.weight(.semibold))
74
+                        .foregroundColor(.secondary)
75
+
76
+                    if meter.loadResistance > 0 {
77
+                        secondaryDetailRow(
78
+                            title: "Load",
79
+                            value: "\(meter.loadResistance.format(decimalDigits: 1)) \u{2126}",
80
+                            symbol: "cable.connector",
81
+                            color: .yellow
82
+                        )
55 83
                     }
56
-                    Text("\(meter.usbPlusVoltage.format(decimalDigits: 2))V")
57
-                    Text("\(meter.usbMinusVoltage.format(decimalDigits: 2))V")
58
-                    if meter.supportsChargerDetection {
59
-                        Text(meter.chargerTypeDescription)
84
+
85
+                    if shouldShowChargerType {
86
+                        secondaryDetailRow(
87
+                            title: "Charger",
88
+                            value: meter.chargerTypeDescription,
89
+                            symbol: "bolt.badge.checkmark",
90
+                            color: .purple
91
+                        )
60 92
                     }
61 93
                 }
94
+                .padding(18)
95
+                .meterCard(tint: meter.color, fillOpacity: 0.06, strokeOpacity: 0.10)
62 96
             }
63
-            .font(.footnote)
64 97
         }
65 98
     }
99
+
100
+    private var statusBadge: some View {
101
+        Text(meter.operationalState == .dataIsAvailable ? "Live" : "Waiting")
102
+            .font(.caption.weight(.semibold))
103
+            .padding(.horizontal, 10)
104
+            .padding(.vertical, 6)
105
+            .foregroundColor(meter.operationalState == .dataIsAvailable ? .green : .secondary)
106
+            .meterCard(
107
+                tint: meter.operationalState == .dataIsAvailable ? .green : .secondary,
108
+                fillOpacity: 0.12,
109
+                strokeOpacity: 0.16,
110
+                cornerRadius: 999
111
+            )
112
+    }
113
+
114
+    private var shouldShowSecondaryDetails: Bool {
115
+        meter.loadResistance > 0 || shouldShowChargerType
116
+    }
117
+
118
+    private var shouldShowChargerType: Bool {
119
+        meter.supportsChargerDetection && meter.chargerTypeDescription != "Unknown"
120
+    }
121
+
122
+    private func liveMetricCard(
123
+        title: String,
124
+        symbol: String,
125
+        color: Color,
126
+        value: String,
127
+        range: String?
128
+    ) -> some View {
129
+        VStack(alignment: .leading, spacing: 10) {
130
+            HStack {
131
+                Image(systemName: symbol)
132
+                    .font(.system(size: 15, weight: .semibold))
133
+                    .foregroundColor(color)
134
+                    .frame(width: 34, height: 34)
135
+                    .background(Circle().fill(color.opacity(0.12)))
136
+                Spacer()
137
+            }
138
+
139
+            Text(title)
140
+                .font(.subheadline.weight(.semibold))
141
+                .foregroundColor(.secondary)
142
+
143
+            Text(value)
144
+                .font(.system(.title3, design: .rounded).weight(.bold))
145
+                .monospacedDigit()
146
+
147
+            if let range, !range.isEmpty {
148
+                Text(range)
149
+                    .font(.caption)
150
+                    .foregroundColor(.secondary)
151
+                    .lineLimit(2)
152
+            }
153
+        }
154
+        .frame(maxWidth: .infinity, minHeight: 128, alignment: .leading)
155
+        .padding(16)
156
+        .meterCard(tint: color, fillOpacity: 0.10, strokeOpacity: 0.12)
157
+    }
158
+
159
+    private func secondaryDetailRow(
160
+        title: String,
161
+        value: String,
162
+        symbol: String,
163
+        color: Color
164
+    ) -> some View {
165
+        HStack(spacing: 12) {
166
+            Image(systemName: symbol)
167
+                .foregroundColor(color)
168
+                .frame(width: 28)
169
+            Text(title)
170
+                .foregroundColor(.secondary)
171
+            Spacer()
172
+            Text(value)
173
+                .fontWeight(.semibold)
174
+                .multilineTextAlignment(.trailing)
175
+        }
176
+        .font(.footnote)
177
+    }
178
+
179
+    private func rangeText(min: Double, max: Double, unit: String) -> String? {
180
+        guard min.isFinite, max.isFinite else { return nil }
181
+        return "Min \(min.format(decimalDigits: 3)) \(unit)  Max \(max.format(decimalDigits: 3)) \(unit)"
182
+    }
66 183
 }
+36 -25
USB Meter/Views/Meter/Measurements/MeasurementPointView.swift
@@ -17,42 +17,53 @@ struct MeasurementPointView: View {
17 17
     @State var showDetail: Bool = false
18 18
     
19 19
     var body: some View {
20
-        VStack {
21
-            HStack {
22
-                Image(systemName: "pencil.and.ellipsis.rectangle")
23
-                    .imageScale(.large)
24
-                Text ("\(voltage.timestamp.format(as: "yyyy-MM-dd hh:mm:ss"))")
20
+        VStack(alignment: .leading, spacing: 12) {
21
+            HStack(spacing: 12) {
22
+                Image(systemName: "waveform.path.ecg.rectangle.fill")
23
+                    .foregroundColor(.blue)
24
+                    .frame(width: 36, height: 36)
25
+                    .background(Circle().fill(Color.blue.opacity(0.16)))
26
+                VStack(alignment: .leading, spacing: 3) {
27
+                    Text(voltage.timestamp.format(as: "yyyy-MM-dd HH:mm:ss"))
28
+                        .font(.subheadline.weight(.semibold))
29
+                    Text("Captured sample")
30
+                        .font(.caption)
31
+                        .foregroundColor(.secondary)
32
+                }
25 33
                 Spacer()
26 34
                 Button(action: {
27
-                    self.showDetail.toggle()
35
+                    showDetail.toggle()
28 36
                 }) {
29
-                    Image(systemName: "chevron.right.circle")
37
+                    Image(systemName: "chevron.right.circle.fill")
30 38
                         .imageScale(.large)
39
+                        .foregroundColor(.secondary)
31 40
                         .rotationEffect(.degrees(showDetail ? 90 : 0))
32 41
                         .animation(.easeInOut(duration: 0.25), value: showDetail)
33 42
                 }
43
+                .buttonStyle(.plain)
34 44
             }
45
+
35 46
             if showDetail {
36
-                VStack {
37
-                    HStack {
38
-                        Text("Power:")
39
-                        Spacer()
40
-                        Text("\(power.value.format(fractionDigits: 4))")
41
-                    }
42
-                    HStack {
43
-                        Text("Voltage:")
44
-                        Spacer()
45
-                        Text("\(voltage.value.format(fractionDigits: 4))")
46
-                    }
47
-                    HStack {
48
-                        Text("Current:")
49
-                        Spacer()
50
-                        Text("\(current.value.format(fractionDigits: 4))")
51
-                    }
47
+                VStack(spacing: 10) {
48
+                    detailRow(title: "Power", value: "\(power.value.format(fractionDigits: 4)) W")
49
+                    detailRow(title: "Voltage", value: "\(voltage.value.format(fractionDigits: 4)) V")
50
+                    detailRow(title: "Current", value: "\(current.value.format(fractionDigits: 4)) A")
52 51
                 }
53
-                .padding()
54 52
             }
55 53
         }
56
-        
54
+        .padding(16)
55
+        .meterCard(tint: .blue, fillOpacity: 0.14, strokeOpacity: 0.20, cornerRadius: 18)
56
+    }
57
+
58
+    private func detailRow(title: String, value: String) -> some View {
59
+        HStack {
60
+            Text(title)
61
+                .foregroundColor(.secondary)
62
+            Spacer()
63
+            Text(value)
64
+                .fontWeight(.semibold)
65
+                .monospacedDigit()
66
+        }
67
+        .font(.footnote)
57 68
     }
58 69
 }
+44 -33
USB Meter/Views/Meter/Measurements/MeasurementsView.swift
@@ -16,43 +16,54 @@ struct MeasurementsView: View {
16 16
     
17 17
     var body: some View {
18 18
         NavigationView {
19
-            VStack {
20
-                Text("Local timeline captured by the app while connected to the meter.")
21
-                    .font(.footnote)
22
-                    .foregroundColor(.secondary)
23
-                    .multilineTextAlignment(.center)
24
-                    .padding(.horizontal)
25
-                    .padding(.top, 8)
26
-                List {
27
-                    ForEach (measurements.power.points) { point in
28
-                        // MARK: Crapa la stergere daca lista incape in fereastra:  Fatal error: Index out of range
29
-                        MeasurementPointView(power: point, voltage: self.measurements.voltage.points[point.id], current: self.measurements.current.points[point.id])
30
-                    }.onDelete { (indexSet) in
31
-                        for idx in indexSet {
32
-                            self.measurements.remove(at: idx)
19
+            ScrollView {
20
+                VStack(alignment: .leading, spacing: 14) {
21
+                    VStack(alignment: .leading, spacing: 8) {
22
+                        Text("App History")
23
+                            .font(.system(.title3, design: .rounded).weight(.bold))
24
+                        Text("Local timeline captured by the app while connected to the meter.")
25
+                            .font(.footnote)
26
+                            .foregroundColor(.secondary)
27
+                    }
28
+                    .padding(18)
29
+                    .meterCard(tint: .blue, fillOpacity: 0.18, strokeOpacity: 0.24)
30
+
31
+                    if measurements.power.points.isEmpty {
32
+                        Text("No history samples have been captured yet.")
33
+                            .font(.footnote)
34
+                            .foregroundColor(.secondary)
35
+                            .padding(18)
36
+                            .meterCard(tint: .secondary, fillOpacity: 0.14, strokeOpacity: 0.20)
37
+                    } else {
38
+                        LazyVStack(spacing: 12) {
39
+                            ForEach(measurements.power.points) { point in
40
+                                MeasurementPointView(
41
+                                    power: point,
42
+                                    voltage: measurements.voltage.points[point.id],
43
+                                    current: measurements.current.points[point.id]
44
+                                )
45
+                            }
33 46
                         }
34 47
                     }
35 48
                 }
49
+                .padding()
36 50
             }
37
-            .navigationBarItems(leading: HStack{
38
-                    Button(action: {self.visibility.toggle()}) {
39
-                        Text("Done")
40
-                    }
41
-                },
42
-                trailing: HStack{
43
-                    #if targetEnvironment(macCatalyst)
44
-                    EditButton()
45
-                    #endif
46
-                    Button(action: {
47
-                        self.measurements.power.reset()
48
-                        self.measurements.voltage.reset()
49
-                        self.measurements.current.reset()
50
-                    }) {
51
-                        Text("🗑")
52
-                            .foregroundColor(.red)
53
-                    }
54
-                })
55
-                .navigationBarTitle("App History", displayMode: .inline)
51
+            .background(
52
+                LinearGradient(
53
+                    colors: [.blue.opacity(0.14), Color.clear],
54
+                    startPoint: .topLeading,
55
+                    endPoint: .bottomTrailing
56
+                )
57
+                .ignoresSafeArea()
58
+            )
59
+            .navigationBarItems(
60
+                leading: Button("Done") { visibility.toggle() },
61
+                trailing: Button("Clear") {
62
+                    measurements.reset()
63
+                }
64
+                .foregroundColor(.red)
65
+            )
66
+            .navigationBarTitle("App History", displayMode: .inline)
56 67
         }
57 68
         .navigationViewStyle(StackNavigationViewStyle())
58 69
     }
+42 -34
USB Meter/Views/Meter/MeterSettingsView.swift
@@ -18,28 +18,26 @@ struct MeterSettingsView: View {
18 18
 
19 19
     var body: some View {
20 20
         ScrollView {
21
-            VStack (spacing: 10) {
22
-                // MARK: Name
23
-                VStack {
21
+            VStack (spacing: 14) {
22
+                settingsCard(title: "Name", tint: meter.color) {
24 23
                     HStack {
25
-                        Text ("Name").fontWeight(.semibold)
26 24
                         Spacer()
27 25
                         if !editingName {
28 26
                             Text(meter.name)
27
+                                .foregroundColor(.secondary)
29 28
                         }
30
-                        ChevronView( rotate: $editingName )
29
+                        ChevronView(rotate: $editingName)
31 30
                     }
32 31
                     if editingName {
33 32
                         EditNameView(editingName: $editingName, newName: meter.name)
34 33
                     }
35 34
                 }
36
-                .padding()
37
-                .background(RoundedRectangle(cornerRadius: 15).foregroundColor(.secondary).opacity(0.1))
35
+
38 36
                 if meter.operationalState == .dataIsAvailable {
39
-                    VStack(alignment: .leading, spacing: 8) {
40
-                        Text("Device Info").fontWeight(.semibold)
37
+                    settingsCard(title: "Device Info", tint: .blue) {
41 38
                         DeviceInfoRow(label: "Advertised Model", value: meter.modelString)
42 39
                         DeviceInfoRow(label: "Displayed Model", value: meter.deviceModelSummary)
40
+                        DeviceInfoRow(label: "Working Voltage", value: meter.documentedWorkingVoltage)
43 41
                         DeviceInfoRow(label: "Temperature Unit", value: meter.temperatureUnitDescription)
44 42
                         if meter.modelNumber != 0 {
45 43
                             DeviceInfoRow(label: "Model Code", value: "\(meter.modelNumber)")
@@ -54,12 +52,10 @@ struct MeterSettingsView: View {
54 52
                             DeviceInfoRow(label: "Boot Count", value: "\(meter.bootCount)")
55 53
                         }
56 54
                     }
57
-                    .padding()
58
-                    .background(RoundedRectangle(cornerRadius: 15).foregroundColor(.secondary).opacity(0.1))
59 55
                 }
56
+
60 57
                 if meter.operationalState == .dataIsAvailable && meter.supportsManualTemperatureUnitSelection {
61
-                    VStack(alignment: .leading, spacing: 8) {
62
-                        Text("Temperature Unit").fontWeight(.semibold)
58
+                    settingsCard(title: "Temperature Unit", tint: .orange) {
63 59
                         Text("TC66 reports temperature using the unit selected on the device. Keep this setting matched to the meter.")
64 60
                             .font(.footnote)
65 61
                             .foregroundColor(.secondary)
@@ -70,50 +66,65 @@ struct MeterSettingsView: View {
70 66
                         }
71 67
                         .pickerStyle(SegmentedPickerStyle())
72 68
                     }
73
-                    .padding()
74
-                    .background(RoundedRectangle(cornerRadius: 15).foregroundColor(.secondary).opacity(0.1))
75 69
                 }
70
+
76 71
                 if meter.operationalState == .dataIsAvailable && meter.supportsUMSettings {
77
-                    // MARK: Screen Timeout
78
-                    // Ar trebui separat enabled/disabled de valorile in minute eventual stocata valoarea in iCloud la dezactivare pentru restaurare
79
-                    VStack{
72
+                    settingsCard(title: "Screen Timeout", tint: .purple) {
80 73
                         HStack {
81
-                            Text ("Screen Timeout").fontWeight(.semibold)
82 74
                             Spacer()
83 75
                             if !editingScreenTimeout {
84
-                                Text ( meter.screenTimeout != 0 ? "\(meter.screenTimeout) Minutes" : "Off" )
76
+                                Text(meter.screenTimeout != 0 ? "\(meter.screenTimeout) Minutes" : "Off")
77
+                                    .foregroundColor(.secondary)
85 78
                             }
86
-                            ChevronView( rotate: $editingScreenTimeout )
79
+                            ChevronView(rotate: $editingScreenTimeout)
87 80
                         }
88 81
                         if editingScreenTimeout {
89 82
                             EditScreenTimeoutView()
90 83
                         }
91 84
                     }
92
-                    .padding()
93
-                    .background(RoundedRectangle(cornerRadius: 15).foregroundColor(.secondary).opacity(0.1))
94
-                    // MARK: Screen Brightness
95
-                    VStack{
85
+
86
+                    settingsCard(title: "Screen Brightness", tint: .yellow) {
96 87
                         HStack {
97
-                            Text ("Screen Brightness").fontWeight(.semibold)
98 88
                             Spacer()
99 89
                             if !editingScreenBrightness {
100
-                                Text ( "\(meter.screenBrightness)" )
90
+                                Text("\(meter.screenBrightness)")
91
+                                    .foregroundColor(.secondary)
101 92
                             }
102
-                            ChevronView( rotate: $editingScreenBrightness )
93
+                            ChevronView(rotate: $editingScreenBrightness)
103 94
                         }
104 95
                         if editingScreenBrightness {
105 96
                             EditScreenBrightnessView()
106 97
                         }
107 98
                     }
108
-                    .padding()
109
-                    .background(RoundedRectangle(cornerRadius: 15).foregroundColor(.secondary).opacity(0.1))
110 99
                 }
111 100
             }
101
+            .padding()
112 102
         }
113
-        .padding()
103
+        .background(
104
+            LinearGradient(
105
+                colors: [meter.color.opacity(0.14), Color.clear],
106
+                startPoint: .topLeading,
107
+                endPoint: .bottomTrailing
108
+            )
109
+            .ignoresSafeArea()
110
+        )
114 111
         .navigationBarTitle("Meter Settings")
115 112
         .navigationBarItems( trailing: RSSIView( RSSI: meter.btSerial.RSSI ).frame( width: 24 ) )
116 113
     }
114
+
115
+    private func settingsCard<Content: View>(
116
+        title: String,
117
+        tint: Color,
118
+        @ViewBuilder content: () -> Content
119
+    ) -> some View {
120
+        VStack(alignment: .leading, spacing: 12) {
121
+            Text(title)
122
+                .font(.headline)
123
+            content()
124
+        }
125
+        .padding(18)
126
+        .meterCard(tint: tint, fillOpacity: 0.18, strokeOpacity: 0.24)
127
+    }
117 128
 }
118 129
 
119 130
 private struct DeviceInfoRow: View {
@@ -148,7 +159,6 @@ struct EditNameView: View {
148 159
             .lineLimit(1)
149 160
             .disableAutocorrection(true)
150 161
             .multilineTextAlignment(.center)
151
-            .padding(.horizontal)
152 162
     }
153 163
 }
154 164
 
@@ -170,7 +180,6 @@ struct EditScreenTimeoutView: View {
170 180
             Text("Off").tag(0)
171 181
         }
172 182
         .pickerStyle( SegmentedPickerStyle() )
173
-        .padding(.horizontal)
174 183
     }
175 184
 }
176 185
 
@@ -188,6 +197,5 @@ struct EditScreenBrightnessView: View {
188 197
             Text("5").tag(5)
189 198
         }
190 199
         .pickerStyle( SegmentedPickerStyle() )
191
-        .padding(.horizontal)
192 200
     }
193 201
 }
+187 -76
USB Meter/Views/Meter/MeterView.swift
@@ -19,68 +19,50 @@ struct MeterView: View {
19 19
     @State var measurementsViewVisibility: Bool = false
20 20
     private var myBounds: CGRect { UIScreen.main.bounds }
21 21
 
22
+    private let actionColumns = [
23
+        GridItem(.adaptive(minimum: 112, maximum: 180), spacing: 12)
24
+    ]
25
+
22 26
     var body: some View {
23 27
         ScrollView {
24
-            VStack (alignment: .center, spacing: 10) {
25
-                // MARK: Name
26
-                VStack {
27
-                    Text("Meter")
28
-                        .font(.headline)
29
-                    Text("\(meter.name)")
30
-                }
31
-                .padding()
32
-                .background(RoundedRectangle(cornerRadius: 20).foregroundColor(meter.color).opacity(0.1))
33
-                // MARK: Mac
34
-                VStack {
35
-                    Text("MAC")
36
-                        .font(.headline)
37
-                    Text("\(meter.btSerial.macAddress.description)")
38
-                }
39
-                .padding()
40
-                .background(RoundedRectangle(cornerRadius: 20).foregroundColor(.secondary).opacity(0.1))
41
-            }
42
-            // MARK: Connect/Disconnect
43
-            connectionControlButton()
44
-            // MARK: Show Data
45
-            if ( meter.operationalState ==  .dataIsAvailable) {
46
-                Text("Model: \(meter.deviceModelSummary)")
47
-                HStack(spacing: 24) {
48
-                    meterSheetButton(icon: "map", title: meter.dataGroupsTitle) {
49
-                        dataGroupsViewVisibility.toggle()
50
-                    }
51
-                    .sheet(isPresented: self.$dataGroupsViewVisibility) {
52
-                        DataGroupsView(visibility: self.$dataGroupsViewVisibility)
53
-                            .environmentObject(self.meter)
54
-                    }
28
+            VStack(alignment: .leading, spacing: 16) {
29
+                headerCard
30
+                connectionControlButton()
55 31
 
56
-                    if meter.supportsRecordingView {
57
-                        meterSheetButton(icon: "record.circle", title: "Charge Record") {
58
-                            recordingViewVisibility.toggle()
59
-                        }
60
-                        .sheet(isPresented: self.$recordingViewVisibility) {
61
-                            RecordingView(visibility: self.$recordingViewVisibility)
62
-                                .environmentObject(self.meter)
63
-                        }
64
-                    }
32
+                if meter.operationalState == .dataIsAvailable {
33
+                    actionGrid
65 34
 
66
-                    meterSheetButton(icon: "recordingtape", title: "App History") {
67
-                        measurementsViewVisibility.toggle()
68
-                    }
69
-                    .sheet(isPresented: self.$measurementsViewVisibility) {
70
-                        MeasurementsView(visibility: self.$measurementsViewVisibility)
71
-                            .environmentObject(self.meter.measurements)
35
+                    if meter.measurements.power.context.isValid {
36
+                        MeasurementChartView()
37
+                            .environmentObject(meter.measurements)
38
+                            .frame(minHeight: myBounds.height / 3.4)
39
+                            .padding(16)
40
+                            .meterCard(tint: meter.color, fillOpacity: 0.12, strokeOpacity: 0.20)
72 41
                     }
42
+
43
+                    ControlView()
44
+                        .padding(16)
45
+                        .meterCard(tint: .indigo, fillOpacity: 0.12, strokeOpacity: 0.20)
46
+
47
+                    LiveView()
48
+                        .padding(18)
49
+                        .meterCard(tint: meter.color, fillOpacity: 0.12, strokeOpacity: 0.20)
73 50
                 }
74
-                if self.meter.measurements.power.context.isValid {
75
-                    MeasurementChartView()
76
-                    .environmentObject(self.meter.measurements)
77
-                        .frame(minHeight: myBounds.height/3)
78
-                }
79
-                ControlView()
80
-                LiveView()
81 51
             }
52
+            .padding()
82 53
         }
83
-        //.frame(minWidth: 0, maxWidth: .greatestFiniteMagnitude, minHeight: 0, maxHeight: .greatestFiniteMagnitude)
54
+        .background(
55
+            LinearGradient(
56
+                colors: [
57
+                    meter.color.opacity(0.22),
58
+                    Color.secondary.opacity(0.08),
59
+                    Color.clear
60
+                ],
61
+                startPoint: .topLeading,
62
+                endPoint: .bottomTrailing
63
+            )
64
+            .ignoresSafeArea()
65
+        )
84 66
         .navigationBarTitle("Meter")
85 67
         .navigationBarItems(trailing: HStack (spacing: 0) {
86 68
             if meter.operationalState > .notPresent {
@@ -89,50 +71,179 @@ struct MeterView: View {
89 71
                     .padding(.vertical)
90 72
             }
91 73
             NavigationLink(destination: MeterSettingsView().environmentObject(meter)) {
92
-                Image( systemName: "gear" )
93
-                    //.imageScale(.large)
74
+                Image(systemName: "gearshape.fill")
94 75
                     .padding(.vertical)
95 76
                     .padding(.leading)
96
-                //.background(RoundedRectangle(cornerRadius: 20)).opacity(0.05)
97 77
             }
98 78
         })
99 79
     }
80
+
81
+    private var headerCard: some View {
82
+        VStack(alignment: .leading, spacing: 14) {
83
+            HStack(alignment: .top) {
84
+                VStack(alignment: .leading, spacing: 6) {
85
+                    Text(meter.name)
86
+                        .font(.system(.title2, design: .rounded).weight(.bold))
87
+                    Text(meter.deviceModelSummary)
88
+                        .font(.subheadline.weight(.semibold))
89
+                        .foregroundColor(.secondary)
90
+                }
91
+                Spacer()
92
+                VStack(alignment: .trailing, spacing: 6) {
93
+                    Text(statusText)
94
+                        .font(.caption.weight(.bold))
95
+                        .padding(.horizontal, 12)
96
+                        .padding(.vertical, 6)
97
+                        .meterCard(tint: statusColor, fillOpacity: 0.24, strokeOpacity: 0.30, cornerRadius: 999)
98
+                    if meter.operationalState > .notPresent {
99
+                        Text("RSSI \(meter.btSerial.RSSI)")
100
+                            .font(.caption)
101
+                            .foregroundColor(.secondary)
102
+                    }
103
+                }
104
+            }
105
+
106
+            Divider().opacity(0.35)
107
+
108
+            HStack(spacing: 12) {
109
+                headerPill(title: "MAC", value: meter.btSerial.macAddress.description)
110
+                headerPill(title: "Range", value: meter.documentedWorkingVoltage)
111
+            }
112
+        }
113
+        .padding(20)
114
+        .meterCard(tint: meter.color, fillOpacity: 0.22, strokeOpacity: 0.24)
115
+    }
116
+
117
+    private var actionGrid: some View {
118
+        LazyVGrid(columns: actionColumns, spacing: 12) {
119
+            meterSheetButton(icon: "square.grid.2x2.fill", title: meter.dataGroupsTitle, tint: .teal) {
120
+                dataGroupsViewVisibility.toggle()
121
+            }
122
+            .sheet(isPresented: $dataGroupsViewVisibility) {
123
+                DataGroupsView(visibility: $dataGroupsViewVisibility)
124
+                    .environmentObject(meter)
125
+            }
126
+
127
+            if meter.supportsRecordingView {
128
+                meterSheetButton(icon: "gauge.with.dots.needle.50percent", title: "Charge Record", tint: .pink) {
129
+                    recordingViewVisibility.toggle()
130
+                }
131
+                .sheet(isPresented: $recordingViewVisibility) {
132
+                    RecordingView(visibility: $recordingViewVisibility)
133
+                        .environmentObject(meter)
134
+                }
135
+            }
136
+
137
+            meterSheetButton(icon: "clock.arrow.circlepath", title: "App History", tint: .blue) {
138
+                measurementsViewVisibility.toggle()
139
+            }
140
+            .sheet(isPresented: $measurementsViewVisibility) {
141
+                MeasurementsView(visibility: $measurementsViewVisibility)
142
+                    .environmentObject(meter.measurements)
143
+            }
144
+        }
145
+    }
100 146
     
101 147
     fileprivate func connectionControlButton() -> some View {
102
-        /*
103
-         MARK: De adaugat si celelalte situatii
104
-         case peripheralNotConnected
105
-         case peripheralConnectionPending
106
-         case peripheralConnected
107
-         case ready
108
-         */
109
-        let buttonColor = meter.operationalState > .peripheralNotConnected ? Color.red : Color.green
148
+        let connected = meter.operationalState >= .peripheralConnectionPending
149
+        let tint = connected ? Color.red : Color.green
150
+
110 151
         return Group {
111 152
             if meter.operationalState == .notPresent {
112
-                Text("Not found at this time.").foregroundColor(.red)
153
+                HStack(spacing: 10) {
154
+                    Image(systemName: "exclamationmark.triangle.fill")
155
+                        .foregroundColor(.orange)
156
+                    Text("Not found at this time.")
157
+                        .fontWeight(.semibold)
158
+                    Spacer()
159
+                }
160
+                .padding(16)
161
+                .meterCard(tint: .orange, fillOpacity: 0.18, strokeOpacity: 0.24)
113 162
             } else {
114
-                
115
-                HStack {
163
+                Button(action: {
116 164
                     if meter.operationalState < .peripheralConnectionPending {
117
-                        Button (action: { self.meter.connect() } ) { Text("Connect") }
165
+                        meter.connect()
118 166
                     } else {
119
-                        Button (action: { self.meter.disconnect() } ) { Text("Disconnect") }
167
+                        meter.disconnect()
168
+                    }
169
+                }) {
170
+                    HStack {
171
+                        Image(systemName: connected ? "xmark.circle.fill" : "bolt.horizontal.circle.fill")
172
+                        Text(connected ? "Disconnect" : "Connect")
173
+                            .fontWeight(.semibold)
174
+                        Spacer()
175
+                        Text(statusText)
176
+                            .font(.footnote.weight(.medium))
177
+                            .foregroundColor(.secondary)
120 178
                     }
179
+                    .padding(.horizontal, 18)
180
+                    .padding(.vertical, 16)
181
+                    .frame(maxWidth: .infinity)
182
+                    .foregroundColor(tint)
183
+                    .meterCard(tint: tint, fillOpacity: 0.18, strokeOpacity: 0.24)
121 184
                 }
122
-                .padding()
123
-                .background(RoundedRectangle(cornerRadius: 20).foregroundColor(buttonColor).opacity(0.1))
124
-                .frame(maxWidth: .greatestFiniteMagnitude)
185
+                .buttonStyle(.plain)
125 186
             }
126 187
         }
127
-        
128 188
     }
129 189
 
130
-    fileprivate func meterSheetButton(icon: String, title: String, action: @escaping () -> Void) -> some View {
190
+    fileprivate func meterSheetButton(icon: String, title: String, tint: Color, action: @escaping () -> Void) -> some View {
131 191
         Button(action: action) {
132
-            VStack {
192
+            VStack(spacing: 10) {
133 193
                 Image(systemName: icon)
194
+                    .font(.system(size: 20, weight: .semibold))
195
+                    .frame(width: 40, height: 40)
196
+                    .background(Circle().fill(tint.opacity(0.14)))
134 197
                 Text(title)
198
+                    .font(.footnote.weight(.semibold))
199
+                    .multilineTextAlignment(.center)
200
+                    .lineLimit(2)
201
+                    .minimumScaleFactor(0.9)
135 202
             }
203
+            .foregroundColor(tint)
204
+            .frame(maxWidth: .infinity, minHeight: 106)
205
+            .padding(.horizontal, 8)
206
+            .meterCard(tint: tint, fillOpacity: 0.18, strokeOpacity: 0.22)
207
+        }
208
+        .buttonStyle(.plain)
209
+    }
210
+
211
+    private func headerPill(title: String, value: String) -> some View {
212
+        VStack(alignment: .leading, spacing: 4) {
213
+            Text(title)
214
+                .font(.caption.weight(.semibold))
215
+                .foregroundColor(.secondary)
216
+            Text(value)
217
+                .font(.footnote.weight(.semibold))
218
+                .lineLimit(1)
219
+                .minimumScaleFactor(0.8)
136 220
         }
221
+        .frame(maxWidth: .infinity, alignment: .leading)
222
+        .padding(.horizontal, 12)
223
+        .padding(.vertical, 10)
224
+        .meterCard(tint: meter.color, fillOpacity: 0.16, strokeOpacity: 0.20, cornerRadius: 16)
225
+    }
226
+
227
+    private var statusText: String {
228
+        switch meter.operationalState {
229
+        case .notPresent:
230
+            return "Missing"
231
+        case .peripheralNotConnected:
232
+            return "Ready"
233
+        case .peripheralConnectionPending:
234
+            return "Connecting"
235
+        case .peripheralConnected:
236
+            return "Linked"
237
+        case .peripheralReady:
238
+            return "Preparing"
239
+        case .comunicating:
240
+            return "Syncing"
241
+        case .dataIsAvailable:
242
+            return "Live"
243
+        }
244
+    }
245
+
246
+    private var statusColor: Color {
247
+        Meter.operationalColor(for: meter.operationalState)
137 248
     }
138 249
 }
+52 -18
USB Meter/Views/Meter/RecordingView.swift
@@ -17,15 +17,30 @@ struct RecordingView: View {
17 17
         NavigationView {
18 18
             ScrollView {
19 19
                 VStack(spacing: 16) {
20
-                    VStack(spacing: 6) {
21
-                        Text("Charge Record")
22
-                            .font(.headline)
23
-                        Text(usbMeter.chargeRecordStatusText)
24
-                            .foregroundColor(usbMeter.chargeRecordStatusColor)
20
+                    VStack(alignment: .leading, spacing: 8) {
21
+                        HStack {
22
+                            Text("Charge Record")
23
+                                .font(.system(.title3, design: .rounded).weight(.bold))
24
+                            Spacer()
25
+                            Text(usbMeter.chargeRecordStatusText)
26
+                                .font(.caption.weight(.bold))
27
+                                .foregroundColor(usbMeter.chargeRecordStatusColor)
28
+                                .padding(.horizontal, 10)
29
+                                .padding(.vertical, 6)
30
+                                .meterCard(
31
+                                    tint: usbMeter.chargeRecordStatusColor,
32
+                                    fillOpacity: 0.18,
33
+                                    strokeOpacity: 0.24,
34
+                                    cornerRadius: 999
35
+                                )
36
+                        }
37
+                        Text("App-side charge accumulation based on the stop-threshold workflow.")
38
+                            .font(.footnote)
39
+                            .foregroundColor(.secondary)
25 40
                     }
26 41
                     .frame(maxWidth: .infinity)
27
-                    .padding()
28
-                    .background(RoundedRectangle(cornerRadius: 16).foregroundColor(.secondary).opacity(0.1))
42
+                    .padding(18)
43
+                    .meterCard(tint: .pink, fillOpacity: 0.18, strokeOpacity: 0.24)
29 44
 
30 45
                     HStack(alignment: .top) {
31 46
                         VStack(alignment: .leading, spacing: 10) {
@@ -41,19 +56,22 @@ struct RecordingView: View {
41 56
                             Text(usbMeter.chargeRecordDurationDescription)
42 57
                             Text("\(usbMeter.chargeRecordStopThreshold.format(decimalDigits: 2)) A")
43 58
                         }
59
+                        .monospacedDigit()
44 60
                     }
45
-                    .padding()
46
-                    .background(RoundedRectangle(cornerRadius: 16).foregroundColor(.secondary).opacity(0.1))
61
+                    .font(.footnote.weight(.semibold))
62
+                    .padding(18)
63
+                    .meterCard(tint: .pink, fillOpacity: 0.14, strokeOpacity: 0.20)
47 64
 
48 65
                     if usbMeter.chargeRecordTimeRange != nil {
49 66
                         VStack(alignment: .leading, spacing: 12) {
50 67
                             HStack {
51 68
                                 Text("Charge Curve")
52
-                                    .fontWeight(.semibold)
69
+                                    .font(.headline)
53 70
                                 Spacer()
54 71
                                 Button("Reset Graph") {
55 72
                                     usbMeter.resetChargeRecordGraph()
56 73
                                 }
74
+                                .foregroundColor(.red)
57 75
                             }
58 76
                             MeasurementChartView(timeRange: usbMeter.chargeRecordTimeRange)
59 77
                                 .environmentObject(usbMeter.measurements)
@@ -62,13 +80,13 @@ struct RecordingView: View {
62 80
                                 .font(.footnote)
63 81
                                 .foregroundColor(.secondary)
64 82
                         }
65
-                        .padding()
66
-                        .background(RoundedRectangle(cornerRadius: 16).foregroundColor(.secondary).opacity(0.1))
83
+                        .padding(18)
84
+                        .meterCard(tint: .blue, fillOpacity: 0.14, strokeOpacity: 0.20)
67 85
                     }
68 86
 
69 87
                     VStack(alignment: .leading, spacing: 12) {
70 88
                         Text("Stop Threshold")
71
-                            .fontWeight(.semibold)
89
+                            .font(.headline)
72 90
                         Slider(value: $usbMeter.chargeRecordStopThreshold, in: 0...0.30, step: 0.01)
73 91
                         Text("The app starts accumulating when current rises above this threshold and stops when it falls back to or below it.")
74 92
                             .font(.footnote)
@@ -77,14 +95,17 @@ struct RecordingView: View {
77 95
                             usbMeter.resetChargeRecord()
78 96
                         }
79 97
                         .frame(maxWidth: .infinity)
98
+                        .padding(.vertical, 10)
99
+                        .meterCard(tint: .orange, fillOpacity: 0.16, strokeOpacity: 0.22, cornerRadius: 14)
100
+                        .buttonStyle(.plain)
80 101
                     }
81
-                    .padding()
82
-                    .background(RoundedRectangle(cornerRadius: 16).foregroundColor(.secondary).opacity(0.1))
102
+                    .padding(18)
103
+                    .meterCard(tint: .orange, fillOpacity: 0.14, strokeOpacity: 0.20)
83 104
 
84 105
                     if usbMeter.supportsDataGroupCommands || usbMeter.recordedAH > 0 || usbMeter.recordedWH > 0 || usbMeter.recordingDuration > 0 {
85 106
                         VStack(alignment: .leading, spacing: 12) {
86 107
                             Text("Meter Totals")
87
-                                .fontWeight(.semibold)
108
+                                .font(.headline)
88 109
                             HStack(alignment: .top) {
89 110
                                 VStack(alignment: .leading, spacing: 10) {
90 111
                                     Text("Capacity")
@@ -103,7 +124,9 @@ struct RecordingView: View {
103 124
                                         Text("Read-only")
104 125
                                     }
105 126
                                 }
127
+                                .monospacedDigit()
106 128
                             }
129
+                            .font(.footnote.weight(.semibold))
107 130
                             Text("These values are reported by the meter for the active data group.")
108 131
                                 .font(.footnote)
109 132
                                 .foregroundColor(.secondary)
@@ -112,14 +135,25 @@ struct RecordingView: View {
112 135
                                     usbMeter.clear()
113 136
                                 }
114 137
                                 .frame(maxWidth: .infinity)
138
+                                .padding(.vertical, 10)
139
+                                .meterCard(tint: .red, fillOpacity: 0.16, strokeOpacity: 0.22, cornerRadius: 14)
140
+                                .buttonStyle(.plain)
115 141
                             }
116 142
                         }
117
-                        .padding()
118
-                        .background(RoundedRectangle(cornerRadius: 16).foregroundColor(.secondary).opacity(0.1))
143
+                        .padding(18)
144
+                        .meterCard(tint: .teal, fillOpacity: 0.14, strokeOpacity: 0.20)
119 145
                     }
120 146
                 }
121 147
                 .padding()
122 148
             }
149
+            .background(
150
+                LinearGradient(
151
+                    colors: [.pink.opacity(0.14), Color.clear],
152
+                    startPoint: .topLeading,
153
+                    endPoint: .bottomTrailing
154
+                )
155
+                .ignoresSafeArea()
156
+            )
123 157
             .navigationBarTitle("Charge Record", displayMode: .inline)
124 158
             .navigationBarItems(trailing: Button("Done") { visibility.toggle() })
125 159
         }
+54 -4
USB Meter/Views/MeterRowView.swift
@@ -13,10 +13,60 @@ struct MeterRowView: View {
13 13
     @EnvironmentObject private var meter: Meter
14 14
     
15 15
     var body: some View {
16
-        HStack {
17
-            Image( systemName: "antenna.radiowaves.left.and.right" ).foregroundColor(Meter.operationalColor(for: meter.operationalState) )
18
-            Text("\(meter.name)")
19
-//            Spacer()
16
+        HStack(spacing: 14) {
17
+            Image(systemName: "sensor.tag.radiowaves.forward.fill")
18
+                .font(.system(size: 18, weight: .semibold))
19
+                .foregroundColor(Meter.operationalColor(for: meter.operationalState))
20
+                .frame(width: 42, height: 42)
21
+                .background(
22
+                    Circle()
23
+                        .fill(Meter.operationalColor(for: meter.operationalState).opacity(0.18))
24
+                )
25
+
26
+            VStack(alignment: .leading, spacing: 4) {
27
+                Text(meter.name)
28
+                    .font(.headline)
29
+                Text(meter.deviceModelSummary)
30
+                    .font(.caption)
31
+                    .foregroundColor(.secondary)
32
+            }
33
+
34
+            Spacer()
35
+
36
+            VStack(alignment: .trailing, spacing: 4) {
37
+                Text(statusText)
38
+                    .font(.caption.weight(.semibold))
39
+                    .foregroundColor(Meter.operationalColor(for: meter.operationalState))
40
+                Text(meter.btSerial.macAddress.description)
41
+                    .font(.caption2)
42
+                    .foregroundColor(.secondary)
43
+            }
44
+        }
45
+        .padding(14)
46
+        .meterCard(
47
+            tint: Meter.operationalColor(for: meter.operationalState),
48
+            fillOpacity: 0.16,
49
+            strokeOpacity: 0.22,
50
+            cornerRadius: 18
51
+        )
52
+    }
53
+
54
+    private var statusText: String {
55
+        switch meter.operationalState {
56
+        case .notPresent:
57
+            return "Missing"
58
+        case .peripheralNotConnected:
59
+            return "Available"
60
+        case .peripheralConnectionPending:
61
+            return "Connecting"
62
+        case .peripheralConnected:
63
+            return "Linked"
64
+        case .peripheralReady:
65
+            return "Ready"
66
+        case .comunicating:
67
+            return "Syncing"
68
+        case .dataIsAvailable:
69
+            return "Live"
20 70
         }
21 71
     }
22 72
 }