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

Merge remote-tracking branch 'ivalkou/dev' into Crowdin

Jon B.M 4 лет назад
Родитель
Сommit
c51a050969

+ 12 - 0
FreeAPS.xcodeproj/project.pbxproj

@@ -81,6 +81,7 @@
 		385CEAC425F2F154002D6D5B /* AnnouncementsStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 385CEAC325F2F154002D6D5B /* AnnouncementsStorage.swift */; };
 		3862CC05273D152B00BF832C /* CalibrationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3862CC04273D152B00BF832C /* CalibrationService.swift */; };
 		3862CC1F273FDC9200BF832C /* CalibrationsChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3862CC1E273FDC9200BF832C /* CalibrationsChart.swift */; };
+		3862CC2E2743F9F700BF832C /* CalendarManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3862CC2D2743F9F700BF832C /* CalendarManager.swift */; };
 		386A124C271704DA00DDC61C /* CGMBLEKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 386A124B271704DA00DDC61C /* CGMBLEKit.framework */; };
 		386A124D271704DA00DDC61C /* CGMBLEKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 386A124B271704DA00DDC61C /* CGMBLEKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
 		386A124F271707F000DDC61C /* DexcomSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 386A124E271707F000DDC61C /* DexcomSource.swift */; };
@@ -398,6 +399,7 @@
 		385CEAC325F2F154002D6D5B /* AnnouncementsStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnnouncementsStorage.swift; sourceTree = "<group>"; };
 		3862CC04273D152B00BF832C /* CalibrationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalibrationService.swift; sourceTree = "<group>"; };
 		3862CC1E273FDC9200BF832C /* CalibrationsChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalibrationsChart.swift; sourceTree = "<group>"; };
+		3862CC2D2743F9F700BF832C /* CalendarManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarManager.swift; sourceTree = "<group>"; };
 		386A124B271704DA00DDC61C /* CGMBLEKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = CGMBLEKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		386A124E271707F000DDC61C /* DexcomSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DexcomSource.swift; sourceTree = "<group>"; };
 		3870FF4225EC13F40088248F /* BloodGlucose.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BloodGlucose.swift; sourceTree = "<group>"; };
@@ -791,6 +793,7 @@
 		3811DE9125C9D88200A708ED /* Services */ = {
 			isa = PBXGroup;
 			children = (
+				3862CC2C2743F9DC00BF832C /* Calendar */,
 				38AEE75025F021F10013F05B /* SettingsManager */,
 				38B4F3C425E5016800E76A18 /* Notifications */,
 				3811DE9225C9D88200A708ED /* Appearance */,
@@ -946,6 +949,14 @@
 			path = Calibrations;
 			sourceTree = "<group>";
 		};
+		3862CC2C2743F9DC00BF832C /* Calendar */ = {
+			isa = PBXGroup;
+			children = (
+				3862CC2D2743F9F700BF832C /* CalendarManager.swift */,
+			);
+			path = Calendar;
+			sourceTree = "<group>";
+		};
 		3883582E25EEAFC000E024B2 /* Views */ = {
 			isa = PBXGroup;
 			children = (
@@ -1651,6 +1662,7 @@
 				38FCF3FD25E997A80078B0D1 /* PumpHistoryStorage.swift in Sources */,
 				38D0B3B625EBE24900CB6E88 /* Battery.swift in Sources */,
 				38C4D33725E9A1A300D30B77 /* DispatchQueue+Extensions.swift in Sources */,
+				3862CC2E2743F9F700BF832C /* CalendarManager.swift in Sources */,
 				38B4F3C325E2A20B00E76A18 /* PumpSetupView.swift in Sources */,
 				382C134B25F14E3700715CE1 /* BGTargets.swift in Sources */,
 				38AEE75725F0F18E0013F05B /* CarbsStorage.swift in Sources */,

+ 2 - 2
FreeAPS.xcworkspace/xcshareddata/swiftpm/Package.resolved

@@ -24,8 +24,8 @@
         "repositoryURL": "https://github.com/ivalkou/LibreTransmitterX",
         "state": {
           "branch": null,
-          "revision": "97301803d0c9be325d5c31c0f0ecbc3c2c24a1da",
-          "version": "1.0.5"
+          "revision": "966c96777d801b4cd4b59bf293ed3c70160f006e",
+          "version": "1.0.6"
         }
       },
       {

+ 4 - 0
FreeAPS/Resources/Info.plist

@@ -65,6 +65,10 @@
 	</dict>
 	<key>UIApplicationSupportsIndirectInputEvents</key>
 	<true/>
+	<key>NFCReaderUsageDescription</key>
+	<string>NFC is used to scan Libre sensors.</string>
+	<key>NSCalendarsUsageDescription</key>
+	<string>Calendar is used to create a new glucose events.</string>
 	<key>UIBackgroundModes</key>
 	<array>
 		<string>bluetooth-central</string>

+ 2 - 1
FreeAPS/Resources/json/defaults/freeaps/freeaps_settings.json

@@ -10,5 +10,6 @@
     "insulinReqFraction": 0.7,
     "skipBolusScreenAfterCarbs": false,
     "cgm": "nightscout",
-    "uploadGlucose": false
+    "uploadGlucose": false,
+    "useCalendar": false
 }

+ 1 - 0
FreeAPS/Sources/Assemblies/ServiceAssembly.swift

@@ -13,5 +13,6 @@ final class ServiceAssembly: Assembly {
             reporter.setup()
             return reporter
         }
+        container.register(CalendarManager.self) { r in BaseCalendarManager(resilver: r) }
     }
 }

+ 1 - 0
FreeAPS/Sources/Models/FreeAPSSettings.swift

@@ -13,4 +13,5 @@ struct FreeAPSSettings: JSON, Equatable {
     var skipBolusScreenAfterCarbs: Bool?
     var cgm: CGMType?
     var uploadGlucose: Bool?
+    var useCalendar: Bool?
 }

+ 41 - 0
FreeAPS/Sources/Modules/CGM/CGMStateModel.swift

@@ -1,18 +1,27 @@
+import Combine
 import SwiftUI
 
 extension CGM {
     final class StateModel: BaseStateModel<Provider> {
         @Injected() var settingsManager: SettingsManager!
         @Injected() var libreSource: LibreTransmitterSource!
+        @Injected() var calendarManager: CalendarManager!
 
         @Published var cgm: CGMType = .nightscout
         @Published var transmitterID = ""
         @Published var uploadGlucose = false
+        @Published var createCalendarEvents = false
+        @Published var calendarIDs: [String] = []
+        @Published var currentCalendarID: String = ""
+        @Persisted(key: "CalendarManager.currentCalendarID") var storedCalendarID: String? = nil
 
         override func subscribe() {
             cgm = settingsManager.settings.cgm ?? .nightscout
             uploadGlucose = settingsManager.settings.uploadGlucose ?? false
             transmitterID = UserDefaults.standard.dexcomTransmitterID ?? ""
+            currentCalendarID = storedCalendarID ?? ""
+            calendarIDs = calendarManager.calendarIDs()
+            createCalendarEvents = settingsManager.settings.useCalendar ?? false
 
             $cgm
                 .removeDuplicates()
@@ -28,6 +37,38 @@ extension CGM {
                     self?.settingsManager.settings.uploadGlucose = value
                 }
                 .store(in: &lifetime)
+
+            $createCalendarEvents
+                .removeDuplicates()
+                .flatMap { [weak self] ok -> AnyPublisher<Bool, Never> in
+                    guard ok, let self = self else { return Just(false).eraseToAnyPublisher() }
+                    return self.calendarManager.requestAccessIfNeeded()
+                }
+                .map { [weak self] ok -> [String] in
+                    guard ok, let self = self else { return [] }
+                    return self.calendarManager.calendarIDs()
+                }
+                .receive(on: DispatchQueue.main)
+                .weakAssign(to: \.calendarIDs, on: self)
+                .store(in: &lifetime)
+
+            $createCalendarEvents
+                .removeDuplicates()
+                .sink { [weak self] use in
+                    self?.settingsManager.settings.useCalendar = use
+                }
+                .store(in: &lifetime)
+
+            $currentCalendarID
+                .removeDuplicates()
+                .sink { [weak self] id in
+                    guard id.isNotEmpty else {
+                        self?.calendarManager.currentCalendarID = nil
+                        return
+                    }
+                    self?.calendarManager.currentCalendarID = id
+                }
+                .store(in: &lifetime)
         }
 
         func onChangeID() {

+ 11 - 0
FreeAPS/Sources/Modules/CGM/View/CGMRootView.swift

@@ -37,6 +37,17 @@ extension CGM {
                     Text("Calibrations").navigationLink(to: .calibrations, from: self)
                 }
 
+                Section(header: Text("Calendar")) {
+                    Toggle("Create events in calendar", isOn: $state.createCalendarEvents)
+                    if state.calendarIDs.isNotEmpty {
+                        Picker("Calendar", selection: $state.currentCalendarID) {
+                            ForEach(state.calendarIDs, id: \.self) {
+                                Text($0).tag($0)
+                            }
+                        }
+                    }
+                }
+
                 Section(header: Text("Other")) {
                     Toggle("Upload glucose to Nightscout", isOn: $state.uploadGlucose)
                 }

+ 3 - 3
FreeAPS/Sources/Modules/CREditor/CREditorStateModel.swift

@@ -7,7 +7,7 @@ extension CREditor {
 
         let timeValues = stride(from: 0.0, to: 1.days.timeInterval, by: 30.minutes.timeInterval).map { $0 }
 
-        let rateValues = stride(from: 3, to: 50.01, by: 0.1).map { $0 }
+        let rateValues = stride(from: 3, to: 50.01, by: 0.1).map { Decimal($0) }
 
         var canAdd: Bool {
             guard let lastItem = items.last else { return true }
@@ -17,7 +17,7 @@ extension CREditor {
         override func subscribe() {
             items = provider.profile.schedule.map { value in
                 let timeIndex = timeValues.firstIndex(of: Double(value.offset * 60)) ?? 0
-                let rateIndex = rateValues.firstIndex(of: Double(value.ratio)) ?? 0
+                let rateIndex = rateValues.firstIndex(of: value.ratio) ?? 0
                 return Item(rateIndex: rateIndex, timeIndex: timeIndex)
             }
 
@@ -44,7 +44,7 @@ extension CREditor {
                 fotmatter.dateFormat = "HH:mm:ss"
                 let date = Date(timeIntervalSince1970: self.timeValues[item.timeIndex])
                 let minutes = Int(date.timeIntervalSince1970 / 60)
-                let rate = Decimal(self.rateValues[item.rateIndex])
+                let rate = self.rateValues[item.rateIndex]
                 return CarbRatioEntry(start: fotmatter.string(from: date), offset: minutes, ratio: rate)
             }
             let profile = CarbRatios(units: .grams, schedule: schedule)

+ 2 - 0
FreeAPS/Sources/Modules/Home/HomeStateModel.swift

@@ -8,6 +8,7 @@ extension Home {
         @Injected() var settingsManager: SettingsManager!
         @Injected() var apsManager: APSManager!
         @Injected() var nightscoutManager: NightscoutManager!
+        @Injected() var calendarManager: CalendarManager!
         private let timer = DispatchTimer(timeInterval: 5)
         private(set) var filteredHours = 24
 
@@ -145,6 +146,7 @@ extension Home {
                 } else {
                     self.glucoseDelta = nil
                 }
+                self.calendarManager.createEvent(for: self.recentGlucose, delta: self.glucoseDelta)
             }
         }
 

+ 2 - 2
FreeAPS/Sources/Modules/ISFEditor/ISFEditorStateModel.swift

@@ -13,9 +13,9 @@ extension ISFEditor {
         var rateValues: [Decimal] {
             switch units {
             case .mgdL:
-                return stride(from: 9, to: 540.01, by: 1.0).map { $0 }
+                return stride(from: 9, to: 540.01, by: 1.0).map { Decimal($0) }
             case .mmolL:
-                return stride(from: 0.1, to: 30.01, by: 0.1).map { $0 }
+                return stride(from: 0.1, to: 30.01, by: 0.1).map { Decimal($0) }
             }
         }
 

+ 161 - 0
FreeAPS/Sources/Services/Calendar/CalendarManager.swift

@@ -0,0 +1,161 @@
+import Combine
+import EventKit
+import Swinject
+
+protocol CalendarManager {
+    func requestAccessIfNeeded() -> AnyPublisher<Bool, Never>
+    func calendarIDs() -> [String]
+    var currentCalendarID: String? { get set }
+    func createEvent(for glucose: BloodGlucose?, delta: Int?)
+}
+
+final class BaseCalendarManager: CalendarManager, Injectable {
+    private lazy var eventStore: EKEventStore = { EKEventStore() }()
+
+    @Persisted(key: "CalendarManager.currentCalendarID") var currentCalendarID: String? = nil
+    @Injected() private var settingsManager: SettingsManager!
+
+    init(resilver: Resolver) {
+        injectServices(resilver)
+    }
+
+    func requestAccessIfNeeded() -> AnyPublisher<Bool, Never> {
+        Future { promise in
+            let status = EKEventStore.authorizationStatus(for: .event)
+            switch status {
+            case .notDetermined:
+                EKEventStore().requestAccess(to: .event) { granted, error in
+                    if let error = error {
+                        warning(.service, "Calendar access not granded", error: error)
+                    }
+                    promise(.success(granted))
+                }
+            case .denied,
+                 .restricted:
+                promise(.success(false))
+            case .authorized:
+                promise(.success(true))
+            @unknown default:
+                warning(.service, "Unknown calendar access status")
+                promise(.success(false))
+            }
+        }.eraseToAnyPublisher()
+    }
+
+    func calendarIDs() -> [String] {
+        EKEventStore().calendars(for: .event).map(\.title)
+    }
+
+    func createEvent(for glucose: BloodGlucose?, delta: Int?) {
+        guard settingsManager.settings.useCalendar ?? false else { return }
+
+        guard let calendar = currentCalendar else { return }
+
+        deleteAllEvents(in: calendar)
+
+        guard let glucose = glucose, let glucoseValue = glucose.glucose else { return }
+
+        // create an event now
+        let event = EKEvent(eventStore: eventStore)
+
+        let glucoseText = glucoseFormatter
+            .string(from: Double(
+                settingsManager.settings.units == .mmolL ?glucoseValue
+                    .asMmolL : Decimal(glucoseValue)
+            ) as NSNumber)!
+        let directionText = glucose.direction?.symbol ?? "↔︎"
+        let deltaText = delta
+            .map {
+                deltaFormatter
+                    .string(from: Double(settingsManager.settings.units == .mmolL ? $0.asMmolL : Decimal($0)) as NSNumber)!
+            } ?? "--"
+
+        let title = glucoseText + " " + directionText + " " + deltaText
+
+        event.title = title
+        event.notes = "FreeAPS X"
+        event.startDate = Date()
+        event.endDate = Date(timeIntervalSinceNow: 60 * 10)
+        event.calendar = calendar
+
+        do {
+            try eventStore.save(event, span: .thisEvent)
+        } catch {
+            warning(.service, "Cannot create calendar event", error: error)
+        }
+    }
+
+    var currentCalendar: EKCalendar? {
+        let calendars = eventStore.calendars(for: .event)
+        guard calendars.isNotEmpty else { return nil }
+        return calendars.first { $0.title == self.currentCalendarID }
+    }
+
+    private func deleteAllEvents(in calendar: EKCalendar) {
+        let predicate = eventStore.predicateForEvents(
+            withStart: Date(timeIntervalSinceNow: -24 * 3600),
+            end: Date(),
+            calendars: [calendar]
+        )
+
+        let events = eventStore.events(matching: predicate)
+
+        for event in events {
+            do {
+                try eventStore.remove(event, span: .thisEvent)
+            } catch {
+                warning(.service, "Cannot remove calendar events", error: error)
+            }
+        }
+    }
+
+    private var glucoseFormatter: NumberFormatter {
+        let formatter = NumberFormatter()
+        formatter.numberStyle = .decimal
+        formatter.maximumFractionDigits = 0
+        if settingsManager.settings.units == .mmolL {
+            formatter.minimumFractionDigits = 1
+            formatter.maximumFractionDigits = 1
+        }
+        return formatter
+    }
+
+    private var deltaFormatter: NumberFormatter {
+        let formatter = NumberFormatter()
+        formatter.numberStyle = .decimal
+        formatter.maximumFractionDigits = 2
+        formatter.positivePrefix = "+"
+        return formatter
+    }
+}
+
+extension BloodGlucose.Direction {
+    var symbol: String {
+        switch self {
+        case .tripleUp:
+            return "↑↑↑"
+        case .doubleUp:
+            return "↑↑"
+        case .singleUp:
+            return "↑"
+        case .fortyFiveUp:
+            return "↗︎"
+        case .flat:
+            return "→"
+        case .fortyFiveDown:
+            return "↘︎"
+        case .singleDown:
+            return "↓"
+        case .doubleDown:
+            return "↓↓"
+        case .tripleDown:
+            return "↓↓↓"
+        case .none:
+            return "↔︎"
+        case .notComputable:
+            return "↔︎"
+        case .rateOutOfRange:
+            return "↔︎"
+        }
+    }
+}