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

Disable save button with no changes; show saving animation

Deniz Cengiz 1 год назад
Родитель
Сommit
bfec47bff9

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

@@ -6,8 +6,8 @@ enum BasalProfileEditor {
 
     class Item: Identifiable, Hashable, Equatable {
         let id = UUID()
-        var rateIndex = 0
-        var timeIndex = 0
+        var rateIndex: Int
+        var timeIndex: Int
 
         init(rateIndex: Int, timeIndex: Int) {
             self.rateIndex = rateIndex
@@ -15,10 +15,11 @@ enum BasalProfileEditor {
         }
 
         static func == (lhs: Item, rhs: Item) -> Bool {
-            lhs.timeIndex == rhs.timeIndex
+            lhs.rateIndex == rhs.rateIndex && lhs.timeIndex == rhs.timeIndex
         }
 
         func hash(into hasher: inout Hasher) {
+            hasher.combine(rateIndex)
             hasher.combine(timeIndex)
         }
     }

+ 15 - 2
FreeAPS/Sources/Modules/BasalProfileEditor/BasalProfileEditorStateModel.swift

@@ -2,7 +2,8 @@ import SwiftUI
 
 extension BasalProfileEditor {
     final class StateModel: BaseStateModel<Provider> {
-        @Published var syncInProgress = false
+        @Published var syncInProgress: Bool = false
+        @Published var initialItems: [Item] = []
         @Published var items: [Item] = []
         @Published var total: Decimal = 0.0
 
@@ -15,6 +16,10 @@ extension BasalProfileEditor {
             return lastItem.timeIndex < timeValues.count - 1
         }
 
+        var hasChanges: Bool {
+            initialItems != items
+        }
+
         override func subscribe() {
             rateValues = provider.supportedBasalRates ?? stride(from: 5.0, to: 1001.0, by: 5.0)
                 .map { ($0.decimal ?? .zero) / 100 }
@@ -23,6 +28,9 @@ extension BasalProfileEditor {
                 let rateIndex = rateValues.firstIndex(of: value.rate) ?? 0
                 return Item(rateIndex: rateIndex, timeIndex: timeIndex)
             }
+
+            initialItems = items.map { Item(rateIndex: $0.rateIndex, timeIndex: $0.timeIndex) }
+
             calcTotal()
         }
 
@@ -58,6 +66,8 @@ extension BasalProfileEditor {
         }
 
         func save() {
+            guard hasChanges else { return }
+
             syncInProgress = true
             let profile = items.map { item -> BasalProfileEntry in
                 let fotmatter = DateFormatter()
@@ -72,6 +82,7 @@ extension BasalProfileEditor {
                 .receive(on: DispatchQueue.main)
                 .sink { _ in
                     self.syncInProgress = false
+                    self.initialItems = self.items.map { Item(rateIndex: $0.rateIndex, timeIndex: $0.timeIndex) }
                 } receiveValue: {}
                 .store(in: &lifetime)
         }
@@ -81,7 +92,9 @@ extension BasalProfileEditor {
                 let uniq = Array(Set(self.items))
                 let sorted = uniq.sorted { $0.timeIndex < $1.timeIndex }
                 sorted.first?.timeIndex = 0
-                self.items = sorted
+                if self.items != sorted {
+                    self.items = sorted
+                }
             }
         }
     }

+ 7 - 2
FreeAPS/Sources/Modules/BasalProfileEditor/View/BasalProfileEditorRootView.swift

@@ -40,6 +40,8 @@ extension BasalProfileEditor {
 
         var body: some View {
             Form {
+                let shouldDisableButton = state.syncInProgress || state.items.isEmpty || !state.hasChanges
+
                 Section(header: Text("Schedule")) {
                     list
                 }.listRowBackground(Color.chart)
@@ -69,11 +71,14 @@ extension BasalProfileEditor {
                         } label: {
                             Text(state.syncInProgress ? "Saving..." : "Save")
                         }
-                        .disabled(state.syncInProgress || state.items.isEmpty)
+                        .disabled(shouldDisableButton)
                         .frame(maxWidth: .infinity, alignment: .center)
                         .tint(.white)
                     }
-                }.listRowBackground(state.syncInProgress || state.items.isEmpty ? Color(.systemGray4) : Color(.systemBlue))
+                }.listRowBackground(shouldDisableButton ? Color(.systemGray4) : Color(.systemBlue))
+            }
+            .onChange(of: state.items) { _ in
+                state.calcTotal()
             }
             .scrollContentBackground(.hidden).background(color)
             .onAppear(perform: configureView)

+ 2 - 1
FreeAPS/Sources/Modules/CarbRatioEditor/CarbRatioEditorDataFlow.swift

@@ -14,11 +14,12 @@ enum CarbRatioEditor {
         }
 
         static func == (lhs: Item, rhs: Item) -> Bool {
-            lhs.timeIndex == rhs.timeIndex
+            lhs.timeIndex == rhs.timeIndex && lhs.rateIndex == rhs.rateIndex
         }
 
         func hash(into hasher: inout Hasher) {
             hasher.combine(timeIndex)
+            hasher.combine(rateIndex)
         }
     }
 }

+ 25 - 1
FreeAPS/Sources/Modules/CarbRatioEditor/CarbRatioEditorStateModel.swift

@@ -3,7 +3,9 @@ import SwiftUI
 extension CarbRatioEditor {
     final class StateModel: BaseStateModel<Provider> {
         @Published var items: [Item] = []
+        @Published var initialItems: [Item] = []
         @Published var autotune: Autotune?
+        @Published var shouldDisplaySaving: Bool = false
 
         let timeValues = stride(from: 0.0, to: 1.days.timeInterval, by: 30.minutes.timeInterval).map { $0 }
 
@@ -14,6 +16,20 @@ extension CarbRatioEditor {
             return lastItem.timeIndex < timeValues.count - 1
         }
 
+        var hasChanges: Bool {
+            if initialItems.count != items.count {
+                return true
+            }
+
+            for (initialItem, currentItem) in zip(initialItems, items) {
+                if initialItem.rateIndex != currentItem.rateIndex || initialItem.timeIndex != currentItem.timeIndex {
+                    return true
+                }
+            }
+
+            return false
+        }
+
         override func subscribe() {
             items = provider.profile.schedule.map { value in
                 let timeIndex = timeValues.firstIndex(of: Double(value.offset * 60)) ?? 0
@@ -21,6 +37,8 @@ extension CarbRatioEditor {
                 return Item(rateIndex: rateIndex, timeIndex: timeIndex)
             }
 
+            initialItems = items.map { Item(rateIndex: $0.rateIndex, timeIndex: $0.timeIndex) }
+
             autotune = provider.autotune
         }
 
@@ -38,6 +56,9 @@ extension CarbRatioEditor {
         }
 
         func save() {
+            guard hasChanges else { return }
+            shouldDisplaySaving = true
+
             let schedule = items.enumerated().map { _, item -> CarbRatioEntry in
                 let fotmatter = DateFormatter()
                 fotmatter.timeZone = TimeZone(secondsFromGMT: 0)
@@ -49,6 +70,7 @@ extension CarbRatioEditor {
             }
             let profile = CarbRatios(units: .grams, schedule: schedule)
             provider.saveProfile(profile)
+            initialItems = items.map { Item(rateIndex: $0.rateIndex, timeIndex: $0.timeIndex) }
         }
 
         func validate() {
@@ -56,7 +78,9 @@ extension CarbRatioEditor {
                 let uniq = Array(Set(self.items))
                 let sorted = uniq.sorted { $0.timeIndex < $1.timeIndex }
                 sorted.first?.timeIndex = 0
-                self.items = sorted
+                if self.items != sorted {
+                    self.items = sorted
+                }
             }
         }
     }

+ 23 - 11
FreeAPS/Sources/Modules/CarbRatioEditor/View/CarbRatioEditorRootView.swift

@@ -40,6 +40,8 @@ extension CarbRatioEditor {
 
         var body: some View {
             Form {
+                let shouldDisableButton = state.shouldDisplaySaving || state.items.isEmpty || !state.hasChanges
+
                 if let autotune = state.autotune, !state.settingsManager.settings.onlyAutotuneBasals {
                     Section(header: Text("Autotune")) {
                         HStack {
@@ -56,18 +58,28 @@ extension CarbRatioEditor {
                 }.listRowBackground(Color.chart)
 
                 Section {
-                    Button {
-                        let impactHeavy = UIImpactFeedbackGenerator(style: .heavy)
-                        impactHeavy.impactOccurred()
-                        state.save()
-                    } label: {
-                        Text("Save")
-                    }
-                    .disabled(state.items.isEmpty)
-                    .frame(maxWidth: .infinity, alignment: .center)
-                    .tint(.white)
+                    HStack {
+                        if state.shouldDisplaySaving {
+                            ProgressView().padding(.trailing, 10)
+                        }
 
-                }.listRowBackground(state.items.isEmpty ? Color(.systemGray4) : Color(.systemBlue))
+                        Button {
+                            let impactHeavy = UIImpactFeedbackGenerator(style: .heavy)
+                            impactHeavy.impactOccurred()
+                            state.save()
+
+                            // deactivate saving display after 1.25 seconds
+                            DispatchQueue.main.asyncAfter(deadline: .now() + 1.25) {
+                                state.shouldDisplaySaving = false
+                            }
+                        } label: {
+                            Text(state.shouldDisplaySaving ? "Saving..." : "Save")
+                        }
+                        .disabled(shouldDisableButton)
+                        .frame(maxWidth: .infinity, alignment: .center)
+                        .tint(.white)
+                    }
+                }.listRowBackground(shouldDisableButton ? Color(.systemGray4) : Color(.systemBlue))
             }
             .scrollContentBackground(.hidden).background(color)
             .onAppear(perform: configureView)

+ 2 - 1
FreeAPS/Sources/Modules/ISFEditor/ISFEditorDataFlow.swift

@@ -14,11 +14,12 @@ enum ISFEditor {
         }
 
         static func == (lhs: Item, rhs: Item) -> Bool {
-            lhs.timeIndex == rhs.timeIndex
+            lhs.timeIndex == rhs.timeIndex && lhs.rateIndex == rhs.rateIndex
         }
 
         func hash(into hasher: inout Hasher) {
             hasher.combine(timeIndex)
+            hasher.combine(rateIndex)
         }
     }
 }

+ 22 - 7
FreeAPS/Sources/Modules/ISFEditor/ISFEditorStateModel.swift

@@ -5,6 +5,8 @@ extension ISFEditor {
     final class StateModel: BaseStateModel<Provider> {
         @Injected() var determinationStorage: DeterminationStorage!
         @Published var items: [Item] = []
+        @Published var initialItems: [Item] = []
+        @Published var shouldDisplaySaving: Bool = false
         private(set) var autosensISF: Decimal?
         private(set) var autosensRatio: Decimal = 0
         @Published var autotune: Autotune?
@@ -24,6 +26,10 @@ extension ISFEditor {
             return lastItem.timeIndex < timeValues.count - 1
         }
 
+        var hasChanges: Bool {
+            initialItems != items
+        }
+
         private(set) var units: GlucoseUnits = .mgdL
 
         override func subscribe() {
@@ -37,6 +43,8 @@ extension ISFEditor {
                 return Item(rateIndex: rateIndex, timeIndex: timeIndex)
             }
 
+            initialItems = items.map { Item(rateIndex: $0.rateIndex, timeIndex: $0.timeIndex) }
+
             autotune = provider.autotune
 
             if let newISF = provider.autosense.newisf {
@@ -66,6 +74,9 @@ extension ISFEditor {
         }
 
         func save() {
+            guard hasChanges else { return }
+            shouldDisplaySaving.toggle()
+
             let sensitivities = items.map { item -> InsulinSensitivityEntry in
                 let fotmatter = DateFormatter()
                 fotmatter.timeZone = TimeZone(secondsFromGMT: 0)
@@ -81,17 +92,21 @@ extension ISFEditor {
                 sensitivities: sensitivities
             )
             provider.saveProfile(profile)
+            initialItems = items.map { Item(rateIndex: $0.rateIndex, timeIndex: $0.timeIndex) }
         }
 
         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
-
-                if self.items.isEmpty {
-                    self.units = self.settingsManager.settings.units
+                DispatchQueue.main.async {
+                    let uniq = Array(Set(self.items))
+                    let sorted = uniq.sorted { $0.timeIndex < $1.timeIndex }
+                    sorted.first?.timeIndex = 0
+                    if self.items != sorted {
+                        self.items = sorted
+                    }
+                    if self.items.isEmpty {
+                        self.units = self.settingsManager.settings.units
+                    }
                 }
             }
         }

+ 23 - 11
FreeAPS/Sources/Modules/ISFEditor/View/ISFEditorRootView.swift

@@ -41,6 +41,8 @@ extension ISFEditor {
 
         var body: some View {
             Form {
+                let shouldDisableButton = state.items.isEmpty || !state.hasChanges
+
                 if let autotune = state.autotune, !state.settingsManager.settings.onlyAutotuneBasals {
                     Section(header: Text("Autotune")) {
                         HStack {
@@ -97,18 +99,28 @@ extension ISFEditor {
                 }.listRowBackground(Color.chart)
 
                 Section {
-                    Button {
-                        let impactHeavy = UIImpactFeedbackGenerator(style: .heavy)
-                        impactHeavy.impactOccurred()
-                        state.save()
-                    } label: {
-                        Text("Save")
-                    }
-                    .disabled(state.items.isEmpty)
-                    .frame(maxWidth: .infinity, alignment: .center)
-                    .tint(.white)
+                    HStack {
+                        if state.shouldDisplaySaving {
+                            ProgressView().padding(.trailing, 10)
+                        }
+
+                        Button {
+                            let impactHeavy = UIImpactFeedbackGenerator(style: .heavy)
+                            impactHeavy.impactOccurred()
+                            state.save()
 
-                }.listRowBackground(state.items.isEmpty ? Color(.systemGray4) : Color(.systemBlue))
+                            // deactivate saving display after 1.25 seconds
+                            DispatchQueue.main.asyncAfter(deadline: .now() + 1.25) {
+                                state.shouldDisplaySaving = false
+                            }
+                        } label: {
+                            Text(state.shouldDisplaySaving ? "Saving..." : "Save")
+                        }
+                        .disabled(shouldDisableButton)
+                        .frame(maxWidth: .infinity, alignment: .center)
+                        .tint(.white)
+                    }
+                }.listRowBackground(shouldDisableButton ? Color(.systemGray4) : Color(.systemBlue))
             }
             .scrollContentBackground(.hidden).background(color)
             .onAppear(perform: configureView)

+ 3 - 1
FreeAPS/Sources/Modules/TargetsEditor/TargetsEditorDataFlow.swift

@@ -16,11 +16,13 @@ enum TargetsEditor {
         }
 
         static func == (lhs: Item, rhs: Item) -> Bool {
-            lhs.timeIndex == rhs.timeIndex
+            lhs.timeIndex == rhs.timeIndex && lhs.lowIndex == rhs.lowIndex && lhs.highIndex == rhs.highIndex
         }
 
         func hash(into hasher: inout Hasher) {
             hasher.combine(timeIndex)
+            hasher.combine(lowIndex)
+            hasher.combine(highIndex)
         }
     }
 }

+ 12 - 0
FreeAPS/Sources/Modules/TargetsEditor/TargetsEditorStateModel.swift

@@ -3,6 +3,8 @@ import SwiftUI
 extension TargetsEditor {
     final class StateModel: BaseStateModel<Provider> {
         @Published var items: [Item] = []
+        @Published var initialItems: [Item] = []
+        @Published var shouldDisplaySaving: Bool = false
 
         let timeValues = stride(from: 0.0, to: 1.days.timeInterval, by: 30.minutes.timeInterval).map { $0 }
 
@@ -15,6 +17,10 @@ extension TargetsEditor {
             return lastItem.timeIndex < timeValues.count - 1
         }
 
+        var hasChanges: Bool {
+            initialItems != items
+        }
+
         private(set) var units: GlucoseUnits = .mgdL
 
         override func subscribe() {
@@ -28,6 +34,8 @@ extension TargetsEditor {
                 let highIndex = rateValues.firstIndex(of: value.high) ?? 0
                 return Item(lowIndex: lowIndex, highIndex: highIndex, timeIndex: timeIndex)
             }
+
+            initialItems = items.map { Item(lowIndex: $0.lowIndex, highIndex: $0.highIndex, timeIndex: $0.timeIndex) }
         }
 
         func add() {
@@ -46,6 +54,9 @@ extension TargetsEditor {
         }
 
         func save() {
+            guard hasChanges else { return }
+            shouldDisplaySaving.toggle()
+
             let targets = items.map { item -> BGTargetEntry in
                 let formatter = DateFormatter()
                 formatter.timeZone = TimeZone(secondsFromGMT: 0)
@@ -58,6 +69,7 @@ extension TargetsEditor {
             }
             let profile = BGTargets(units: .mgdL, userPrefferedUnits: .mgdL, targets: targets)
             provider.saveProfile(profile)
+            initialItems = items.map { Item(lowIndex: $0.lowIndex, highIndex: $0.highIndex, timeIndex: $0.timeIndex) }
         }
 
         func validate() {

+ 23 - 11
FreeAPS/Sources/Modules/TargetsEditor/View/TargetsEditorRootView.swift

@@ -40,23 +40,35 @@ extension TargetsEditor {
 
         var body: some View {
             Form {
+                let shouldDisableButton = state.shouldDisplaySaving || state.items.isEmpty || !state.hasChanges
+
                 Section(header: Text("Schedule")) {
                     list
                 }.listRowBackground(Color.chart)
 
                 Section {
-                    Button {
-                        let impactHeavy = UIImpactFeedbackGenerator(style: .heavy)
-                        impactHeavy.impactOccurred()
-                        state.save()
-                    } label: {
-                        Text("Save")
-                    }
-                    .disabled(state.items.isEmpty)
-                    .frame(maxWidth: .infinity, alignment: .center)
-                    .tint(.white)
+                    HStack {
+                        if state.shouldDisplaySaving {
+                            ProgressView().padding(.trailing, 10)
+                        }
 
-                }.listRowBackground(state.items.isEmpty ? Color(.systemGray4) : Color(.systemBlue))
+                        Button {
+                            let impactHeavy = UIImpactFeedbackGenerator(style: .heavy)
+                            impactHeavy.impactOccurred()
+                            state.save()
+
+                            // deactivate saving display after 1.25 seconds
+                            DispatchQueue.main.asyncAfter(deadline: .now() + 1.25) {
+                                state.shouldDisplaySaving = false
+                            }
+                        } label: {
+                            Text(state.shouldDisplaySaving ? "Saving..." : "Save")
+                        }
+                        .disabled(shouldDisableButton)
+                        .frame(maxWidth: .infinity, alignment: .center)
+                        .tint(.white)
+                    }
+                }.listRowBackground(shouldDisableButton ? Color(.systemGray4) : Color(.systemBlue))
             }
             .scrollContentBackground(.hidden).background(color)
             .onAppear(perform: configureView)