1 contributor
//
// ChargedDeviceLibrarySheetView.swift
// USB Meter
//
// Created by Codex on 10/04/2026.
//
import SwiftUI
enum ChargedDeviceLibraryMode {
case device
case charger
var kind: ChargedDeviceKind {
switch self {
case .device:
return .device
case .charger:
return .charger
}
}
var title: String {
kind.pluralTitle
}
var singularTitle: String {
kind.title
}
}
struct ChargedDeviceLibrarySheetView: View {
@EnvironmentObject private var appData: AppData
@Environment(\.dismiss) private var dismiss
let meterMACAddress: String
let meterTint: Color
let mode: ChargedDeviceLibraryMode
/// true = standalone sheet with own NavigationView; false = pushed into parent nav stack
let standalone: Bool
@State private var showingNewEditor = false
@State private var editingChargedDevice: ChargedDeviceSummary?
@State private var pendingDeletion: ChargedDeviceSummary?
init(
meterMACAddress: String,
meterTint: Color,
mode: ChargedDeviceLibraryMode,
standalone: Bool = true
) {
self.meterMACAddress = meterMACAddress
self.meterTint = meterTint
self.mode = mode
self.standalone = standalone
}
var body: some View {
if standalone {
NavigationView { listContent }
.navigationViewStyle(StackNavigationViewStyle())
} else {
listContent
}
}
private var listContent: some View {
List {
if displayedChargedDevices.isEmpty {
VStack(alignment: .leading, spacing: 10) {
HStack(spacing: 8) {
Text("No \(mode.title.lowercased()) yet.")
.font(.headline)
ContextInfoButton(
title: mode.title,
message: emptyStateDescription
)
}
}
.padding(.vertical, 10)
.listRowBackground(Color.clear)
} else {
ForEach(displayedChargedDevices) { chargedDevice in
Button {
select(chargedDevice)
dismiss()
} label: {
ChargedDeviceLibraryRowView(
chargedDevice: chargedDevice,
isSelected: chargedDevice.id == selectedDeviceID
)
}
.buttonStyle(.plain)
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
Button(role: .destructive) {
pendingDeletion = chargedDevice
} label: {
Label("Delete", systemImage: "trash")
}
Button {
editingChargedDevice = chargedDevice
} label: {
Label("Edit", systemImage: "pencil")
}
.tint(.blue)
}
.contextMenu {
Button {
editingChargedDevice = chargedDevice
} label: {
Label("Edit \(mode.singularTitle)", systemImage: "pencil")
}
Button(role: .destructive) {
pendingDeletion = chargedDevice
} label: {
Label("Delete \(mode.singularTitle)", systemImage: "trash")
}
}
}
}
}
.listStyle(InsetGroupedListStyle())
.background(
LinearGradient(
colors: [meterTint.opacity(0.14), Color.clear],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
.ignoresSafeArea()
)
.navigationTitle(mode.title)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
if standalone {
Button("Done") { dismiss() }
}
}
ToolbarItem(placement: .confirmationAction) {
Button("New") { showingNewEditor = true }
}
}
.sheet(isPresented: $showingNewEditor) {
newEditorSheet
}
.sheet(item: $editingChargedDevice) { device in
editEditorSheet(device)
}
.confirmationDialog(
"Delete \(pendingDeletion?.name ?? mode.singularTitle)?",
isPresented: Binding(
get: { pendingDeletion != nil },
set: { if !$0 { pendingDeletion = nil } }
),
titleVisibility: .visible
) {
Button("Delete", role: .destructive) {
if let device = pendingDeletion {
_ = appData.deleteChargedDevice(id: device.id)
pendingDeletion = nil
}
}
Button("Cancel", role: .cancel) { pendingDeletion = nil }
} message: {
Text("This will permanently remove the \(mode.singularTitle.lowercased()) and all associated data.")
}
}
@ViewBuilder
private var newEditorSheet: some View {
if mode == .charger {
ChargerEditorSheetView(meterMACAddress: meterMACAddress)
.environmentObject(appData)
} else {
ChargedDeviceEditorSheetView(meterMACAddress: meterMACAddress)
.environmentObject(appData)
}
}
@ViewBuilder
private func editEditorSheet(_ chargedDevice: ChargedDeviceSummary) -> some View {
if chargedDevice.isCharger {
ChargerEditorSheetView(chargedDevice: chargedDevice)
.environmentObject(appData)
} else {
ChargedDeviceEditorSheetView(
meterMACAddress: nil,
chargedDevice: chargedDevice
)
.environmentObject(appData)
}
}
private var displayedChargedDevices: [ChargedDeviceSummary] {
switch mode {
case .device:
return appData.deviceSummaries
case .charger:
return appData.chargerSummaries
}
}
private var selectedDeviceID: UUID? {
switch mode {
case .device:
return appData.currentChargedDeviceSummary(for: meterMACAddress)?.id
case .charger:
return appData.currentChargerSummary(for: meterMACAddress)?.id
}
}
private var emptyStateDescription: String {
switch mode {
case .device:
return "Create one here, then select it before or during a charging session. The selected device becomes the default for this meter."
case .charger:
return "Create one here, then select it for wireless charging sessions. The selected charger becomes the default wireless source for this meter."
}
}
private func select(_ chargedDevice: ChargedDeviceSummary) {
switch mode {
case .device:
appData.assignChargedDevice(chargedDevice.id, to: meterMACAddress)
case .charger:
appData.assignCharger(chargedDevice.id, to: meterMACAddress)
}
}
}