Просмотр исходного кода

Deletion BGs from history page

Ivan Valkou 4 лет назад
Родитель
Сommit
4e90b6ee57

+ 45 - 8
FreeAPS/Sources/Modules/DataTable/DataTableDataFlow.swift

@@ -4,6 +4,24 @@ import SwiftUI
 enum DataTable {
     enum Config {}
 
+    enum Mode: String, Hashable, Identifiable, CaseIterable {
+        case treatments
+        case glucose
+
+        var id: String { rawValue }
+
+        var name: String {
+            var name: String = ""
+            switch self {
+            case .treatments:
+                name = "Treatments"
+            case .glucose:
+                name = "Glucose"
+            }
+            return NSLocalizedString(name, comment: "History Mode")
+        }
+    }
+
     enum DataType: String, Equatable {
         case carbs
         case bolus
@@ -13,24 +31,27 @@ enum DataTable {
         case resume
 
         var name: String {
+            var name: String = ""
             switch self {
             case .carbs:
-                return "Carbs"
+                name = "Carbs"
             case .bolus:
-                return "Bolus"
+                name = "Bolus"
             case .tempBasal:
-                return "Temp Basal"
+                name = "Temp Basal"
             case .tempTarget:
-                return "Temp Target"
+                name = "Temp Target"
             case .suspend:
-                return "Suspend"
+                name = "Suspend"
             case .resume:
-                return "Resume"
+                name = "Resume"
             }
+
+            return NSLocalizedString(name, comment: "Treatment type")
         }
     }
 
-    class Item: Identifiable, Hashable, Equatable {
+    class Treatment: Identifiable, Hashable, Equatable {
         let id = UUID()
         let units: GlucoseUnits
         let type: DataType
@@ -62,7 +83,7 @@ enum DataTable {
             self.duration = duration
         }
 
-        static func == (lhs: Item, rhs: Item) -> Bool {
+        static func == (lhs: Treatment, rhs: Treatment) -> Bool {
             lhs.id == rhs.id
         }
 
@@ -130,11 +151,27 @@ enum DataTable {
             return numberFormater.string(from: duration as NSNumber)! + " min"
         }
     }
+
+    class Glucose: Identifiable, Hashable, Equatable {
+        static func == (lhs: DataTable.Glucose, rhs: DataTable.Glucose) -> Bool {
+            lhs.glucose == rhs.glucose
+        }
+
+        let glucose: BloodGlucose
+
+        init(glucose: BloodGlucose) {
+            self.glucose = glucose
+        }
+
+        var id: String { glucose.id }
+    }
 }
 
 protocol DataTableProvider: Provider {
     func pumpHistory() -> [PumpHistoryEvent]
     func tempTargets() -> [TempTarget]
     func carbs() -> [CarbsEntry]
+    func glucose() -> [BloodGlucose]
     func deleteCarbs(at date: Date)
+    func deleteGlucose(id: String)
 }

+ 11 - 0
FreeAPS/Sources/Modules/DataTable/DataTableProvider.swift

@@ -4,8 +4,10 @@ extension DataTable {
     final class Provider: BaseProvider, DataTableProvider {
         @Injected() var pumpHistoryStorage: PumpHistoryStorage!
         @Injected() var tempTargetsStorage: TempTargetsStorage!
+        @Injected() var glucoseStorage: GlucoseStorage!
         @Injected() var carbsStorage: CarbsStorage!
         @Injected() var nightscoutManager: NightscoutManager!
+        @Injected() var healthkitManager: HealthKitManager!
 
         func pumpHistory() -> [PumpHistoryEvent] {
             pumpHistoryStorage.recent()
@@ -22,5 +24,14 @@ extension DataTable {
         func deleteCarbs(at date: Date) {
             nightscoutManager.deleteCarbs(at: date)
         }
+
+        func glucose() -> [BloodGlucose] {
+            glucoseStorage.recent().sorted { $0.date > $1.date }
+        }
+
+        func deleteGlucose(id: String) {
+            glucoseStorage.removeGlucose(ids: [id])
+            healthkitManager.deleteGlucise(syncID: id)
+        }
     }
 }

+ 38 - 16
FreeAPS/Sources/Modules/DataTable/DataTableStateModel.swift

@@ -3,38 +3,44 @@ import SwiftUI
 extension DataTable {
     final class StateModel: BaseStateModel<Provider> {
         @Injected() var broadcaster: Broadcaster!
-        @Published var items: [Item] = []
+        @Published var mode: Mode = .treatments
+        @Published var treatments: [Treatment] = []
+        @Published var glucose: [Glucose] = []
+        var units: GlucoseUnits = .mmolL
 
         override func subscribe() {
-            setupItems()
+            units = settingsManager.settings.units
+            setupTreatments()
+            setupGlucose()
             broadcaster.register(SettingsObserver.self, observer: self)
             broadcaster.register(PumpHistoryObserver.self, observer: self)
             broadcaster.register(TempTargetsObserver.self, observer: self)
             broadcaster.register(CarbsObserver.self, observer: self)
+            broadcaster.register(GlucoseObserver.self, observer: self)
         }
 
-        private func setupItems() {
+        private func setupTreatments() {
             DispatchQueue.global().async {
                 let units = self.settingsManager.settings.units
 
                 let carbs = self.provider.carbs().map {
-                    Item(units: units, type: .carbs, date: $0.createdAt, amount: $0.carbs)
+                    Treatment(units: units, type: .carbs, date: $0.createdAt, amount: $0.carbs)
                 }
 
                 let boluses = self.provider.pumpHistory()
                     .filter { $0.type == .bolus }
                     .map {
-                        Item(units: units, type: .bolus, date: $0.timestamp, amount: $0.amount)
+                        Treatment(units: units, type: .bolus, date: $0.timestamp, amount: $0.amount)
                     }
 
                 let tempBasals = self.provider.pumpHistory()
                     .filter { $0.type == .tempBasal || $0.type == .tempBasalDuration }
                     .chunks(ofCount: 2)
-                    .compactMap { chunk -> Item? in
+                    .compactMap { chunk -> Treatment? in
                         let chunk = Array(chunk)
                         guard chunk.count == 2, chunk[0].type == .tempBasal,
                               chunk[1].type == .tempBasalDuration else { return nil }
-                        return Item(
+                        return Treatment(
                             units: units,
                             type: .tempBasal,
                             date: chunk[0].timestamp,
@@ -46,7 +52,7 @@ extension DataTable {
 
                 let tempTargets = self.provider.tempTargets()
                     .map {
-                        Item(
+                        Treatment(
                             units: units,
                             type: .tempTarget,
                             date: $0.createdAt,
@@ -59,26 +65,37 @@ extension DataTable {
                 let suspend = self.provider.pumpHistory()
                     .filter { $0.type == .pumpSuspend }
                     .map {
-                        Item(units: units, type: .suspend, date: $0.timestamp)
+                        Treatment(units: units, type: .suspend, date: $0.timestamp)
                     }
 
                 let resume = self.provider.pumpHistory()
                     .filter { $0.type == .pumpResume }
                     .map {
-                        Item(units: units, type: .resume, date: $0.timestamp)
+                        Treatment(units: units, type: .resume, date: $0.timestamp)
                     }
 
                 DispatchQueue.main.async {
-                    self.items = [carbs, boluses, tempBasals, tempTargets, suspend, resume]
+                    self.treatments = [carbs, boluses, tempBasals, tempTargets, suspend, resume]
                         .flatMap { $0 }
                         .sorted { $0.date > $1.date }
                 }
             }
         }
 
+        func setupGlucose() {
+            DispatchQueue.main.async {
+                self.glucose = self.provider.glucose().map(Glucose.init)
+            }
+        }
+
         func deleteCarbs(at date: Date) {
             provider.deleteCarbs(at: date)
         }
+
+        func deleteGlucose(at index: Int) {
+            let id = glucose[index].id
+            provider.deleteGlucose(id: id)
+        }
     }
 }
 
@@ -86,21 +103,26 @@ extension DataTable.StateModel:
     SettingsObserver,
     PumpHistoryObserver,
     TempTargetsObserver,
-    CarbsObserver
+    CarbsObserver,
+    GlucoseObserver
 {
     func settingsDidChange(_: FreeAPSSettings) {
-        setupItems()
+        setupTreatments()
     }
 
     func pumpHistoryDidUpdate(_: [PumpHistoryEvent]) {
-        setupItems()
+        setupTreatments()
     }
 
     func tempTargetsDidUpdate(_: [TempTarget]) {
-        setupItems()
+        setupTreatments()
     }
 
     func carbsDidUpdate(_: [CarbsEntry]) {
-        setupItems()
+        setupTreatments()
+    }
+
+    func glucoseDidUpdate(_: [BloodGlucose]) {
+        setupGlucose()
     }
 }

+ 94 - 35
FreeAPS/Sources/Modules/DataTable/View/DataTableRootView.swift

@@ -8,6 +8,18 @@ extension DataTable {
         @State private var isRemoveCarbsAlertPresented = false
         @State private var removeCarbsAlert: Alert?
 
+        private var glucoseFormatter: NumberFormatter {
+            let formatter = NumberFormatter()
+            formatter.numberStyle = .decimal
+            formatter.maximumFractionDigits = 0
+            if state.units == .mmolL {
+                formatter.minimumFractionDigits = 1
+                formatter.maximumFractionDigits = 1
+            }
+            formatter.roundingMode = .halfUp
+            return formatter
+        }
+
         private var dateFormatter: DateFormatter {
             let formatter = DateFormatter()
             formatter.timeStyle = .short
@@ -15,54 +27,101 @@ extension DataTable {
         }
 
         var body: some View {
-            Form {
-                list
+            VStack {
+                Picker("Mode", selection: $state.mode) {
+                    ForEach(Mode.allCases.indexed(), id: \.1) { index, item in
+                        Text(item.name).tag(index)
+                    }
+                }
+                .pickerStyle(SegmentedPickerStyle())
+                .padding(.horizontal)
+
+                Form {
+                    switch state.mode {
+                    case .treatments: treatmentsList
+                    case .glucose: glucoseList
+                    }
+                }
             }
             .onAppear(perform: configureView)
             .navigationTitle("History")
             .navigationBarTitleDisplayMode(.automatic)
             .navigationBarItems(
-                leading: Button("Close", action: state.hideModal)
+                leading: Button("Close", action: state.hideModal),
+                trailing: state.mode == .glucose ? EditButton().asAny() : EmptyView().asAny()
             )
         }
 
-        private var list: some View {
+        private var treatmentsList: some View {
             List {
-                ForEach(state.items.indexed(), id: \.1.id) { _, item in
-                    HStack {
-                        Image(systemName: "circle.fill").foregroundColor(item.color)
-                        Text(dateFormatter.string(from: item.date))
-                            .moveDisabled(true)
-                        Text(item.type.name)
-                        Text(item.amountText).foregroundColor(.secondary)
-                        if let duration = item.durationText {
-                            Text(duration).foregroundColor(.secondary)
-                        }
+                ForEach(state.treatments) { item in
+                    treatmentView(item)
+                }
+            }
+        }
 
-                        if item.type == .carbs {
-                            Spacer()
-                            Image(systemName: "xmark.circle").foregroundColor(.secondary)
-                                .contentShape(Rectangle())
-                                .padding(.vertical)
-                                .onTapGesture {
-                                    removeCarbsAlert = Alert(
-                                        title: Text("Delete carbs?"),
-                                        message: Text(item.amountText),
-                                        primaryButton: .destructive(
-                                            Text("Delete"),
-                                            action: { state.deleteCarbs(at: item.date) }
-                                        ),
-                                        secondaryButton: .cancel()
-                                    )
-                                    isRemoveCarbsAlertPresented = true
-                                }
-                                .alert(isPresented: $isRemoveCarbsAlertPresented) {
-                                    removeCarbsAlert!
-                                }
+        private var glucoseList: some View {
+            List {
+                ForEach(state.glucose) { item in
+                    gluciseView(item)
+                }.onDelete(perform: deleteGlucose)
+            }
+        }
+
+        @ViewBuilder private func treatmentView(_ item: Treatment) -> some View {
+            HStack {
+                Image(systemName: "circle.fill").foregroundColor(item.color)
+                Text(dateFormatter.string(from: item.date))
+                    .moveDisabled(true)
+                Text(item.type.name)
+                Text(item.amountText).foregroundColor(.secondary)
+                if let duration = item.durationText {
+                    Text(duration).foregroundColor(.secondary)
+                }
+
+                if item.type == .carbs {
+                    Spacer()
+                    Image(systemName: "xmark.circle").foregroundColor(.secondary)
+                        .contentShape(Rectangle())
+                        .padding(.vertical)
+                        .onTapGesture {
+                            removeCarbsAlert = Alert(
+                                title: Text("Delete carbs?"),
+                                message: Text(item.amountText),
+                                primaryButton: .destructive(
+                                    Text("Delete"),
+                                    action: { state.deleteCarbs(at: item.date) }
+                                ),
+                                secondaryButton: .cancel()
+                            )
+                            isRemoveCarbsAlertPresented = true
+                        }
+                        .alert(isPresented: $isRemoveCarbsAlertPresented) {
+                            removeCarbsAlert!
                         }
-                    }
                 }
             }
         }
+
+        @ViewBuilder private func gluciseView(_ item: Glucose) -> some View {
+            VStack(alignment: .leading, spacing: 4) {
+                HStack {
+                    Text(dateFormatter.string(from: item.glucose.dateString))
+                    Spacer()
+                    Text(item.glucose.glucose.map {
+                        glucoseFormatter.string(from: Double(
+                            state.units == .mmolL ? $0.asMmolL : Decimal($0)
+                        ) as NSNumber)!
+                    } ?? "--")
+                    Text(state.units.rawValue)
+                    Text(item.glucose.direction?.symbol ?? "--")
+                }
+                Text("ID: " + item.glucose.id).font(.caption2).foregroundColor(.secondary)
+            }
+        }
+
+        private func deleteGlucose(at offsets: IndexSet) {
+            state.deleteGlucose(at: offsets[offsets.startIndex])
+        }
     }
 }

+ 22 - 0
FreeAPS/Sources/Services/HealthKit/HealthKitManager.swift

@@ -17,6 +17,8 @@ protocol HealthKitManager: GlucoseSource {
     func createObserver()
     /// Enable background delivering objects from Apple Health to FreeAPS
     func enableBackgroundDelivery()
+    /// Delete glucose with syncID
+    func deleteGlucise(syncID: String)
 }
 
 final class BaseHealthKitManager: HealthKitManager, Injectable {
@@ -319,6 +321,26 @@ final class BaseHealthKitManager: HealthKitManager, Injectable {
         }
         .eraseToAnyPublisher()
     }
+
+    func deleteGlucise(syncID: String) {
+        guard settingsManager.settings.useAppleHealth,
+              let sampleType = Config.healthBGObject,
+              checkAvailabilitySave(objectTypeToHealthStore: sampleType)
+        else { return }
+
+        processQueue.async {
+            let predicate = HKQuery.predicateForObjects(
+                withMetadataKey: HKMetadataKeySyncIdentifier,
+                operatorType: .equalTo,
+                value: syncID
+            )
+
+            self.healthKitStore.deleteObjects(of: sampleType, predicate: predicate) { _, _, error in
+                guard let error = error else { return }
+                warning(.service, "Cannot delete sample with syncID: \(syncID)", error: error)
+            }
+        }
+    }
 }
 
 enum HealthKitPermissionRequestStatus {