@@ -384,3 +384,137 @@ struct ChargedDeviceTemplateIconView: View {
|
||
| 384 | 384 |
return fallbackSystemName |
| 385 | 385 |
} |
| 386 | 386 |
} |
| 387 |
+ |
|
| 388 |
+// MARK: - Sidebar Library View |
|
| 389 |
+ |
|
| 390 |
+/// Full-management library for the sidebar — navigates into ChargedDeviceDetailView |
|
| 391 |
+/// instead of select-and-dismiss like ChargedDeviceLibrarySheetView. |
|
| 392 |
+struct SidebarChargedDeviceLibraryView: View {
|
|
| 393 |
+ @EnvironmentObject private var appData: AppData |
|
| 394 |
+ |
|
| 395 |
+ let mode: ChargedDeviceLibraryMode |
|
| 396 |
+ |
|
| 397 |
+ @State private var showingNewEditor = false |
|
| 398 |
+ @State private var editingChargedDevice: ChargedDeviceSummary? |
|
| 399 |
+ @State private var pendingDeletion: ChargedDeviceSummary? |
|
| 400 |
+ |
|
| 401 |
+ private var tint: Color {
|
|
| 402 |
+ mode == .device ? .orange : .pink |
|
| 403 |
+ } |
|
| 404 |
+ |
|
| 405 |
+ var body: some View {
|
|
| 406 |
+ List {
|
|
| 407 |
+ if displayedDevices.isEmpty {
|
|
| 408 |
+ VStack(alignment: .leading, spacing: 10) {
|
|
| 409 |
+ HStack(spacing: 8) {
|
|
| 410 |
+ Text("No \(mode.title.lowercased()) yet.")
|
|
| 411 |
+ .font(.headline) |
|
| 412 |
+ ContextInfoButton( |
|
| 413 |
+ title: mode.title, |
|
| 414 |
+ message: emptyStateDescription |
|
| 415 |
+ ) |
|
| 416 |
+ } |
|
| 417 |
+ } |
|
| 418 |
+ .padding(.vertical, 10) |
|
| 419 |
+ .listRowBackground(Color.clear) |
|
| 420 |
+ } else {
|
|
| 421 |
+ ForEach(displayedDevices) { device in
|
|
| 422 |
+ NavigationLink(destination: ChargedDeviceDetailView(chargedDeviceID: device.id)) {
|
|
| 423 |
+ ChargedDeviceLibraryRowView(chargedDevice: device, isSelected: false) |
|
| 424 |
+ } |
|
| 425 |
+ .swipeActions(edge: .trailing, allowsFullSwipe: false) {
|
|
| 426 |
+ Button(role: .destructive) {
|
|
| 427 |
+ pendingDeletion = device |
|
| 428 |
+ } label: {
|
|
| 429 |
+ Label("Delete", systemImage: "trash")
|
|
| 430 |
+ } |
|
| 431 |
+ Button {
|
|
| 432 |
+ editingChargedDevice = device |
|
| 433 |
+ } label: {
|
|
| 434 |
+ Label("Edit", systemImage: "pencil")
|
|
| 435 |
+ } |
|
| 436 |
+ .tint(.blue) |
|
| 437 |
+ } |
|
| 438 |
+ .contextMenu {
|
|
| 439 |
+ Button {
|
|
| 440 |
+ editingChargedDevice = device |
|
| 441 |
+ } label: {
|
|
| 442 |
+ Label("Edit \(mode.singularTitle)", systemImage: "pencil")
|
|
| 443 |
+ } |
|
| 444 |
+ Button(role: .destructive) {
|
|
| 445 |
+ pendingDeletion = device |
|
| 446 |
+ } label: {
|
|
| 447 |
+ Label("Delete \(mode.singularTitle)", systemImage: "trash")
|
|
| 448 |
+ } |
|
| 449 |
+ } |
|
| 450 |
+ } |
|
| 451 |
+ } |
|
| 452 |
+ } |
|
| 453 |
+ .listStyle(InsetGroupedListStyle()) |
|
| 454 |
+ .background( |
|
| 455 |
+ LinearGradient( |
|
| 456 |
+ colors: [tint.opacity(0.14), Color.clear], |
|
| 457 |
+ startPoint: .topLeading, |
|
| 458 |
+ endPoint: .bottomTrailing |
|
| 459 |
+ ) |
|
| 460 |
+ .ignoresSafeArea() |
|
| 461 |
+ ) |
|
| 462 |
+ .navigationTitle(mode.title) |
|
| 463 |
+ .navigationBarTitleDisplayMode(.inline) |
|
| 464 |
+ .toolbar {
|
|
| 465 |
+ ToolbarItem(placement: .primaryAction) {
|
|
| 466 |
+ Button("New") { showingNewEditor = true }
|
|
| 467 |
+ } |
|
| 468 |
+ } |
|
| 469 |
+ .sheet(isPresented: $showingNewEditor) { newEditorSheet }
|
|
| 470 |
+ .sheet(item: $editingChargedDevice) { device in editEditorSheet(device) }
|
|
| 471 |
+ .confirmationDialog( |
|
| 472 |
+ "Delete \(pendingDeletion?.name ?? mode.singularTitle)?", |
|
| 473 |
+ isPresented: Binding( |
|
| 474 |
+ get: { pendingDeletion != nil },
|
|
| 475 |
+ set: { if !$0 { pendingDeletion = nil } }
|
|
| 476 |
+ ), |
|
| 477 |
+ titleVisibility: .visible |
|
| 478 |
+ ) {
|
|
| 479 |
+ Button("Delete", role: .destructive) {
|
|
| 480 |
+ if let device = pendingDeletion {
|
|
| 481 |
+ _ = appData.deleteChargedDevice(id: device.id) |
|
| 482 |
+ pendingDeletion = nil |
|
| 483 |
+ } |
|
| 484 |
+ } |
|
| 485 |
+ Button("Cancel", role: .cancel) { pendingDeletion = nil }
|
|
| 486 |
+ } message: {
|
|
| 487 |
+ Text("This will permanently remove the \(mode.singularTitle.lowercased()) and all associated data.")
|
|
| 488 |
+ } |
|
| 489 |
+ } |
|
| 490 |
+ |
|
| 491 |
+ private var displayedDevices: [ChargedDeviceSummary] {
|
|
| 492 |
+ mode == .device ? appData.deviceSummaries : appData.chargerSummaries |
|
| 493 |
+ } |
|
| 494 |
+ |
|
| 495 |
+ @ViewBuilder |
|
| 496 |
+ private var newEditorSheet: some View {
|
|
| 497 |
+ if mode == .charger {
|
|
| 498 |
+ ChargerEditorSheetView(appData: appData, meterMACAddress: nil) |
|
| 499 |
+ } else {
|
|
| 500 |
+ ChargedDeviceEditorSheetView(meterMACAddress: nil) |
|
| 501 |
+ .environmentObject(appData) |
|
| 502 |
+ } |
|
| 503 |
+ } |
|
| 504 |
+ |
|
| 505 |
+ @ViewBuilder |
|
| 506 |
+ private func editEditorSheet(_ device: ChargedDeviceSummary) -> some View {
|
|
| 507 |
+ if device.isCharger {
|
|
| 508 |
+ ChargerEditorSheetView(appData: appData, chargedDevice: device) |
|
| 509 |
+ } else {
|
|
| 510 |
+ ChargedDeviceEditorSheetView(meterMACAddress: nil, chargedDevice: device) |
|
| 511 |
+ .environmentObject(appData) |
|
| 512 |
+ } |
|
| 513 |
+ } |
|
| 514 |
+ |
|
| 515 |
+ private var emptyStateDescription: String {
|
|
| 516 |
+ mode == .device |
|
| 517 |
+ ? "Create one here, then select it before or during a charging session. The selected device becomes the default for this meter." |
|
| 518 |
+ : "Create one here, then select it for wireless charging sessions. The selected charger becomes the default wireless source for this meter." |
|
| 519 |
+ } |
|
| 520 |
+} |
|
@@ -9,6 +9,7 @@ import SwiftUI |
||
| 9 | 9 |
|
| 10 | 10 |
struct SidebarChargedDevicesSectionView: View {
|
| 11 | 11 |
let title: String |
| 12 |
+ let mode: ChargedDeviceLibraryMode |
|
| 12 | 13 |
let chargedDevices: [ChargedDeviceSummary] |
| 13 | 14 |
let emptyStateText: String |
| 14 | 15 |
let tint: Color |
@@ -16,22 +17,43 @@ struct SidebarChargedDevicesSectionView: View {
|
||
| 16 | 17 |
|
| 17 | 18 |
var body: some View {
|
| 18 | 19 |
Section(header: headerView) {
|
| 19 |
- if chargedDevices.isEmpty {
|
|
| 20 |
- Text(emptyStateText) |
|
| 21 |
- .font(.footnote) |
|
| 22 |
- .foregroundColor(.secondary) |
|
| 23 |
- .frame(maxWidth: .infinity, alignment: .leading) |
|
| 24 |
- .padding(18) |
|
| 25 |
- .meterCard(tint: tint, fillOpacity: 0.12, strokeOpacity: 0.18) |
|
| 26 |
- } else {
|
|
| 27 |
- ForEach(chargedDevices) { chargedDevice in
|
|
| 28 |
- NavigationLink(destination: ChargedDeviceDetailView(chargedDeviceID: chargedDevice.id)) {
|
|
| 29 |
- ChargedDeviceSidebarCardView(chargedDevice: chargedDevice) |
|
| 30 |
- } |
|
| 31 |
- .buttonStyle(.plain) |
|
| 20 |
+ // Library overview row — navigates to the full management library |
|
| 21 |
+ NavigationLink(destination: SidebarChargedDeviceLibraryView(mode: mode)) {
|
|
| 22 |
+ libraryRow |
|
| 23 |
+ } |
|
| 24 |
+ .buttonStyle(.plain) |
|
| 25 |
+ |
|
| 26 |
+ ForEach(chargedDevices) { chargedDevice in
|
|
| 27 |
+ NavigationLink(destination: ChargedDeviceDetailView(chargedDeviceID: chargedDevice.id)) {
|
|
| 28 |
+ ChargedDeviceSidebarCardView(chargedDevice: chargedDevice) |
|
| 32 | 29 |
} |
| 30 |
+ .buttonStyle(.plain) |
|
| 31 |
+ } |
|
| 32 |
+ } |
|
| 33 |
+ } |
|
| 34 |
+ |
|
| 35 |
+ private var libraryRow: some View {
|
|
| 36 |
+ HStack(spacing: 12) {
|
|
| 37 |
+ Image(systemName: mode == .device ? "square.grid.2x2" : "bolt.circle") |
|
| 38 |
+ .font(.system(size: 16, weight: .semibold)) |
|
| 39 |
+ .foregroundColor(tint) |
|
| 40 |
+ .frame(width: 36, height: 36) |
|
| 41 |
+ .background(tint.opacity(0.14)) |
|
| 42 |
+ .clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous)) |
|
| 43 |
+ |
|
| 44 |
+ VStack(alignment: .leading, spacing: 2) {
|
|
| 45 |
+ Text("All \(title)")
|
|
| 46 |
+ .font(.subheadline.weight(.semibold)) |
|
| 47 |
+ .foregroundColor(.primary) |
|
| 48 |
+ let count = chargedDevices.count |
|
| 49 |
+ Text("\(count) \(count == 1 ? mode.singularTitle.lowercased() : mode.title.lowercased())")
|
|
| 50 |
+ .font(.caption) |
|
| 51 |
+ .foregroundColor(.secondary) |
|
| 33 | 52 |
} |
| 53 |
+ |
|
| 54 |
+ Spacer() |
|
| 34 | 55 |
} |
| 56 |
+ .padding(.vertical, 4) |
|
| 35 | 57 |
} |
| 36 | 58 |
|
| 37 | 59 |
private var headerView: some View {
|
@@ -82,6 +82,7 @@ struct SidebarView: View {
|
||
| 82 | 82 |
|
| 83 | 83 |
SidebarChargedDevicesSectionView( |
| 84 | 84 |
title: "Devices", |
| 85 |
+ mode: .device, |
|
| 85 | 86 |
chargedDevices: appData.deviceSummaries, |
| 86 | 87 |
emptyStateText: "No devices yet. Open Charge Record on a live meter or use the add button here to create one and start learning capacity.", |
| 87 | 88 |
tint: .orange, |
@@ -90,6 +91,7 @@ struct SidebarView: View {
|
||
| 90 | 91 |
|
| 91 | 92 |
SidebarChargedDevicesSectionView( |
| 92 | 93 |
title: "Chargers", |
| 94 |
+ mode: .charger, |
|
| 93 | 95 |
chargedDevices: appData.chargerSummaries, |
| 94 | 96 |
emptyStateText: "No chargers yet. Add one here so wireless sessions can track both the charged device and the charger being used.", |
| 95 | 97 |
tint: .pink, |