1 contributor
257 lines | 9.266kb
//
//  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)"
    }
}