USB-Meter / USB Meter / Views / MeterDetailView.swift
1 contributor
300 lines | 11.9kb
import SwiftUI

struct MeterDetailView: View {
    @EnvironmentObject private var appData: AppData
    @Environment(\.dismiss) private var dismiss
    @State private var editorVisibility = false
    @State private var deleteConfirmationVisibility = false

    let meterSummary: AppData.MeterSummary

    var body: some View {
        ScrollView {
            VStack(spacing: 18) {
                headerCard
                statusCard
                identifiersCard
                chargedDevicesCard
                chargersCard
            }
            .padding()
        }
        .background(
            LinearGradient(
                colors: [meterSummary.tint.opacity(0.18), Color.clear],
                startPoint: .topLeading,
                endPoint: .bottomTrailing
            )
            .ignoresSafeArea()
        )
        .navigationTitle(meterSummary.displayName)
        .toolbar {
            ToolbarItemGroup(placement: .primaryAction) {
                Button("Edit") {
                    editorVisibility = true
                }
                Button(role: .destructive) {
                    deleteConfirmationVisibility = true
                } label: {
                    Image(systemName: "trash")
                }
            }
        }
        .sheet(isPresented: $editorVisibility) {
            MeterEditorSheetView(existingMeterSummary: meterSummary)
                .environmentObject(appData)
        }
        .alert("Delete Meter?", isPresented: $deleteConfirmationVisibility) {
            Button("Delete", role: .destructive) {
                if appData.deleteMeter(macAddress: meterSummary.macAddress) {
                    dismiss()
                }
            }
            Button("Cancel", role: .cancel) {}
        } message: {
            Text("This removes the stored meter entry and its saved metadata from the sidebar until the meter is discovered again.")
        }
    }

    private var headerCard: some View {
        VStack(alignment: .leading, spacing: 8) {
            Text(meterSummary.displayName)
                .font(.title2.weight(.semibold))
            Text(meterSummary.modelSummary)
                .font(.subheadline)
                .foregroundColor(.secondary)
            if let advertisedName = meterSummary.advertisedName {
                Text("Advertised as " + advertisedName)
                    .font(.caption2)
                    .foregroundColor(.secondary)
            }
        }
        .frame(maxWidth: .infinity, alignment: .leading)
        .padding(18)
        .meterCard(tint: meterSummary.tint, fillOpacity: 0.22, strokeOpacity: 0.28, cornerRadius: 20)
    }

    private var statusCard: some View {
        VStack(alignment: .leading, spacing: 10) {
            Text("Status")
                .font(.headline)
            HStack(spacing: 8) {
                Circle()
                    .fill(meterSummary.tint)
                    .frame(width: 10, height: 10)
                Text("Offline")
                    .font(.caption.weight(.semibold))
                    .foregroundColor(.secondary)
            }
            Text("The meter is not currently connected. Bring it within Bluetooth range or wake it up to open live diagnostics.")
                .font(.caption)
                .foregroundColor(.secondary)
        }
        .frame(maxWidth: .infinity, alignment: .leading)
        .padding(18)
        .meterCard(tint: meterSummary.tint, fillOpacity: 0.16, strokeOpacity: 0.22, cornerRadius: 18)
    }

    private var identifiersCard: some View {
        VStack(alignment: .leading, spacing: 10) {
            Text("Identifiers")
                .font(.headline)
            infoRow(label: "MAC Address", value: meterSummary.macAddress)
            if let advertisedName = meterSummary.advertisedName {
                infoRow(label: "Advertised as", value: advertisedName)
            }
        }
        .frame(maxWidth: .infinity, alignment: .leading)
        .padding(18)
        .meterCard(tint: .secondary, fillOpacity: 0.12, strokeOpacity: 0.18, cornerRadius: 18)
    }

    private var chargedDevicesCard: some View {
        let chargedDevices = appData.chargedDevices(for: meterSummary.macAddress)

        return VStack(alignment: .leading, spacing: 10) {
            Text("Devices")
                .font(.headline)

            if chargedDevices.isEmpty {
                Text("No devices are linked to this meter yet. Connect it, open Charge Record, and select the device being charged to start learning capacity and charge curves.")
                    .font(.caption)
                    .foregroundColor(.secondary)
            } else {
                ForEach(chargedDevices.prefix(3)) { chargedDevice in
                    NavigationLink(destination: ChargedDeviceDetailView(chargedDeviceID: chargedDevice.id)) {
                        HStack(spacing: 12) {
                            ChargedDeviceQRCodeView(qrIdentifier: chargedDevice.qrIdentifier, side: 52)

                            VStack(alignment: .leading, spacing: 4) {
                                Label(chargedDevice.name, systemImage: chargedDevice.deviceClass.symbolName)
                                    .font(.subheadline.weight(.semibold))
                                Text(chargedDevice.estimatedBatteryCapacityWh.map { "\($0.format(decimalDigits: 2)) Wh" } ?? "Capacity: learning")
                                    .font(.caption)
                                    .foregroundColor(.secondary)
                            }

                            Spacer()
                        }
                    }
                    .buttonStyle(.plain)
                }
            }
        }
        .frame(maxWidth: .infinity, alignment: .leading)
        .padding(18)
        .meterCard(tint: .orange, fillOpacity: 0.12, strokeOpacity: 0.18, cornerRadius: 18)
    }

    private var chargersCard: some View {
        let chargers = appData.chargers(for: meterSummary.macAddress)

        return VStack(alignment: .leading, spacing: 10) {
            Text("Chargers")
                .font(.headline)

            if chargers.isEmpty {
                Text("No chargers are linked to this meter yet. Pick one from Charge Record when you monitor a wireless charging session.")
                    .font(.caption)
                    .foregroundColor(.secondary)
            } else {
                ForEach(chargers.prefix(3)) { charger in
                    NavigationLink(destination: ChargedDeviceDetailView(chargedDeviceID: charger.id)) {
                        HStack(spacing: 12) {
                            ChargedDeviceQRCodeView(qrIdentifier: charger.qrIdentifier, side: 52)

                            VStack(alignment: .leading, spacing: 4) {
                                Label(charger.name, systemImage: charger.deviceClass.symbolName)
                                    .font(.subheadline.weight(.semibold))
                                Text(charger.chargerMaximumPowerWatts.map { "Max power: \($0.format(decimalDigits: 2)) W" } ?? "Wireless charger")
                                    .font(.caption)
                                    .foregroundColor(.secondary)
                            }

                            Spacer()
                        }
                    }
                    .buttonStyle(.plain)
                }
            }
        }
        .frame(maxWidth: .infinity, alignment: .leading)
        .padding(18)
        .meterCard(tint: .pink, fillOpacity: 0.12, strokeOpacity: 0.18, cornerRadius: 18)
    }

    private func infoRow(label: String, value: String) -> some View {
        HStack {
            Text(label)
            Spacer()
            Text(value)
                .foregroundColor(.secondary)
                .font(.caption)
        }
    }
}

struct MeterDetailView_Previews: PreviewProvider {
    static var previews: some View {
        MeterDetailView(
            meterSummary: AppData.MeterSummary(
                macAddress: "AA:BB:CC:DD:EE:FF",
                displayName: "Desk Meter",
                modelSummary: "UM25C",
                advertisedName: "UM25C-123",
                lastSeen: Date(),
                lastConnected: Date().addingTimeInterval(-3600),
                meter: nil
            )
        )
        .environmentObject(appData)
    }
}

struct MeterEditorSheetView: View {
    @EnvironmentObject private var appData: AppData
    @Environment(\.dismiss) private var dismiss

    let existingMeterSummary: AppData.MeterSummary?

    @State private var customName: String
    @State private var macAddress: String
    @State private var advertisedName: String
    @State private var selectedModel: Model

    init(existingMeterSummary: AppData.MeterSummary? = nil) {
        self.existingMeterSummary = existingMeterSummary
        _customName = State(initialValue: existingMeterSummary?.displayName ?? "")
        _macAddress = State(initialValue: existingMeterSummary?.macAddress ?? "")
        _advertisedName = State(initialValue: existingMeterSummary?.advertisedName ?? "")
        _selectedModel = State(initialValue: Self.model(for: existingMeterSummary?.modelSummary))
    }

    var body: some View {
        NavigationView {
            Form {
                Section(header: Text("Identity")) {
                    TextField("Display name", text: $customName)
                    TextField("MAC Address", text: $macAddress)
                        .textInputAutocapitalization(.characters)
                        .disableAutocorrection(true)
                        .disabled(existingMeterSummary != nil)

                    Picker("Model", selection: $selectedModel) {
                        ForEach(Model.allCases, id: \.self) { model in
                            Text(model.canonicalName)
                                .tag(model)
                        }
                    }

                    TextField("Advertised name", text: $advertisedName)
                }

                Section {
                    Text("Use the real BLE MAC address format `AA:BB:CC:DD:EE:FF`. Saved meters remain visible in the sidebar even when they are currently offline.")
                        .font(.footnote)
                        .foregroundColor(.secondary)
                }
            }
            .navigationTitle(existingMeterSummary == nil ? "New Meter" : "Edit Meter")
            .navigationBarTitleDisplayMode(.inline)
            .toolbar {
                ToolbarItem(placement: .cancellationAction) {
                    Button("Cancel") {
                        dismiss()
                    }
                }
                ToolbarItem(placement: .confirmationAction) {
                    Button(existingMeterSummary == nil ? "Save" : "Update") {
                        let normalizedMAC = AppData.normalizedMACAddress(macAddress)
                        let didSave = appData.createKnownMeter(
                            macAddress: normalizedMAC,
                            customName: customName,
                            modelName: selectedModel.canonicalName,
                            advertisedName: advertisedName
                        )
                        if didSave {
                            dismiss()
                        }
                    }
                    .disabled(isSaveDisabled)
                }
            }
        }
        .navigationViewStyle(StackNavigationViewStyle())
    }

    private var isSaveDisabled: Bool {
        AppData.isValidMACAddress(AppData.normalizedMACAddress(macAddress)) == false
    }

    private static func model(for summary: String?) -> Model {
        if summary?.contains("UM34C") == true {
            return .UM34C
        }
        if summary?.contains("TC66C") == true {
            return .TC66C
        }
        return .UM25C
    }
}