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 } }