// // LiveView.swift // USB Meter // // Created by Bogdan Timofte on 09/03/2020. // Copyright © 2020 Bogdan Timofte. All rights reserved. // import SwiftUI struct LiveView: View { @EnvironmentObject private var meter: Meter var compactLayout: Bool = false var availableSize: CGSize? = nil var body: some View { VStack(alignment: .leading, spacing: 16) { HStack { Text("Live Data") .font(.headline) Spacer() statusBadge } LazyVGrid(columns: liveMetricColumns, spacing: compactLayout ? 10 : 12) { liveMetricCard( title: "Voltage", symbol: "bolt.fill", color: .green, value: "\(meter.voltage.format(decimalDigits: 3)) V", range: rangeText( min: meter.measurements.voltage.context.minValue, max: meter.measurements.voltage.context.maxValue, unit: "V" ) ) liveMetricCard( title: "Current", symbol: "waveform.path.ecg", color: .blue, value: "\(meter.current.format(decimalDigits: 3)) A", range: rangeText( min: meter.measurements.current.context.minValue, max: meter.measurements.current.context.maxValue, unit: "A" ) ) liveMetricCard( title: "Power", symbol: "flame.fill", color: .pink, value: "\(meter.power.format(decimalDigits: 3)) W", range: rangeText( min: meter.measurements.power.context.minValue, max: meter.measurements.power.context.maxValue, unit: "W" ) ) liveMetricCard( title: "Temperature", symbol: "thermometer.medium", color: .orange, value: meter.primaryTemperatureDescription, range: meter.secondaryTemperatureDescription ) } if compactLayout && usesExpandedCompactLayout { Spacer(minLength: 0) } if shouldShowSecondaryDetails { Group { if compactLayout { HStack(spacing: 12) { if meter.loadResistance > 0 { secondaryDetailChip( title: "Load", value: "\(meter.loadResistance.format(decimalDigits: 1)) \u{2126}", symbol: "cable.connector", color: .yellow ) } if shouldShowChargerType { secondaryDetailChip( title: "Charger", value: meter.chargerTypeDescription, symbol: "bolt.badge.checkmark", color: .purple ) } } } else { VStack(alignment: .leading, spacing: 12) { Text("Details") .font(.subheadline.weight(.semibold)) .foregroundColor(.secondary) if meter.loadResistance > 0 { secondaryDetailRow( title: "Load", value: "\(meter.loadResistance.format(decimalDigits: 1)) \u{2126}", symbol: "cable.connector", color: .yellow ) } if shouldShowChargerType { secondaryDetailRow( title: "Charger", value: meter.chargerTypeDescription, symbol: "bolt.badge.checkmark", color: .purple ) } } } } .padding(compactLayout ? 14 : 18) .meterCard(tint: meter.color, fillOpacity: 0.06, strokeOpacity: 0.10) } } .frame(maxWidth: .infinity, maxHeight: compactLayout ? .infinity : nil, alignment: .topLeading) } private var liveMetricColumns: [GridItem] { if compactLayout { let count = usesExpandedCompactLayout ? 2 : 4 return Array(repeating: GridItem(.flexible(), spacing: 10), count: count) } return [GridItem(.flexible()), GridItem(.flexible())] } private var statusBadge: some View { Text(meter.operationalState == .dataIsAvailable ? "Live" : "Waiting") .font(.caption.weight(.semibold)) .padding(.horizontal, 10) .padding(.vertical, 6) .foregroundColor(meter.operationalState == .dataIsAvailable ? .green : .secondary) .meterCard( tint: meter.operationalState == .dataIsAvailable ? .green : .secondary, fillOpacity: 0.12, strokeOpacity: 0.16, cornerRadius: 999 ) } private var shouldShowSecondaryDetails: Bool { meter.loadResistance > 0 || shouldShowChargerType } private var shouldShowChargerType: Bool { meter.supportsChargerDetection && meter.chargerTypeDescription != "Unknown" } private var usesExpandedCompactLayout: Bool { compactLayout && (availableSize?.height ?? 0) >= 520 } private func liveMetricCard( title: String, symbol: String, color: Color, value: String, range: String? ) -> some View { VStack(alignment: .leading, spacing: 10) { HStack { Image(systemName: symbol) .font(.system(size: compactLayout ? 14 : 15, weight: .semibold)) .foregroundColor(color) .frame(width: compactLayout ? 30 : 34, height: compactLayout ? 30 : 34) .background(Circle().fill(color.opacity(0.12))) Spacer() } Text(title) .font((compactLayout ? Font.caption : .subheadline).weight(.semibold)) .foregroundColor(.secondary) Text(value) .font(.system(compactLayout ? .headline : .title3, design: .rounded).weight(.bold)) .monospacedDigit() if !compactLayout, let range, !range.isEmpty { Text(range) .font(.caption) .foregroundColor(.secondary) .lineLimit(2) } } .frame( maxWidth: .infinity, minHeight: compactLayout ? (usesExpandedCompactLayout ? 128 : 96) : 128, alignment: .leading ) .padding(compactLayout ? 12 : 16) .meterCard(tint: color, fillOpacity: 0.10, strokeOpacity: 0.12) } private func secondaryDetailRow( title: String, value: String, symbol: String, color: Color ) -> some View { HStack(spacing: 12) { Image(systemName: symbol) .foregroundColor(color) .frame(width: 28) Text(title) .foregroundColor(.secondary) Spacer() Text(value) .fontWeight(.semibold) .multilineTextAlignment(.trailing) } .font(.footnote) } private func secondaryDetailChip( title: String, value: String, symbol: String, color: Color ) -> some View { HStack(spacing: 10) { Image(systemName: symbol) .foregroundColor(color) .frame(width: 22, height: 22) .background(Circle().fill(color.opacity(0.12))) VStack(alignment: .leading, spacing: 2) { Text(title) .foregroundColor(.secondary) Text(value) .fontWeight(.semibold) .lineLimit(1) } Spacer(minLength: 0) } .font(.caption) .frame(maxWidth: .infinity, alignment: .leading) } private func rangeText(min: Double, max: Double, unit: String) -> String? { guard min.isFinite, max.isFinite else { return nil } return "Min \(min.format(decimalDigits: 3)) \(unit) Max \(max.format(decimalDigits: 3)) \(unit)" } }