1 contributor
//
// SidebarChargedDeviceLibraryView.swift
// USB Meter
//
// Created by Codex on 22/04/2026.
//
import SwiftUI
/// Full-management library for the sidebar — navigates into detail instead of select-and-dismiss.
struct SidebarChargedDeviceLibraryView: View {
@EnvironmentObject private var appData: AppData
let mode: ChargedDeviceLibraryMode
@State private var showingNewEditor = false
@State private var editingChargedDevice: ChargedDeviceSummary?
@State private var pendingDeletion: ChargedDeviceSummary?
private var tint: Color {
mode == .device ? .orange : .pink
}
var body: some View {
List {
if displayedDevices.isEmpty {
emptyStateView
} else {
deviceRows
}
}
.listStyle(InsetGroupedListStyle())
.background(backgroundGradient)
.navigationTitle(mode.title)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button("New") { showingNewEditor = true }
}
}
.sidebarToggleToolbarItem()
.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, action: deletePendingDevice)
Button("Cancel", role: .cancel) { pendingDeletion = nil }
} message: {
Text("This will permanently remove the \(mode.singularTitle.lowercased()) and all associated data.")
}
}
private var emptyStateView: some View {
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)
}
private var deviceRows: some View {
ForEach(displayedDevices) { device in
NavigationLink(destination: ChargedDeviceSettingsView(chargedDeviceID: device.id)) {
ChargedDeviceLibraryRowView(chargedDevice: device, isSelected: false)
}
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
rowActions(for: device)
}
.contextMenu {
Button {
editingChargedDevice = device
} label: {
Label("Edit \(mode.singularTitle)", systemImage: "pencil")
}
Button(role: .destructive) {
pendingDeletion = device
} label: {
Label("Delete \(mode.singularTitle)", systemImage: "trash")
}
}
}
}
private var backgroundGradient: some View {
LinearGradient(
colors: [tint.opacity(0.14), Color.clear],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
.ignoresSafeArea()
}
private var displayedDevices: [ChargedDeviceSummary] {
mode == .device ? appData.deviceSummaries : appData.chargerSummaries
}
@ViewBuilder
private var newEditorSheet: some View {
if mode == .charger {
ChargerEditorSheetView(meterMACAddress: nil)
.environmentObject(appData)
} else {
ChargedDeviceEditorSheetView(meterMACAddress: nil)
.environmentObject(appData)
}
}
@ViewBuilder
private func editEditorSheet(_ device: ChargedDeviceSummary) -> some View {
if device.isCharger {
ChargerEditorSheetView(chargedDevice: device)
.environmentObject(appData)
} else {
ChargedDeviceEditorSheetView(meterMACAddress: nil, chargedDevice: device)
.environmentObject(appData)
}
}
@ViewBuilder
private func rowActions(for device: ChargedDeviceSummary) -> some View {
Button(role: .destructive) {
pendingDeletion = device
} label: {
Label("Delete", systemImage: "trash")
}
Button {
editingChargedDevice = device
} label: {
Label("Edit", systemImage: "pencil")
}
.tint(.blue)
}
private var emptyStateDescription: String {
mode == .device
? "Create one here, then select it before or during a charging session. The selected device becomes the default for this meter."
: "Create one here, then select it for wireless charging sessions. The selected charger becomes the default wireless source for this meter."
}
private func deletePendingDevice() {
if let device = pendingDeletion {
_ = appData.deleteChargedDevice(id: device.id)
pendingDeletion = nil
}
}
}