USB-Meter / USB Meter / Views / MeterDetailView.swift
1 contributor
226 lines | 8.39kb
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
            }
            .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) {
            HStack(spacing: 8) {
                Text("Status")
                    .font(.headline)
                ContextInfoButton(
                    title: "Status",
                    message: "The meter is not currently connected. Bring it within Bluetooth range or wake it up to open live diagnostics."
                )
            }
            HStack(spacing: 8) {
                Circle()
                    .fill(meterSummary.tint)
                    .frame(width: 10, height: 10)
                Text("Offline")
                    .font(.caption.weight(.semibold))
                    .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 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: ContextInfoHeader(
                        title: "Identity",
                        message: "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."
                    )
                ) {
                    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)
                }
            }
            .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
    }
}