USB-Meter / USB Meter / Views / Sidebar / SidebarList / Sections / SidebarUSBMetersSectionView.swift
Newer Older
134 lines | 5.04kb
Bogdan Timofte authored 2 months ago
1
//
2
//  SidebarUSBMetersSectionView.swift
3
//  USB Meter
4
//
5

            
6
import SwiftUI
7
import CoreBluetooth
8

            
9
struct SidebarUSBMetersSectionView: View {
10
    let meters: [AppData.MeterSummary]
11
    let managerState: CBManagerState
12
    let hasLiveMeters: Bool
13
    let scanStartedAt: Date?
14
    let now: Date
15
    let noDevicesHelpDelay: TimeInterval
Bogdan Timofte authored a month ago
16
    let isExpanded: Bool
17
    let onToggle: () -> Void
Bogdan Timofte authored a month ago
18
    let onAddMeter: () -> Void
Bogdan Timofte authored 2 months ago
19

            
20
    var body: some View {
21
        Section(header: usbSectionHeader) {
Bogdan Timofte authored a month ago
22
            if isExpanded {
23
                if meters.isEmpty {
24
                    Text(devicesEmptyStateText)
25
                        .font(.footnote)
26
                        .foregroundColor(.secondary)
27
                        .frame(maxWidth: .infinity, alignment: .leading)
28
                        .padding(18)
29
                        .meterCard(
30
                            tint: isWaitingForFirstDiscovery ? .blue : .secondary,
31
                            fillOpacity: 0.14,
32
                            strokeOpacity: 0.20
33
                        )
34
                        .transition(.opacity.combined(with: .move(edge: .top)))
35
                } else {
36
                    ForEach(meters) { meterSummary in
37
                        if let meter = meterSummary.meter {
38
                            NavigationLink(destination: MeterView().environmentObject(meter)) {
Bogdan Timofte authored a month ago
39
                                SidebarMeterCardView()
Bogdan Timofte authored a month ago
40
                                    .environmentObject(meter)
41
                            }
42
                            .buttonStyle(.plain)
43
                            .transition(.opacity.combined(with: .move(edge: .top)))
Bogdan Timofte authored a month ago
44
                        } else {
45
                            NavigationLink(destination: MeterView(offlineSummary: meterSummary)) {
46
                                SidebarOfflineMeterCardView(summary: meterSummary)
47
                            }
48
                            .buttonStyle(.plain)
49
                            .transition(.opacity.combined(with: .move(edge: .top)))
Bogdan Timofte authored 2 months ago
50
                        }
51
                    }
52
                }
53
            }
54
        }
55
    }
56

            
57
    private var isWaitingForFirstDiscovery: Bool {
58
        guard managerState == .poweredOn else {
59
            return false
60
        }
61
        guard hasLiveMeters == false else {
62
            return false
63
        }
64
        guard let scanStartedAt else {
65
            return false
66
        }
67
        return now.timeIntervalSince(scanStartedAt) < noDevicesHelpDelay
68
    }
69

            
70
    private var devicesEmptyStateText: String {
71
        if isWaitingForFirstDiscovery {
72
            return "Scanning for nearby supported meters..."
73
        }
74
        return "No meters yet. Nearby supported meters will appear here and remain available after they disappear."
75
    }
76

            
77
    private var usbSectionHeader: some View {
Bogdan Timofte authored 2 months ago
78
        HStack(alignment: .firstTextBaseline) {
Bogdan Timofte authored a month ago
79
            Button(action: onToggle) {
80
                HStack(alignment: .firstTextBaseline, spacing: 4) {
81
                    Image(systemName: "chevron.right")
82
                        .font(.caption.weight(.semibold))
Bogdan Timofte authored 2 months ago
83
                        .foregroundColor(.secondary)
Bogdan Timofte authored a month ago
84
                        .rotationEffect(.degrees(isExpanded ? 90 : 0))
85
                        .animation(.easeInOut(duration: 0.22), value: isExpanded)
86
                    VStack(alignment: .leading, spacing: 2) {
87
                        Text("USB & Known Meters")
88
                            .font(.headline)
89
                        if meters.isEmpty == false {
90
                            Text(sectionSubtitleText)
91
                                .font(.caption)
92
                                .foregroundColor(.secondary)
93
                                .lineLimit(1)
94
                        }
95
                    }
Bogdan Timofte authored 2 months ago
96
                }
97
            }
Bogdan Timofte authored a month ago
98
            .buttonStyle(.plain)
Bogdan Timofte authored 2 months ago
99
            Spacer()
Bogdan Timofte authored a month ago
100
            Button(action: onAddMeter) {
101
                Image(systemName: "plus.circle.fill")
102
                    .font(.body.weight(.semibold))
103
                    .foregroundColor(.blue)
104
            }
105
            .buttonStyle(.plain)
Bogdan Timofte authored 2 months ago
106
            Text("\(meters.count)")
107
                .font(.caption.weight(.bold))
108
                .padding(.horizontal, 10)
109
                .padding(.vertical, 6)
110
                .meterCard(tint: .blue, fillOpacity: 0.18, strokeOpacity: 0.24, cornerRadius: 999)
111
        }
112
    }
Bogdan Timofte authored 2 months ago
113

            
114
    private var sectionSubtitleText: String {
115
        switch (liveMeterCount, offlineMeterCount) {
116
        case let (live, offline) where live > 0 && offline > 0:
117
            return "\(live) live • \(offline) stored"
118
        case let (live, _) where live > 0:
119
            return "\(live) live meter\(live == 1 ? "" : "s")"
120
        case let (_, offline) where offline > 0:
121
            return "\(offline) known meter\(offline == 1 ? "" : "s")"
122
        default:
123
            return ""
124
        }
125
    }
126

            
127
    private var liveMeterCount: Int {
128
        meters.filter { $0.meter != nil }.count
129
    }
130

            
131
    private var offlineMeterCount: Int {
132
        max(0, meters.count - liveMeterCount)
133
    }
Bogdan Timofte authored 2 months ago
134
}