USB-Meter / USB Meter / Views / ChargedDevices / ChargeSessionCompletionSheetView.swift
Newer Older
157 lines | 5.705kb
Bogdan Timofte authored a month ago
1
//
2
//  ChargeSessionCompletionSheetView.swift
3
//  USB Meter
4
//
5

            
6
import SwiftUI
7

            
8
struct ChargeSessionCompletionSheetView: View {
Bogdan Timofte authored a month ago
9
    private enum FinalCheckpoint: String, CaseIterable, Identifiable {
10
        case full
11
        case skip
12
        case custom
13

            
14
        var id: String { rawValue }
15

            
16
        var label: String {
17
            switch self {
18
            case .full:   return "Full"
19
            case .skip:   return "Skip"
20
            case .custom: return "Other %"
21
            }
22
        }
23
    }
24

            
Bogdan Timofte authored a month ago
25
    @EnvironmentObject private var appData: AppData
26
    @Environment(\.dismiss) private var dismiss
27

            
28
    let sessionID: UUID
29
    let title: String
30
    let confirmTitle: String
31
    let explanation: String
32

            
33
    @State private var batteryPercent = ""
Bogdan Timofte authored a month ago
34
    @State private var finalCheckpoint: FinalCheckpoint = .skip
Bogdan Timofte authored a month ago
35

            
36
    var body: some View {
37
        NavigationView {
38
            Form {
39
                Section(
40
                    header: ContextInfoHeader(
41
                        title: "Final Checkpoint",
42
                        message: explanation
43
                    )
44
                ) {
Bogdan Timofte authored a month ago
45
                    Picker("Final Battery", selection: $finalCheckpoint) {
46
                        ForEach(FinalCheckpoint.allCases) { mode in
47
                            Text(mode.label).tag(mode)
48
                        }
49
                    }
50
                    .pickerStyle(.segmented)
51

            
52
                    if finalCheckpoint == .custom {
53
                        TextField("Battery %", text: $batteryPercent)
54
                            .keyboardType(.decimalPad)
55
                    }
Bogdan Timofte authored a month ago
56
                }
57

            
58
                Section {
Bogdan Timofte authored a month ago
59
                    if let refusalReason {
60
                        Label(refusalReason, systemImage: "exclamationmark.triangle.fill")
61
                            .font(.footnote)
62
                            .foregroundColor(.red)
63

            
64
                        Button(role: .destructive) {
65
                            _ = appData.deleteChargeSession(sessionID: sessionID)
66
                            dismiss()
67
                        } label: {
68
                            Label("Discard Session", systemImage: "trash")
69
                        }
70
                    } else if let customCheckpointWarning {
71
                        Label(customCheckpointWarning, systemImage: "exclamationmark.triangle.fill")
72
                            .font(.footnote)
73
                            .foregroundColor(.orange)
74
                    } else if let sessionWarning {
Bogdan Timofte authored a month ago
75
                        Text(sessionWarning)
76
                            .font(.footnote)
77
                            .foregroundColor(.orange)
Bogdan Timofte authored a month ago
78
                    } else if resolvedFinalBatteryPercent == 100 {
Bogdan Timofte authored a month ago
79
                        Text("A final checkpoint at 100% lets the app learn the stop current for this exact charging type when the session data is reliable.")
80
                            .font(.footnote)
81
                            .foregroundColor(.secondary)
82
                    }
83
                }
84
            }
85
            .navigationTitle(title)
86
            .navigationBarTitleDisplayMode(.inline)
87
            .toolbar {
88
                ToolbarItem(placement: .cancellationAction) {
89
                    Button("Cancel") {
90
                        dismiss()
91
                    }
92
                }
93
                ToolbarItem(placement: .confirmationAction) {
94
                    Button(confirmTitle) {
Bogdan Timofte authored a month ago
95
                        guard canSave else { return }
96
                        if appData.stopChargeSession(sessionID: sessionID, finalBatteryPercent: resolvedFinalBatteryPercent) {
Bogdan Timofte authored a month ago
97
                            dismiss()
98
                        }
99
                    }
Bogdan Timofte authored a month ago
100
                    .disabled(!canSave)
101
                    .opacity(canSave ? 1 : 0.45)
Bogdan Timofte authored a month ago
102
                }
103
            }
104
        }
105
        .navigationViewStyle(StackNavigationViewStyle())
106
    }
107

            
Bogdan Timofte authored a month ago
108
    private var session: ChargeSessionSummary? {
109
        appData.chargedDevices
110
            .flatMap(\.sessions)
111
            .first(where: { $0.id == sessionID })
112
    }
113

            
114
    private var canSave: Bool {
115
        session?.hasSavableChargeData == true
116
    }
117

            
118
    private var refusalReason: String? {
119
        canSave ? nil : "This session has no charging data to save. Discard it instead."
120
    }
121

            
122
    private var customCheckpointWarning: String? {
123
        guard finalCheckpoint == .custom,
124
              batteryPercent.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false,
125
              parsedBatteryPercent == nil else {
126
            return nil
127
        }
128
        return "Final battery percentage must be between 0 and 100. Save will close the session without a final checkpoint."
129
    }
130

            
Bogdan Timofte authored a month ago
131
    private var parsedBatteryPercent: Double? {
132
        let normalized = batteryPercent
133
            .trimmingCharacters(in: .whitespacesAndNewlines)
134
            .replacingOccurrences(of: ",", with: ".")
135
        guard let value = Double(normalized), value >= 0, value <= 100 else { return nil }
136
        return value
137
    }
138

            
Bogdan Timofte authored a month ago
139
    private var resolvedFinalBatteryPercent: Double? {
140
        switch finalCheckpoint {
141
        case .full:   return 100
142
        case .skip:   return nil
143
        case .custom: return parsedBatteryPercent
144
        }
145
    }
146

            
Bogdan Timofte authored a month ago
147
    private var sessionWarning: String? {
Bogdan Timofte authored a month ago
148
        guard let session,
Bogdan Timofte authored a month ago
149
              session.chargingTransportMode == .wireless,
150
              let chargerID = session.chargerID,
151
              let charger = appData.chargedDeviceSummary(id: chargerID),
152
              charger.chargerIdleCurrentAmps == nil else {
153
            return nil
154
        }
155
        return "This charger has no idle-current measurement, so the final checkpoint will stop the session but will not learn a wireless stop threshold yet."
156
    }
157
}