Ivan Valkou пре 5 година
родитељ
комит
a4b1fab086

+ 4 - 0
FreeAPS.xcodeproj/project.pbxproj

@@ -119,6 +119,7 @@
 		38A504A625DD9FDA00C5B9E8 /* OmniKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 38B17B2625DD6BBE005CAE3D /* OmniKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
 		38A504A725DD9FDA00C5B9E8 /* OmniKitUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 38B17B2A25DD6BBE005CAE3D /* OmniKitUI.framework */; };
 		38A504A825DD9FDA00C5B9E8 /* OmniKitUI.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 38B17B2A25DD6BBE005CAE3D /* OmniKitUI.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+		38A9260525F012D8009E3739 /* CarbRatios.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38A9260425F012D8009E3739 /* CarbRatios.swift */; };
 		38B17B6625DD90E0005CAE3D /* SwiftDate in Frameworks */ = {isa = PBXBuildFile; productRef = 38B17B6525DD90E0005CAE3D /* SwiftDate */; };
 		38B17B8625DD93BA005CAE3D /* LoopKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 38B17AD125DD6A40005CAE3D /* LoopKit.framework */; };
 		38B17B8725DD93BA005CAE3D /* LoopKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 38B17AD125DD6A40005CAE3D /* LoopKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
@@ -685,6 +686,7 @@
 		38A0364125ED069400FCBB52 /* TempBasal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TempBasal.swift; sourceTree = "<group>"; };
 		38A13D3125E28B4B00EAA382 /* PumpHistoryEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PumpHistoryEvent.swift; sourceTree = "<group>"; };
 		38A5049125DD9C4000C5B9E8 /* UserDefaultsExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserDefaultsExtensions.swift; sourceTree = "<group>"; };
+		38A9260425F012D8009E3739 /* CarbRatios.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarbRatios.swift; sourceTree = "<group>"; };
 		38B17AAE25DD69FA005CAE3D /* SwiftCharts.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = SwiftCharts.xcodeproj; path = SwiftCharts/SwiftCharts.xcodeproj; sourceTree = "<group>"; };
 		38B17AC025DD6A40005CAE3D /* LoopKit.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = LoopKit.xcodeproj; path = LoopKit/LoopKit.xcodeproj; sourceTree = "<group>"; };
 		38B17AF025DD6AE6005CAE3D /* MKRingProgressView.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = MKRingProgressView.xcodeproj; path = MKRingProgressView/MKRingProgressView.xcodeproj; sourceTree = "<group>"; };
@@ -1217,6 +1219,7 @@
 				3871F39B25ED892B0013ECB5 /* TempTarget.swift */,
 				3883583325EEB38000E024B2 /* PumpSettings.swift */,
 				388358C725EEF6D200E024B2 /* BasalProfileEntry.swift */,
+				38A9260425F012D8009E3739 /* CarbRatios.swift */,
 			);
 			path = Models;
 			sourceTree = "<group>";
@@ -1985,6 +1988,7 @@
 				3811DE3325C9D49500A708ED /* HomeBuilder.swift in Sources */,
 				3811DEA925C9D88300A708ED /* AppearanceManager.swift in Sources */,
 				38D0B3D925EC07C400CB6E88 /* CarbHystoryEntry.swift in Sources */,
+				38A9260525F012D8009E3739 /* CarbRatios.swift in Sources */,
 				3811DE2125C9D48300A708ED /* MainBuilder.swift in Sources */,
 				38FCF3D625E8FDF40078B0D1 /* MD5.swift in Sources */,
 				3811DE7925C9D6D300A708ED /* LoginViewModel.swift in Sources */,

+ 0 - 1
FreeAPS/Resources/json/defaults/settings/basal_profile.json

@@ -1,6 +1,5 @@
 [
     {
-        "i": 0,
         "start": "00:00:00",
         "minutes": 0,
         "rate": 1.0

+ 0 - 2
FreeAPS/Resources/json/defaults/settings/carb_ratios.json

@@ -2,8 +2,6 @@
     "units": "grams",
     "schedule": [
         {
-            "x": 0,
-            "i": 0,
             "start": "00:00:00",
             "offset": 0,
             "ratio": 10

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

@@ -1,7 +1,6 @@
 import Foundation
 
 struct BasalProfileEntry: JSON {
-    let i: Int
     let start: String
     let minutes: Int
     let rate: Decimal

+ 17 - 0
FreeAPS/Sources/Models/CarbRatios.swift

@@ -0,0 +1,17 @@
+import Foundation
+
+struct CarbRatios: JSON {
+    let units: CarbUnit
+    let schedule: [CarbRatioEntry]
+}
+
+struct CarbRatioEntry: JSON {
+    let start: String
+    let offset: Int
+    let ratio: Decimal
+}
+
+enum CarbUnit: String, JSON {
+    case grams
+    case exchanges
+}

+ 1 - 4
FreeAPS/Sources/Modules/BasalProfileEditor/BasalProfileEditorDataFlow.swift

@@ -1,11 +1,8 @@
 import Combine
 import Foundation
-import SwiftDate
 
 enum BasalProfileEditor {
-    enum Config {
-        static let maxItemsCount = 48
-    }
+    enum Config {}
 
     class Item: Identifiable, Hashable, Equatable {
         let id = UUID()

+ 3 - 6
FreeAPS/Sources/Modules/BasalProfileEditor/BasalProfileEditorViewModel.swift

@@ -2,13 +2,10 @@ import SwiftUI
 
 extension BasalProfileEditor {
     class ViewModel<Provider>: BaseViewModel<Provider>, ObservableObject where Provider: BasalProfileEditorProvider {
-        @Injected() var devicemanager: DeviceDataManager!
         @Published var syncInProgress = false
         @Published var items: [Item] = []
 
-        var timeValues: [TimeInterval] {
-            stride(from: 0.0, to: 1.days.timeInterval, by: 30.minutes.timeInterval).map { $0 }
-        }
+        let timeValues = stride(from: 0.0, to: 1.days.timeInterval, by: 30.minutes.timeInterval).map { $0 }
 
         private(set) var rateValues: [Double] = []
 
@@ -41,14 +38,14 @@ extension BasalProfileEditor {
 
         func save() {
             syncInProgress = true
-            let profile = items.enumerated().map { index, item -> BasalProfileEntry in
+            let profile = items.map { item -> BasalProfileEntry in
                 let fotmatter = DateFormatter()
                 fotmatter.timeZone = TimeZone(secondsFromGMT: 0)
                 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])
-                return BasalProfileEntry(i: index, start: fotmatter.string(from: date), minutes: minutes, rate: rate)
+                return BasalProfileEntry(start: fotmatter.string(from: date), minutes: minutes, rate: rate)
             }
             provider.saveProfile(profile)
                 .receive(on: DispatchQueue.main)

+ 25 - 1
FreeAPS/Sources/Modules/CREditor/CREditorDataFlow.swift

@@ -1,5 +1,29 @@
+import Foundation
+
 enum CREditor {
     enum Config {}
+
+    class Item: Identifiable, Hashable, Equatable {
+        let id = UUID()
+        var rateIndex = 0
+        var timeIndex = 0
+
+        init(rateIndex: Int, selectedIndex: Int) {
+            self.rateIndex = rateIndex
+            timeIndex = selectedIndex
+        }
+
+        static func == (lhs: Item, rhs: Item) -> Bool {
+            lhs.timeIndex == rhs.timeIndex
+        }
+
+        func hash(into hasher: inout Hasher) {
+            hasher.combine(timeIndex)
+        }
+    }
 }
 
-protocol CREditorProvider: Provider {}
+protocol CREditorProvider: Provider {
+    var profile: CarbRatios { get }
+    func saveProfile(_ profile: CarbRatios)
+}

+ 13 - 1
FreeAPS/Sources/Modules/CREditor/CREditorProvider.swift

@@ -1,3 +1,15 @@
+import Combine
+
 extension CREditor {
-    final class Provider: BaseProvider, CREditorProvider {}
+    final class Provider: BaseProvider, CREditorProvider {
+        var profile: CarbRatios {
+            (try? storage.retrieve(OpenAPS.Settings.carbRatios, as: CarbRatios.self))
+                ?? CarbRatios(from: OpenAPS.defaults(for: OpenAPS.Settings.carbRatios))
+                ?? CarbRatios(units: .grams, schedule: [CarbRatioEntry(start: "00:00:00", offset: 0, ratio: 10)])
+        }
+
+        func saveProfile(_ profile: CarbRatios) {
+            try? storage.save(profile, as: OpenAPS.Settings.carbRatios)
+        }
+    }
 }

+ 54 - 1
FreeAPS/Sources/Modules/CREditor/CREditorViewModel.swift

@@ -2,6 +2,59 @@ import SwiftUI
 
 extension CREditor {
     class ViewModel<Provider>: BaseViewModel<Provider>, ObservableObject where Provider: CREditorProvider {
-        override func subscribe() {}
+        @Published var items: [Item] = []
+
+        let timeValues = stride(from: 0.0, to: 1.days.timeInterval, by: 30.minutes.timeInterval).map { $0 }
+
+        let rateValues = stride(from: 2, to: 50.01, by: 1.0).map { $0 }
+
+        var canAdd: Bool {
+            guard let lastItem = items.last else { return true }
+            return lastItem.timeIndex < timeValues.count - 1
+        }
+
+        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
+                return Item(rateIndex: rateIndex, selectedIndex: timeIndex)
+            }
+        }
+
+        func add() {
+            var selected = 0
+            var rate = 0
+            if let last = items.last {
+                selected = last.timeIndex + 1
+                rate = last.rateIndex
+            }
+
+            let newItem = Item(rateIndex: rate, selectedIndex: selected)
+
+            items.append(newItem)
+        }
+
+        func save() {
+            let schedule = items.enumerated().map { _, item -> CarbRatioEntry in
+                let fotmatter = DateFormatter()
+                fotmatter.timeZone = TimeZone(secondsFromGMT: 0)
+                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])
+                return CarbRatioEntry(start: fotmatter.string(from: date), offset: minutes, ratio: rate)
+            }
+            let profile = CarbRatios(units: .grams, schedule: schedule)
+            provider.saveProfile(profile)
+        }
+
+        func validate() {
+            DispatchQueue.main.async {
+                let uniq = Array(Set(self.items))
+                let sorted = uniq.sorted { $0.timeIndex < $1.timeIndex }
+                sorted.first?.timeIndex = 0
+                self.items = sorted
+            }
+        }
     }
 }

+ 120 - 4
FreeAPS/Sources/Modules/CREditor/View/CREditorRootView.swift

@@ -3,12 +3,128 @@ import SwiftUI
 extension CREditor {
     struct RootView: BaseView {
         @EnvironmentObject var viewModel: ViewModel<Provider>
+        @State private var editMode = EditMode.inactive
+
+        private var dateFormatter: DateFormatter {
+            let formatter = DateFormatter()
+            formatter.timeZone = TimeZone(secondsFromGMT: 0)
+            formatter.timeStyle = .short
+            return formatter
+        }
+
+        private var rateFormatter: NumberFormatter {
+            let formatter = NumberFormatter()
+            formatter.numberStyle = .decimal
+            return formatter
+        }
 
         var body: some View {
-            Text("CREditor screen")
-                .navigationTitle("CREditor")
-                .navigationBarTitleDisplayMode(.automatic)
-                .navigationBarItems(leading: Button("Close", action: viewModel.hideModal))
+            Form {
+                Section(header: Text("Schedule")) {
+                    list
+                    addButton
+                }
+                Section {
+                    Button { viewModel.save() }
+                    label: {
+                        Text("Save")
+                    }
+                    .disabled(viewModel.items.isEmpty)
+                }
+            }
+            .navigationTitle("Carb Ratios")
+            .navigationBarTitleDisplayMode(.automatic)
+            .navigationBarItems(
+                leading: Button("Close", action: viewModel.hideModal),
+                trailing: EditButton()
+            )
+            .environment(\.editMode, $editMode)
+            .onAppear {
+                viewModel.validate()
+            }
+        }
+
+        private func pickers(for index: Int) -> some View {
+            GeometryReader { geometry in
+                VStack {
+                    HStack {
+                        Text("Ratio").frame(width: geometry.size.width / 2)
+                        Text("Time").frame(width: geometry.size.width / 2)
+                    }
+                    HStack(spacing: 0) {
+                        Picker(selection: $viewModel.items[index].rateIndex, label: EmptyView()) {
+                            ForEach(0 ..< viewModel.rateValues.count, id: \.self) { i in
+                                Text(
+                                    (
+                                        self.rateFormatter
+                                            .string(from: viewModel.rateValues[i] as NSNumber) ?? ""
+                                    ) + " g/U"
+                                ).tag(i)
+                            }
+                        }
+                        .frame(maxWidth: geometry.size.width / 2)
+                        .clipped()
+
+                        Picker(selection: $viewModel.items[index].timeIndex, label: EmptyView()) {
+                            ForEach(0 ..< viewModel.timeValues.count, id: \.self) { i in
+                                Text(
+                                    self.dateFormatter
+                                        .string(from: Date(
+                                            timeIntervalSince1970: viewModel
+                                                .timeValues[i]
+                                        ))
+                                ).tag(i)
+                            }
+                        }
+                        .frame(maxWidth: geometry.size.width / 2)
+                        .clipped()
+                    }
+                }
+            }
+        }
+
+        private var list: some View {
+            List {
+                ForEach(viewModel.items.indexed(), id: \.1.id) { index, item in
+                    NavigationLink(destination: pickers(for: index)) {
+                        HStack {
+                            Text("Ratio").foregroundColor(.secondary)
+                            Text(
+                                "\(rateFormatter.string(from: viewModel.rateValues[item.rateIndex] as NSNumber) ?? "0") g/U"
+                            )
+                            Spacer()
+                            Text("starts at").foregroundColor(.secondary)
+                            Text(
+                                "\(dateFormatter.string(from: Date(timeIntervalSince1970: viewModel.timeValues[item.timeIndex])))"
+                            )
+                        }
+                    }
+                    .moveDisabled(true)
+                }
+                .onDelete(perform: onDelete)
+            }
+        }
+
+        private var addButton: some View {
+            guard viewModel.canAdd else {
+                return AnyView(EmptyView())
+            }
+
+            switch editMode {
+            case .inactive:
+                return AnyView(Button(action: onAdd) { Text("Add") })
+            default:
+                return AnyView(EmptyView())
+            }
+        }
+
+        func onAdd() {
+            viewModel.add()
+        }
+
+        private func onDelete(offsets: IndexSet) {
+            viewModel.items.remove(atOffsets: offsets)
+            viewModel.validate()
         }
     }
 }

+ 3 - 0
FreeAPS/Sources/Modules/Settings/View/SettingsRootView.swift

@@ -42,6 +42,9 @@ extension Settings {
                     }
 
                     Group {
+                        Text("Pump profile").chevronCell()
+                            .modal(for: .configEditor(file: OpenAPS.Settings.pumpProfile), from: self)
+                        Text("Profile").chevronCell().modal(for: .configEditor(file: OpenAPS.Settings.profile), from: self)
                         Text("Glucose").chevronCell().modal(for: .configEditor(file: OpenAPS.Monitor.glucose), from: self)
                         Text("Suggested").chevronCell()
                             .modal(for: .configEditor(file: OpenAPS.Enact.suggested), from: self)