USB-Meter / USB Meter / Views / ChargedDevices / Sheets / Library / ChargedDeviceLibrarySheetView.swift
Newer Older
229 lines | 7.603kb
Bogdan Timofte authored a month ago
1
//
2
//  ChargedDeviceLibrarySheetView.swift
3
//  USB Meter
4
//
5
//  Created by Codex on 10/04/2026.
6
//
7

            
8
import SwiftUI
9

            
10
enum ChargedDeviceLibraryMode {
11
    case device
12
    case charger
13

            
14
    var kind: ChargedDeviceKind {
15
        switch self {
16
        case .device:
17
            return .device
18
        case .charger:
19
            return .charger
20
        }
21
    }
22

            
23
    var title: String {
24
        kind.pluralTitle
25
    }
26

            
27
    var singularTitle: String {
28
        kind.title
29
    }
30
}
31

            
32
struct ChargedDeviceLibrarySheetView: View {
33
    @EnvironmentObject private var appData: AppData
34
    @Environment(\.dismiss) private var dismiss
35

            
36
    let meterMACAddress: String
37
    let meterTint: Color
38
    let mode: ChargedDeviceLibraryMode
39
    /// true = standalone sheet with own NavigationView; false = pushed into parent nav stack
40
    let standalone: Bool
41

            
42
    @State private var showingNewEditor = false
43
    @State private var editingChargedDevice: ChargedDeviceSummary?
44
    @State private var pendingDeletion: ChargedDeviceSummary?
45

            
46
    init(
47
        meterMACAddress: String,
48
        meterTint: Color,
49
        mode: ChargedDeviceLibraryMode,
50
        standalone: Bool = true
51
    ) {
52
        self.meterMACAddress = meterMACAddress
53
        self.meterTint = meterTint
54
        self.mode = mode
55
        self.standalone = standalone
56
    }
57

            
58
    var body: some View {
59
        if standalone {
60
            NavigationView { listContent }
61
                .navigationViewStyle(StackNavigationViewStyle())
62
        } else {
63
            listContent
64
        }
65
    }
66

            
67
    private var listContent: some View {
68
        List {
69
            if displayedChargedDevices.isEmpty {
70
                VStack(alignment: .leading, spacing: 10) {
71
                    HStack(spacing: 8) {
72
                        Text("No \(mode.title.lowercased()) yet.")
73
                            .font(.headline)
74
                        ContextInfoButton(
75
                            title: mode.title,
76
                            message: emptyStateDescription
77
                        )
78
                    }
79
                }
80
                .padding(.vertical, 10)
81
                .listRowBackground(Color.clear)
82
            } else {
83
                ForEach(displayedChargedDevices) { chargedDevice in
84
                    Button {
85
                        select(chargedDevice)
86
                        dismiss()
87
                    } label: {
88
                        ChargedDeviceLibraryRowView(
89
                            chargedDevice: chargedDevice,
90
                            isSelected: chargedDevice.id == selectedDeviceID
91
                        )
92
                    }
93
                    .buttonStyle(.plain)
94
                    .swipeActions(edge: .trailing, allowsFullSwipe: false) {
95
                        Button(role: .destructive) {
96
                            pendingDeletion = chargedDevice
97
                        } label: {
98
                            Label("Delete", systemImage: "trash")
99
                        }
100
                        Button {
101
                            editingChargedDevice = chargedDevice
102
                        } label: {
103
                            Label("Edit", systemImage: "pencil")
104
                        }
105
                        .tint(.blue)
106
                    }
107
                    .contextMenu {
108
                        Button {
109
                            editingChargedDevice = chargedDevice
110
                        } label: {
111
                            Label("Edit \(mode.singularTitle)", systemImage: "pencil")
112
                        }
113
                        Button(role: .destructive) {
114
                            pendingDeletion = chargedDevice
115
                        } label: {
116
                            Label("Delete \(mode.singularTitle)", systemImage: "trash")
117
                        }
118
                    }
119
                }
120
            }
121
        }
122
        .listStyle(InsetGroupedListStyle())
123
        .background(
124
            LinearGradient(
125
                colors: [meterTint.opacity(0.14), Color.clear],
126
                startPoint: .topLeading,
127
                endPoint: .bottomTrailing
128
            )
129
            .ignoresSafeArea()
130
        )
131
        .navigationTitle(mode.title)
132
        .navigationBarTitleDisplayMode(.inline)
133
        .toolbar {
134
            ToolbarItem(placement: .cancellationAction) {
135
                if standalone {
136
                    Button("Done") { dismiss() }
137
                }
138
            }
139
            ToolbarItem(placement: .confirmationAction) {
140
                Button("New") { showingNewEditor = true }
141
            }
142
        }
143
        .sheet(isPresented: $showingNewEditor) {
144
            newEditorSheet
145
        }
146
        .sheet(item: $editingChargedDevice) { device in
147
            editEditorSheet(device)
148
        }
149
        .confirmationDialog(
150
            "Delete \(pendingDeletion?.name ?? mode.singularTitle)?",
151
            isPresented: Binding(
152
                get: { pendingDeletion != nil },
153
                set: { if !$0 { pendingDeletion = nil } }
154
            ),
155
            titleVisibility: .visible
156
        ) {
157
            Button("Delete", role: .destructive) {
158
                if let device = pendingDeletion {
159
                    _ = appData.deleteChargedDevice(id: device.id)
160
                    pendingDeletion = nil
161
                }
162
            }
163
            Button("Cancel", role: .cancel) { pendingDeletion = nil }
164
        } message: {
165
            Text("This will permanently remove the \(mode.singularTitle.lowercased()) and all associated data.")
166
        }
167
    }
168

            
169
    @ViewBuilder
170
    private var newEditorSheet: some View {
171
        if mode == .charger {
172
            ChargerEditorSheetView(meterMACAddress: meterMACAddress)
173
                .environmentObject(appData)
174
        } else {
175
            ChargedDeviceEditorSheetView(meterMACAddress: meterMACAddress)
176
                .environmentObject(appData)
177
        }
178
    }
179

            
180
    @ViewBuilder
181
    private func editEditorSheet(_ chargedDevice: ChargedDeviceSummary) -> some View {
182
        if chargedDevice.isCharger {
183
            ChargerEditorSheetView(chargedDevice: chargedDevice)
184
                .environmentObject(appData)
185
        } else {
186
            ChargedDeviceEditorSheetView(
187
                meterMACAddress: nil,
188
                chargedDevice: chargedDevice
189
            )
190
            .environmentObject(appData)
191
        }
192
    }
193

            
194
    private var displayedChargedDevices: [ChargedDeviceSummary] {
195
        switch mode {
196
        case .device:
197
            return appData.deviceSummaries
198
        case .charger:
199
            return appData.chargerSummaries
200
        }
201
    }
202

            
203
    private var selectedDeviceID: UUID? {
204
        switch mode {
205
        case .device:
206
            return appData.currentChargedDeviceSummary(for: meterMACAddress)?.id
207
        case .charger:
208
            return appData.currentChargerSummary(for: meterMACAddress)?.id
209
        }
210
    }
211

            
212
    private var emptyStateDescription: String {
213
        switch mode {
214
        case .device:
215
            return "Create one here, then select it before or during a charging session. The selected device becomes the default for this meter."
216
        case .charger:
217
            return "Create one here, then select it for wireless charging sessions. The selected charger becomes the default wireless source for this meter."
218
        }
219
    }
220

            
221
    private func select(_ chargedDevice: ChargedDeviceSummary) {
222
        switch mode {
223
        case .device:
224
            appData.assignChargedDevice(chargedDevice.id, to: meterMACAddress)
225
        case .charger:
226
            appData.assignCharger(chargedDevice.id, to: meterMACAddress)
227
        }
228
    }
229
}