@@ -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 |
} |
@@ -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 |
- |
|
@@ -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 |
} |
@@ -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.", |
@@ -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 |
} |
@@ -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 |
} |
@@ -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 |
|
@@ -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 |
} |
@@ -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 |
} |
@@ -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 |
} |
@@ -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 |
} |
@@ -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 |
} |
@@ -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 |
} |
@@ -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 |
} |
@@ -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 |
} |
@@ -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 |
} |