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

UI rework
* merge burger menu and settings, rename settings to menu
* fix for temp targets not displaying
* add images to menu view
* refactoring, alignment adjustments
* conditionally change the appearance of the cancel buttons for profiles and temp targets
* rework UI in temp targets to match the appearance of the profile view

polscm32 2 лет назад
Родитель
Сommit
4e43c3d494
18 измененных файлов с 906 добавлено и 368 удалено
  1. 5 9
      FreeAPS.xcodeproj/project.pbxproj
  2. 6 0
      FreeAPS/Resources/Assets.xcassets/settings_pics/Contents.json
  3. 21 0
      FreeAPS/Resources/Assets.xcassets/settings_pics/g6.imageset/Contents.json
  4. BIN
      FreeAPS/Resources/Assets.xcassets/settings_pics/g6.imageset/g6.png
  5. 0 0
      FreeAPS/Resources/Assets.xcassets/settings_pics/owl.imageset/Contents.json
  6. 0 0
      FreeAPS/Resources/Assets.xcassets/settings_pics/owl.imageset/apps.53525.13510798887505226.836b071a-2d62-4ce8-92bc-701910b6b96e.19f04184-2123-4d05-89b5-97f5c4f7a432-2 3 (kopia).png
  7. 0 0
      FreeAPS/Resources/Assets.xcassets/settings_pics/owl.imageset/apps.53525.13510798887505226.836b071a-2d62-4ce8-92bc-701910b6b96e.19f04184-2123-4d05-89b5-97f5c4f7a432-2 3.png
  8. 21 0
      FreeAPS/Resources/Assets.xcassets/settings_pics/pod.imageset/Contents.json
  9. BIN
      FreeAPS/Resources/Assets.xcassets/settings_pics/pod.imageset/pod2x.png
  10. 0 0
      FreeAPS/Sources/Helpers/CheckboxToggleStyle.swift
  11. 57 0
      FreeAPS/Sources/Helpers/SettingsRowView.swift
  12. 1 0
      FreeAPS/Sources/Modules/Home/View/Chart/MainChartView.swift
  13. 128 150
      FreeAPS/Sources/Modules/Home/View/HomeRootView.swift
  14. 22 0
      FreeAPS/Sources/Modules/OverrideProfilesConfig/OverrideProfilesDataFlow.swift
  15. 199 16
      FreeAPS/Sources/Modules/OverrideProfilesConfig/OverrideProfilesStateModel.swift
  16. 407 171
      FreeAPS/Sources/Modules/OverrideProfilesConfig/View/OverrideProfilesRootView.swift
  17. 38 14
      FreeAPS/Sources/Modules/Settings/View/SettingsRootView.swift
  18. 1 8
      FreeAPS/Sources/Modules/Stat/View/StatRootView.swift

+ 5 - 9
FreeAPS.xcodeproj/project.pbxproj

@@ -268,6 +268,7 @@
 		45717281F743594AA9D87191 /* ConfigEditorRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 920DDB21E5D0EB813197500D /* ConfigEditorRootView.swift */; };
 		5075C1608E6249A51495C422 /* TargetsEditorProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BDEA2DC60EDE0A3CA54DC73 /* TargetsEditorProvider.swift */; };
 		53F2382465BF74DB1A967C8B /* PumpConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8630D58BDAD6D9C650B9B39 /* PumpConfigProvider.swift */; };
+		587DA1F62B77F3DD00B28F8A /* SettingsRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587DA1F52B77F3DD00B28F8A /* SettingsRowView.swift */; };
 		5BFA1C2208114643B77F8CEB /* AddTempTargetProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEE53A13D26F101B332EFFC8 /* AddTempTargetProvider.swift */; };
 		5D16287A969E64D18CE40E44 /* PumpConfigStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F60E97100041040446F44E7 /* PumpConfigStateModel.swift */; };
 		61962FCAF8A2D222553AC5A3 /* LibreConfigDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66A5B83E7967C38F7CBD883C /* LibreConfigDataFlow.swift */; };
@@ -830,6 +831,7 @@
 		4DD795BA46B193644D48138C /* TargetsEditorRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TargetsEditorRootView.swift; sourceTree = "<group>"; };
 		500371C09F54F89A97D65FDB /* CalibrationsRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CalibrationsRootView.swift; sourceTree = "<group>"; };
 		505E09DC17A0C3D0AF4B66FE /* ISFEditorStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ISFEditorStateModel.swift; sourceTree = "<group>"; };
+		587DA1F52B77F3DD00B28F8A /* SettingsRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsRowView.swift; sourceTree = "<group>"; };
 		5B8A42073A2D03A278914448 /* AddTempTargetDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AddTempTargetDataFlow.swift; sourceTree = "<group>"; };
 		5C018D1680307A31C9ED7120 /* CGMStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CGMStateModel.swift; sourceTree = "<group>"; };
 		5D5B4F8B4194BB7E260EF251 /* ConfigEditorStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ConfigEditorStateModel.swift; sourceTree = "<group>"; };
@@ -1758,6 +1760,8 @@
 				FE66D16A291F74F8005D6F77 /* Bundle+Extensions.swift */,
 				FEFFA7A12929FE49007B8193 /* UIDevice+Extensions.swift */,
 				CEA4F62229BE10F70011ADF7 /* SavitzkyGolayFilter.swift */,
+				587DA1F52B77F3DD00B28F8A /* SettingsRowView.swift */,
+				BD2FF19F2AE29D43005D1C5D /* CheckboxToggleStyle.swift */,
 			);
 			path = Helpers;
 			sourceTree = "<group>";
@@ -2178,14 +2182,6 @@
 			path = View;
 			sourceTree = "<group>";
 		};
-		BD2FF19E2AE29D24005D1C5D /* Components */ = {
-			isa = PBXGroup;
-			children = (
-				BD2FF19F2AE29D43005D1C5D /* CheckboxToggleStyle.swift */,
-			);
-			path = Components;
-			sourceTree = "<group>";
-		};
 		BD7DA9A32AE06DBA00601B20 /* BolusCalculatorConfig */ = {
 			isa = PBXGroup;
 			children = (
@@ -2219,7 +2215,6 @@
 		C2C98283C436DB934D7E7994 /* Bolus */ = {
 			isa = PBXGroup;
 			children = (
-				BD2FF19E2AE29D24005D1C5D /* Components */,
 				C8D1A7CA8C10C4403D4BBFA7 /* BolusDataFlow.swift */,
 				C19984D62EFC0035A9E9644D /* BolusProvider.swift */,
 				223EC0494F55A91E3EA69EF4 /* BolusStateModel.swift */,
@@ -2867,6 +2862,7 @@
 				3811DEB025C9D88300A708ED /* BaseKeychain.swift in Sources */,
 				3811DE4325C9D4A100A708ED /* SettingsProvider.swift in Sources */,
 				45252C95D220E796FDB3B022 /* ConfigEditorDataFlow.swift in Sources */,
+				587DA1F62B77F3DD00B28F8A /* SettingsRowView.swift in Sources */,
 				3871F38725ED661C0013ECB5 /* Suggestion.swift in Sources */,
 				CE7CA34E2A064973004BE681 /* AppShortcuts.swift in Sources */,
 				38C4D33A25E9A1ED00D30B77 /* NSObject+AssociatedValues.swift in Sources */,

+ 6 - 0
FreeAPS/Resources/Assets.xcassets/settings_pics/Contents.json

@@ -0,0 +1,6 @@
+{
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

+ 21 - 0
FreeAPS/Resources/Assets.xcassets/settings_pics/g6.imageset/Contents.json

@@ -0,0 +1,21 @@
+{
+  "images" : [
+    {
+      "filename" : "g6.png",
+      "idiom" : "universal",
+      "scale" : "1x"
+    },
+    {
+      "idiom" : "universal",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "universal",
+      "scale" : "3x"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

BIN
FreeAPS/Resources/Assets.xcassets/settings_pics/g6.imageset/g6.png


FreeAPS/Resources/Assets.xcassets/owl.imageset/Contents.json → FreeAPS/Resources/Assets.xcassets/settings_pics/owl.imageset/Contents.json


FreeAPS/Resources/Assets.xcassets/owl.imageset/apps.53525.13510798887505226.836b071a-2d62-4ce8-92bc-701910b6b96e.19f04184-2123-4d05-89b5-97f5c4f7a432-2 3 (kopia).png → FreeAPS/Resources/Assets.xcassets/settings_pics/owl.imageset/apps.53525.13510798887505226.836b071a-2d62-4ce8-92bc-701910b6b96e.19f04184-2123-4d05-89b5-97f5c4f7a432-2 3 (kopia).png


FreeAPS/Resources/Assets.xcassets/owl.imageset/apps.53525.13510798887505226.836b071a-2d62-4ce8-92bc-701910b6b96e.19f04184-2123-4d05-89b5-97f5c4f7a432-2 3.png → FreeAPS/Resources/Assets.xcassets/settings_pics/owl.imageset/apps.53525.13510798887505226.836b071a-2d62-4ce8-92bc-701910b6b96e.19f04184-2123-4d05-89b5-97f5c4f7a432-2 3.png


+ 21 - 0
FreeAPS/Resources/Assets.xcassets/settings_pics/pod.imageset/Contents.json

@@ -0,0 +1,21 @@
+{
+  "images" : [
+    {
+      "filename" : "pod2x.png",
+      "idiom" : "universal",
+      "scale" : "1x"
+    },
+    {
+      "idiom" : "universal",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "universal",
+      "scale" : "3x"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

BIN
FreeAPS/Resources/Assets.xcassets/settings_pics/pod.imageset/pod2x.png


FreeAPS/Sources/Modules/Bolus/Components/CheckboxToggleStyle.swift → FreeAPS/Sources/Helpers/CheckboxToggleStyle.swift


+ 57 - 0
FreeAPS/Sources/Helpers/SettingsRowView.swift

@@ -0,0 +1,57 @@
+import SwiftUI
+
+struct SettingsRowView: View {
+    let imageName: String
+    let title: String
+    let tint: Color
+    let spacing: CGFloat?
+    let font: CGFloat?
+
+    init(imageName: String, title: String, tint: Color, spacing: CGFloat? = 12, font: CGFloat? = 35) {
+        self.imageName = imageName
+        self.title = title
+        self.tint = tint
+        self.spacing = spacing
+        self.font = font
+    }
+
+    var body: some View {
+        HStack(spacing: spacing ?? 12, content: {
+            Image(systemName: imageName)
+                .imageScale(.small)
+                .font(.system(size: font ?? 35))
+                .foregroundColor(tint)
+
+            Text(title)
+                .font(.subheadline)
+                .foregroundStyle(.primary)
+        })
+    }
+}
+
+struct SettingsRowViewCustomImage: View {
+    let imageName: String
+    let title: String
+    let frame: CGFloat?
+    let spacing: CGFloat?
+
+    init(imageName: String, title: String, frame: CGFloat? = 35, spacing: CGFloat? = 12) {
+        self.imageName = imageName
+        self.title = title
+        self.frame = frame
+        self.spacing = spacing
+    }
+
+    var body: some View {
+        HStack(spacing: spacing ?? 12, content: {
+            Image(imageName)
+                .resizable()
+                .aspectRatio(contentMode: .fit)
+                .frame(width: frame ?? 35, height: frame ?? 35)
+
+            Text(title)
+                .font(.subheadline)
+                .foregroundStyle(.primary)
+        })
+    }
+}

+ 1 - 0
FreeAPS/Sources/Modules/Home/View/Chart/MainChartView.swift

@@ -164,6 +164,7 @@ struct MainChartView: View {
                         calculateFpus()
                         calculateCarbs()
                         calculatePredictions()
+                        calculateTTs()
                         updateStartEndMarkers()
                         scroller.scrollTo("MainChart", anchor: .trailing)
                     }

+ 128 - 150
FreeAPS/Sources/Modules/Home/View/HomeRootView.swift

@@ -149,24 +149,22 @@ extension Home {
                 lowGlucose: $state.lowGlucose,
                 highGlucose: $state.highGlucose
             ).scaleEffect(0.9)
-            /*
-                 .onTapGesture {
-                     if state.alarm == nil {
-                         state.openCGM()
-                     } else {
-                         state.showModal(for: .snooze)
-                     }
-                 }
-                 .onLongPressGesture {
-                     let impactHeavy = UIImpactFeedbackGenerator(style: .heavy)
-                     impactHeavy.impactOccurred()
-                     if state.alarm == nil {
-                         state.showModal(for: .snooze)
-                     } else {
-                         state.openCGM()
-                     }
-                 }
-             */
+                .onTapGesture {
+                    if state.alarm == nil {
+                        state.openCGM()
+                    } else {
+                        state.showModal(for: .snooze)
+                    }
+                }
+                .onLongPressGesture {
+                    let impactHeavy = UIImpactFeedbackGenerator(style: .heavy)
+                    impactHeavy.impactOccurred()
+                    if state.alarm == nil {
+                        state.showModal(for: .snooze)
+                    } else {
+                        state.openCGM()
+                    }
+                }
         }
 
         var pumpView: some View {
@@ -178,7 +176,11 @@ extension Home {
                 timerDate: $state.timerDate,
                 timeZone: $state.timeZone,
                 state: state
-            )
+            ).onTapGesture {
+                if state.pumpDisplayState != nil {
+                    state.setupPump = true
+                }
+            }
         }
 
         var tempBasalString: String? {
@@ -702,111 +704,111 @@ extension Home {
                 }.clipShape(RoundedRectangle(cornerRadius: 15))
         }
 
-        @ViewBuilder func menuSymbols(action: @escaping () -> Void, systemName: String) -> some View {
-            Button(
-                action: action,
-                label: {
-                    HStack {
-                        Image(systemName: systemName)
-                            .font(.system(size: 21))
-                            .foregroundStyle(colorScheme == .dark ? .white : .black)
-                    }.padding(.top, 1)
-                }
-            )
-        }
-
-        @ViewBuilder func menuElements(action: @escaping () -> Void, title: String) -> some View {
-            Button(
-                action: action,
-                label: {
-                    HStack {
-                        Text(title)
-                            .font(.system(size: 19))
-                            .foregroundStyle(colorScheme == .dark ? .white : .black)
-                        Spacer()
-                        Image(systemName: "arrow.right")
-                            .font(.system(size: 21))
-                            .foregroundStyle(colorScheme == .dark ? .white : .black)
-                    }.padding(.top, 1)
-                }
-            )
-        }
-
-        @ViewBuilder func sideMenuView() -> some View {
-            ZStack {
-                RoundedRectangle(cornerRadius: 8)
-                    .fill(color)
-                    .shadow(
-                        color: Color.black.opacity(0.33),
-                        radius: 3
-                    )
-                    .ignoresSafeArea(edges: .all)
-
-                VStack(alignment: .leading) {
-                    Button {
-                        isMenuPresented.toggle()
-                    } label: {
-                        HStack {
-                            Image(systemName: "arrow.left")
-                                .font(.system(size: 30))
-                                .foregroundStyle(colorScheme == .dark ? .white : .black)
-                            Text("Menu")
-                                .font(.system(size: 30)).fontWeight(.bold)
-                                .foregroundStyle(colorScheme == .dark ? .white : .black)
-                        }
-                    }
-                    .padding(.top, 60)
-
-                    HStack(spacing: 15) {
-                        VStack(alignment: .leading, spacing: 25, content: {
-                            menuSymbols(action: { state.showModal(for: .statistics) }, systemName: "chart.bar.xaxis")
-                                .padding(.top, 20)
-
-                            menuSymbols(action: {
-                                if state.pumpDisplayState != nil {
-                                    state.setupPump = true
-                                }
-                            }, systemName: "cross.vial.fill")
-
-                            menuSymbols(action: {
-                                if state.alarm == nil {
-                                    state.openCGM()
-                                } else {
-                                    state.showModal(for: .snooze)
-                                }
-                            }, systemName: "sensor.tag.radiowaves.forward.fill")
-
-                            menuSymbols(action: { state.showModal(for: .addTempTarget) }, systemName: "target")
-
-                            Spacer()
-                        })
-                        VStack(alignment: .leading, spacing: 25, content: {
-                            menuElements(action: { state.showModal(for: .statistics) }, title: "Statistics")
-                                .padding(.top, 20)
-
-                            menuElements(action: {
-                                if state.pumpDisplayState != nil {
-                                    state.setupPump = true
-                                }
-                            }, title: "Pump Settings")
-
-                            menuElements(action: {
-                                if state.alarm == nil {
-                                    state.openCGM()
-                                } else {
-                                    state.showModal(for: .snooze)
-                                }
-                            }, title: "CGM")
-
-                            menuElements(action: { state.showModal(for: .addTempTarget) }, title: "Temp targets")
-
-                            Spacer()
-                        })
-                    }
-                }.padding(.horizontal, 25)
-            }
-            .frame(width: UIScreen.main.bounds.width / 1.2, height: UIScreen.main.bounds.height - 20)
-        }
+//        @ViewBuilder func menuSymbols(action: @escaping () -> Void, systemName: String) -> some View {
+//            Button(
+//                action: action,
+//                label: {
+//                    HStack {
+//                        Image(systemName: systemName)
+//                            .font(.system(size: 21))
+//                            .foregroundStyle(colorScheme == .dark ? .white : .black)
+//                    }.padding(.top, 1)
+//                }
+//            )
+//        }
+//
+//        @ViewBuilder func menuElements(action: @escaping () -> Void, title: String) -> some View {
+//            Button(
+//                action: action,
+//                label: {
+//                    HStack {
+//                        Text(title)
+//                            .font(.system(size: 19))
+//                            .foregroundStyle(colorScheme == .dark ? .white : .black)
+//                        Spacer()
+//                        Image(systemName: "arrow.right")
+//                            .font(.system(size: 21))
+//                            .foregroundStyle(colorScheme == .dark ? .white : .black)
+//                    }.padding(.top, 1)
+//                }
+//            )
+//        }
+//
+//        @ViewBuilder func sideMenuView() -> some View {
+//            ZStack {
+//                RoundedRectangle(cornerRadius: 8)
+//                    .fill(color)
+//                    .shadow(
+//                        color: Color.black.opacity(0.33),
+//                        radius: 3
+//                    )
+//                    .ignoresSafeArea(edges: .all)
+//
+//                VStack(alignment: .leading) {
+//                    Button {
+//                        isMenuPresented.toggle()
+//                    } label: {
+//                        HStack {
+//                            Image(systemName: "arrow.left")
+//                                .font(.system(size: 30))
+//                                .foregroundStyle(colorScheme == .dark ? .white : .black)
+//                            Text("Menu")
+//                                .font(.system(size: 30)).fontWeight(.bold)
+//                                .foregroundStyle(colorScheme == .dark ? .white : .black)
+//                        }
+//                    }
+//                    .padding(.top, 60)
+//
+//                    HStack(spacing: 15) {
+//                        VStack(alignment: .leading, spacing: 25, content: {
+        ////                            menuSymbols(action: { state.showModal(for: .statistics) }, systemName: "chart.bar.xaxis")
+        ////                                .padding(.top, 20)
+//
+//                            menuSymbols(action: {
+//                                if state.pumpDisplayState != nil {
+//                                    state.setupPump = true
+//                                }
+//                            }, systemName: "cross.vial.fill")
+//
+//                            menuSymbols(action: {
+//                                if state.alarm == nil {
+//                                    state.openCGM()
+//                                } else {
+//                                    state.showModal(for: .snooze)
+//                                }
+//                            }, systemName: "sensor.tag.radiowaves.forward.fill")
+//
+//                            menuSymbols(action: { state.showModal(for: .addTempTarget) }, systemName: "target")
+//
+//                            Spacer()
+//                        })
+//                        VStack(alignment: .leading, spacing: 25, content: {
+//                            menuElements(action: { state.showModal(for: .statistics) }, title: "Statistics")
+//                                .padding(.top, 20)
+//
+//                            menuElements(action: {
+//                                if state.pumpDisplayState != nil {
+//                                    state.setupPump = true
+//                                }
+//                            }, title: "Pump Settings")
+//
+//                            menuElements(action: {
+//                                if state.alarm == nil {
+//                                    state.openCGM()
+//                                } else {
+//                                    state.showModal(for: .snooze)
+//                                }
+//                            }, title: "CGM")
+//
+//                            menuElements(action: { state.showModal(for: .addTempTarget) }, title: "Temp targets")
+//
+//                            Spacer()
+//                        })
+//                    }
+//                }.padding(.horizontal, 25)
+//            }
+//            .frame(width: UIScreen.main.bounds.width / 1.2, height: UIScreen.main.bounds.height - 20)
+//        }
 
         @ViewBuilder func mainView() -> some View {
             GeometryReader { geo in
@@ -829,18 +831,6 @@ extension Home {
                             pumpView
                             Spacer()
                         }.padding(.leading, 20)
-
-                        HStack {
-                            Spacer()
-                            Button {
-                                isMenuPresented.toggle()
-                            }
-                            label: {
-                                Image(systemName: "text.justify")
-                                    .font(.body).foregroundStyle(colorScheme == .dark ? Color.white : Color.black)
-                            }.padding(.trailing, 20).padding(.bottom, 110)
-                        }
-
                     }.padding(.top, 10)
 
                     mealPanel(geo).padding(.top, 30).padding(.bottom, 20)
@@ -957,11 +947,6 @@ extension Home {
                     }) {
                         Image(systemName: "plus.circle.fill")
                             .font(.system(size: 45))
-                            .shadow(
-                                color: colorScheme == .dark ? Color(red: 0.02745098039, green: 0.1098039216, blue: 0.1411764706) :
-                                    Color.black.opacity(0.33),
-                                radius: 3
-                            )
                             .foregroundStyle(Color.tabBar)
                             .padding(.bottom, 2)
                     }
@@ -972,21 +957,14 @@ extension Home {
                         label: "Profile"
                     )
                     Spacer()
-                    tabBarButton(index: 3, systemName: "gear", label: "Settings")
+                    tabBarButton(index: 3, systemName: "text.justify", label: "Menu")
                 }
                 .padding(.horizontal, 20)
             }.blur(radius: isMenuPresented ? 5 : 0)
         }
 
         var body: some View {
-            ZStack(alignment: .trailing) {
-                customTabBar()
-
-                // burger menu
-                if isMenuPresented {
-                    sideMenuView().background(Color.chart)
-                }
-            }
+            customTabBar()
         }
 
         private var popup: some View {

+ 22 - 0
FreeAPS/Sources/Modules/OverrideProfilesConfig/OverrideProfilesDataFlow.swift

@@ -1,5 +1,27 @@
+import Foundation
+import SwiftUI
+
 enum OverrideProfilesConfig {
     enum Config {}
+
+    enum Tab: String, Hashable, Identifiable, CaseIterable {
+        case profiles
+        case tempTargets
+
+        var id: String { rawValue }
+
+        var name: String {
+            var name: String = ""
+            switch self {
+            case .profiles:
+                name = "Profiles"
+            case .tempTargets:
+                name = "Temp Targets"
+            }
+
+            return NSLocalizedString(name, comment: "Selected Tab")
+        }
+    }
 }
 
 protocol OverrideProfilesProvider: Provider {}

+ 199 - 16
FreeAPS/Sources/Modules/OverrideProfilesConfig/OverrideProfilesStateModel.swift

@@ -3,17 +3,20 @@ import SwiftUI
 
 extension OverrideProfilesConfig {
     final class StateModel: BaseStateModel<Provider> {
-        @Published var percentage: Double = 100
+        @Injected() var storage: TempTargetsStorage!
+        @Injected() var apsManager: APSManager!
+
+        @Published var percentageProfiles: Double = 100
         @Published var isEnabled = false
         @Published var _indefinite = true
-        @Published var duration: Decimal = 0
+        @Published var durationProfile: Decimal = 0
         @Published var target: Decimal = 0
         @Published var override_target: Bool = false
         @Published var smbIsOff: Bool = false
         @Published var id: String = ""
         @Published var profileName: String = ""
         @Published var isPreset: Bool = false
-        @Published var presets: [OverridePresets] = []
+        @Published var presetsProfiles: [OverridePresets] = []
         @Published var selection: OverridePresets?
         @Published var advancedSettings: Bool = false
         @Published var isfAndCr: Bool = true
@@ -26,14 +29,31 @@ extension OverrideProfilesConfig {
         @Published var uamMinutes: Decimal = 0
         @Published var defaultSmbMinutes: Decimal = 0
         @Published var defaultUamMinutes: Decimal = 0
+        @Published var selectedTab: Tab = .profiles
 
         var units: GlucoseUnits = .mmolL
 
+        // temp target stuff
+        @Published var low: Decimal = 0
+        // @Published var target: Decimal = 0
+        @Published var high: Decimal = 0
+        @Published var durationTT: Decimal = 0
+        @Published var date = Date()
+        @Published var newPresetName = ""
+        @Published var presetsTT: [TempTarget] = []
+        @Published var percentageTT = 100.0
+        @Published var maxValue: Decimal = 1.2
+        @Published var viewPercantage = false
+        @Published var hbt: Double = 160
+        @Published var didSaveSettings: Bool = false
+
         override func subscribe() {
             units = settingsManager.settings.units
             defaultSmbMinutes = settingsManager.preferences.maxSMBBasalMinutes
             defaultUamMinutes = settingsManager.preferences.maxUAMSMBBasalMinutes
-            presets = [OverridePresets(context: coredataContext)]
+            presetsProfiles = [OverridePresets(context: coredataContext)]
+            presetsTT = storage.presets()
+            maxValue = settingsManager.preferences.autosensMax
         }
 
         let coredataContext = CoreDataStack.shared.persistentContainer.viewContext
@@ -41,9 +61,9 @@ extension OverrideProfilesConfig {
         func saveSettings() {
             coredataContext.perform { [self] in
                 let saveOverride = Override(context: self.coredataContext)
-                saveOverride.duration = self.duration as NSDecimalNumber
+                saveOverride.duration = self.durationProfile as NSDecimalNumber
                 saveOverride.indefinite = self._indefinite
-                saveOverride.percentage = self.percentage
+                saveOverride.percentage = self.percentageProfiles
                 saveOverride.enabled = true
                 saveOverride.smbIsOff = self.smbIsOff
                 if self.isPreset {
@@ -82,9 +102,9 @@ extension OverrideProfilesConfig {
         func savePreset() {
             coredataContext.perform { [self] in
                 let saveOverride = OverridePresets(context: self.coredataContext)
-                saveOverride.duration = self.duration as NSDecimalNumber
+                saveOverride.duration = self.durationProfile as NSDecimalNumber
                 saveOverride.indefinite = self._indefinite
-                saveOverride.percentage = self.percentage
+                saveOverride.percentage = self.percentageProfiles
                 saveOverride.smbIsOff = self.smbIsOff
                 saveOverride.name = self.profileName
                 id = UUID().uuidString
@@ -169,9 +189,9 @@ extension OverrideProfilesConfig {
                 // requestEnabled.fetchLimit = 1
                 try? overrideArray = coredataContext.fetch(requestEnabled)
                 isEnabled = overrideArray.first?.enabled ?? false
-                percentage = overrideArray.first?.percentage ?? 100
+                percentageProfiles = overrideArray.first?.percentage ?? 100
                 _indefinite = overrideArray.first?.indefinite ?? true
-                duration = (overrideArray.first?.duration ?? 0) as Decimal
+                durationProfile = (overrideArray.first?.duration ?? 0) as Decimal
                 smbIsOff = overrideArray.first?.smbIsOff ?? false
                 advancedSettings = overrideArray.first?.advancedSettings ?? false
                 isfAndCr = overrideArray.first?.isfAndCr ?? true
@@ -198,7 +218,7 @@ extension OverrideProfilesConfig {
 
                 let overrideTarget = (overrideArray.first?.target ?? 0) as Decimal
 
-                var newDuration = Double(duration)
+                var newDuration = Double(durationProfile)
                 if isEnabled {
                     let duration = overrideArray.first?.duration ?? 0
                     let addedMinutes = Int(duration as Decimal)
@@ -213,12 +233,12 @@ extension OverrideProfilesConfig {
                     }
                 }
 
-                if newDuration < 0 { newDuration = 0 } else { duration = Decimal(newDuration) }
+                if newDuration < 0 { newDuration = 0 } else { durationProfile = Decimal(newDuration) }
 
                 if !isEnabled {
                     _indefinite = true
-                    percentage = 100
-                    duration = 0
+                    percentageProfiles = 100
+                    durationProfile = 0
                     target = 0
                     override_target = false
                     smbIsOff = false
@@ -232,8 +252,8 @@ extension OverrideProfilesConfig {
         func cancelProfile() {
             _indefinite = true
             isEnabled = false
-            percentage = 100
-            duration = 0
+            percentageProfiles = 100
+            durationProfile = 0
             target = 0
             override_target = false
             smbIsOff = false
@@ -247,5 +267,168 @@ extension OverrideProfilesConfig {
             smbMinutes = defaultSmbMinutes
             uamMinutes = defaultUamMinutes
         }
+
+        // MARK: TEMP TARGET
+
+        func enact() {
+            guard durationTT > 0 else {
+                return
+            }
+            var lowTarget = low
+
+            if viewPercantage {
+                lowTarget = Decimal(round(Double(computeTarget())))
+                coredataContext.performAndWait {
+                    let saveToCoreData = TempTargets(context: self.coredataContext)
+                    saveToCoreData.id = UUID().uuidString
+                    saveToCoreData.active = true
+                    saveToCoreData.hbt = hbt
+                    saveToCoreData.date = Date()
+                    saveToCoreData.duration = durationTT as NSDecimalNumber
+                    saveToCoreData.startDate = Date()
+                    try? self.coredataContext.save()
+                }
+                didSaveSettings = true
+            } else {
+                coredataContext.performAndWait {
+                    let saveToCoreData = TempTargets(context: coredataContext)
+                    saveToCoreData.active = false
+                    saveToCoreData.date = Date()
+                    try? coredataContext.save()
+                }
+            }
+            var highTarget = lowTarget
+
+            if units == .mmolL, !viewPercantage {
+                lowTarget = Decimal(round(Double(lowTarget.asMgdL)))
+                highTarget = lowTarget
+            }
+
+            let entry = TempTarget(
+                name: TempTarget.custom,
+                createdAt: date,
+                targetTop: highTarget,
+                targetBottom: lowTarget,
+                duration: durationTT,
+                enteredBy: TempTarget.manual,
+                reason: TempTarget.custom
+            )
+            storage.storeTempTargets([entry])
+            showModal(for: nil)
+        }
+
+        func cancel() {
+            storage.storeTempTargets([TempTarget.cancel(at: Date())])
+            showModal(for: nil)
+
+            coredataContext.performAndWait {
+                let saveToCoreData = TempTargets(context: self.coredataContext)
+                saveToCoreData.active = false
+                saveToCoreData.date = Date()
+                try? self.coredataContext.save()
+
+                let setHBT = TempTargetsSlider(context: self.coredataContext)
+                setHBT.enabled = false
+                setHBT.date = Date()
+                try? self.coredataContext.save()
+            }
+        }
+
+        func save() {
+            guard durationTT > 0 else {
+                return
+            }
+            var lowTarget = low
+
+            if viewPercantage {
+                lowTarget = Decimal(round(Double(computeTarget())))
+                didSaveSettings = true
+            }
+            var highTarget = lowTarget
+
+            if units == .mmolL, !viewPercantage {
+                lowTarget = Decimal(round(Double(lowTarget.asMgdL)))
+                highTarget = lowTarget
+            }
+
+            let entry = TempTarget(
+                name: newPresetName.isEmpty ? TempTarget.custom : newPresetName,
+                createdAt: Date(),
+                targetTop: highTarget,
+                targetBottom: lowTarget,
+                duration: durationTT,
+                enteredBy: TempTarget.manual,
+                reason: newPresetName.isEmpty ? TempTarget.custom : newPresetName
+            )
+            presetsTT.append(entry)
+            storage.storePresets(presetsTT)
+
+            if viewPercantage {
+                let id = entry.id
+
+                coredataContext.performAndWait {
+                    let saveToCoreData = TempTargetsSlider(context: self.coredataContext)
+                    saveToCoreData.id = id
+                    saveToCoreData.isPreset = true
+                    saveToCoreData.enabled = true
+                    saveToCoreData.hbt = hbt
+                    saveToCoreData.date = Date()
+                    saveToCoreData.duration = durationTT as NSDecimalNumber
+                    try? self.coredataContext.save()
+                }
+            }
+        }
+
+        func enactPreset(id: String) {
+            if var preset = presetsTT.first(where: { $0.id == id }) {
+                preset.createdAt = Date()
+                storage.storeTempTargets([preset])
+                showModal(for: nil)
+
+                coredataContext.performAndWait {
+                    var tempTargetsArray = [TempTargetsSlider]()
+                    let requestTempTargets = TempTargetsSlider.fetchRequest() as NSFetchRequest<TempTargetsSlider>
+                    let sortTT = NSSortDescriptor(key: "date", ascending: false)
+                    requestTempTargets.sortDescriptors = [sortTT]
+                    try? tempTargetsArray = coredataContext.fetch(requestTempTargets)
+
+                    let whichID = tempTargetsArray.first(where: { $0.id == id })
+
+                    if whichID != nil {
+                        let saveToCoreData = TempTargets(context: self.coredataContext)
+                        saveToCoreData.active = true
+                        saveToCoreData.date = Date()
+                        saveToCoreData.hbt = whichID?.hbt ?? 160
+                        // saveToCoreData.id = id
+                        saveToCoreData.startDate = Date()
+                        saveToCoreData.duration = whichID?.duration ?? 0
+
+                        try? self.coredataContext.save()
+                    } else {
+                        let saveToCoreData = TempTargets(context: self.coredataContext)
+                        saveToCoreData.active = false
+                        saveToCoreData.date = Date()
+                        try? self.coredataContext.save()
+                    }
+                }
+            }
+        }
+
+        func removePreset(id: String) {
+            presetsTT = presetsTT.filter { $0.id != id }
+            storage.storePresets(presetsTT)
+        }
+
+        func computeTarget() -> Decimal {
+            var ratio = Decimal(percentageTT / 100)
+            let c = Decimal(hbt - 100)
+            var target = (c / ratio) - c + 100
+
+            if c * (c + target - 100) <= 0 {
+                ratio = maxValue
+                target = (c / ratio) - c + 100
+            }
+            return Decimal(Double(target))
+        }
     }
 }

+ 407 - 171
FreeAPS/Sources/Modules/OverrideProfilesConfig/View/OverrideProfilesRootView.swift

@@ -7,11 +7,17 @@ extension OverrideProfilesConfig {
         let resolver: Resolver
 
         @StateObject var state = StateModel()
+
         @State private var isEditing = false
         @State private var showAlert = false
         @State private var showingDetail = false
         @State private var alertSring = ""
         @State var isSheetPresented: Bool = false
+        // temp targets
+        @State private var isPromptPresented = false
+        @State private var isRemoveAlertPresented = false
+        @State private var removeAlert: Alert?
+        @State private var isEditingTT = false
 
         @Environment(\.dismiss) var dismiss
         @Environment(\.managedObjectContext) var moc
@@ -40,6 +46,11 @@ extension OverrideProfilesConfig {
             )
         ) var fetchedProfiles: FetchedResults<OverridePresets>
 
+        @FetchRequest(
+            entity: TempTargetsSlider.entity(),
+            sortDescriptors: [NSSortDescriptor(key: "date", ascending: false)]
+        ) var isEnabledArray: FetchedResults<TempTargetsSlider>
+
         private var formatter: NumberFormatter {
             let formatter = NumberFormatter()
             formatter.numberStyle = .decimal
@@ -79,215 +90,437 @@ extension OverrideProfilesConfig {
         }
 
         var body: some View {
-            Form {
-                if state.presets.isNotEmpty {
-                    Section {
-                        ForEach(fetchedProfiles) { preset in
-                            profilesView(for: preset)
-                        }.onDelete(perform: removeProfile)
-                    }.listRowBackground(Color.chart)
+            VStack {
+                Picker("Tab", selection: $state.selectedTab) {
+                    ForEach(Tab.allCases) { tab in
+                        Text(NSLocalizedString(tab.name, comment: "")).tag(tab)
+                    }
                 }
+                .pickerStyle(.segmented).padding(.horizontal, 10)
+
+                Form {
+                    switch state.selectedTab {
+                    case .profiles: profiles()
+                    case .tempTargets: tempTargets() }
+                }.scrollContentBackground(.hidden).background(color)
+                    .onAppear(perform: configureView)
+                    .onAppear { state.savedSettings() }
+                    .navigationBarTitle("Profiles")
+                    .navigationBarTitleDisplayMode(.large)
+            }.background(color)
+        }
+
+        @ViewBuilder func profiles() -> some View {
+            if state.presetsProfiles.isNotEmpty {
                 Section {
-                    VStack {
-                        Spacer()
-                        Text("\(state.percentage.formatted(.number)) %")
-                            .foregroundColor(
-                                state
-                                    .percentage >= 130 ? .red :
-                                    (isEditing ? .orange : Color.blue)
-                            )
-                            .font(.largeTitle)
-                        Slider(
-                            value: $state.percentage,
-                            in: 10 ... 200,
-                            step: 1,
-                            onEditingChanged: { editing in
-                                isEditing = editing
-                            }
+                    ForEach(fetchedProfiles) { preset in
+                        profilesView(for: preset)
+                    }.onDelete(perform: removeProfile)
+                }.listRowBackground(Color.chart)
+            }
+            Section {
+                VStack {
+                    Spacer()
+                    Text("\(state.percentageProfiles.formatted(.number)) %")
+                        .foregroundColor(
+                            state
+                                .percentageProfiles >= 130 ? .red :
+                                (isEditing ? .orange : Color.blue)
                         )
-                        Spacer()
-                        Toggle(isOn: $state._indefinite) {
-                            Text("Enable indefinitely")
+                        .font(.largeTitle)
+                    Slider(
+                        value: $state.percentageProfiles,
+                        in: 10 ... 200,
+                        step: 1,
+                        onEditingChanged: { editing in
+                            isEditing = editing
                         }
+                    )
+                    Spacer()
+                    Toggle(isOn: $state._indefinite) {
+                        Text("Enable indefinitely")
                     }
-                    if !state._indefinite {
-                        HStack {
-                            Text("Duration")
-                            DecimalTextField("0", value: $state.duration, formatter: formatter, cleanInput: false)
-                            Text("minutes").foregroundColor(.secondary)
-                        }
+                }
+                if !state._indefinite {
+                    HStack {
+                        Text("Duration")
+                        DecimalTextField("0", value: $state.durationProfile, formatter: formatter, cleanInput: false)
+                        Text("minutes").foregroundColor(.secondary)
                     }
+                }
 
+                HStack {
+                    Toggle(isOn: $state.override_target) {
+                        Text("Override Profile Target")
+                    }
+                }
+                if state.override_target {
                     HStack {
-                        Toggle(isOn: $state.override_target) {
-                            Text("Override Profile Target")
-                        }
+                        Text("Target Glucose")
+                        DecimalTextField("0", value: $state.target, formatter: glucoseFormatter, cleanInput: false)
+                        Text(state.units.rawValue).foregroundColor(.secondary)
                     }
-                    if state.override_target {
-                        HStack {
-                            Text("Target Glucose")
-                            DecimalTextField("0", value: $state.target, formatter: glucoseFormatter, cleanInput: false)
-                            Text(state.units.rawValue).foregroundColor(.secondary)
-                        }
+                }
+                HStack {
+                    Toggle(isOn: $state.advancedSettings) {
+                        Text("More options")
                     }
+                }
+                if state.advancedSettings {
                     HStack {
-                        Toggle(isOn: $state.advancedSettings) {
-                            Text("More options")
+                        Toggle(isOn: $state.smbIsOff) {
+                            Text("Disable SMBs")
                         }
                     }
-                    if state.advancedSettings {
+                    HStack {
+                        Toggle(isOn: $state.smbIsAlwaysOff) {
+                            Text("Schedule when SMBs are Off")
+                        }.disabled(!state.smbIsOff)
+                    }
+                    if state.smbIsAlwaysOff {
                         HStack {
-                            Toggle(isOn: $state.smbIsOff) {
-                                Text("Disable SMBs")
-                            }
+                            Text("First Hour SMBs are Off (24 hours)")
+                            DecimalTextField("0", value: $state.start, formatter: formatter, cleanInput: false)
+                            Text("hour").foregroundColor(.secondary)
                         }
                         HStack {
-                            Toggle(isOn: $state.smbIsAlwaysOff) {
-                                Text("Schedule when SMBs are Off")
-                            }.disabled(!state.smbIsOff)
+                            Text("Last Hour SMBs are Off (24 hours)")
+                            DecimalTextField("0", value: $state.end, formatter: formatter, cleanInput: false)
+                            Text("hour").foregroundColor(.secondary)
                         }
-                        if state.smbIsAlwaysOff {
-                            HStack {
-                                Text("First Hour SMBs are Off (24 hours)")
-                                DecimalTextField("0", value: $state.start, formatter: formatter, cleanInput: false)
-                                Text("hour").foregroundColor(.secondary)
-                            }
-                            HStack {
-                                Text("Last Hour SMBs are Off (24 hours)")
-                                DecimalTextField("0", value: $state.end, formatter: formatter, cleanInput: false)
-                                Text("hour").foregroundColor(.secondary)
-                            }
+                    }
+                    HStack {
+                        Toggle(isOn: $state.isfAndCr) {
+                            Text("Change ISF and CR")
                         }
+                    }
+                    if !state.isfAndCr {
                         HStack {
-                            Toggle(isOn: $state.isfAndCr) {
-                                Text("Change ISF and CR")
+                            Toggle(isOn: $state.isf) {
+                                Text("Change ISF")
                             }
                         }
-                        if !state.isfAndCr {
-                            HStack {
-                                Toggle(isOn: $state.isf) {
-                                    Text("Change ISF")
-                                }
-                            }
-                            HStack {
-                                Toggle(isOn: $state.cr) {
-                                    Text("Change CR")
-                                }
+                        HStack {
+                            Toggle(isOn: $state.cr) {
+                                Text("Change CR")
                             }
                         }
-                        HStack {
-                            Text("SMB Minutes")
-                            DecimalTextField(
-                                "0",
-                                value: $state.smbMinutes,
-                                formatter: formatter,
-                                cleanInput: false
+                    }
+                    HStack {
+                        Text("SMB Minutes")
+                        DecimalTextField(
+                            "0",
+                            value: $state.smbMinutes,
+                            formatter: formatter,
+                            cleanInput: false
+                        )
+                        Text("minutes").foregroundColor(.secondary)
+                    }
+                    HStack {
+                        Text("UAM SMB Minutes")
+                        DecimalTextField(
+                            "0",
+                            value: $state.uamMinutes,
+                            formatter: formatter,
+                            cleanInput: false
+                        )
+                        Text("minutes").foregroundColor(.secondary)
+                    }
+                }
+
+                // MARK: TESTING
+
+                HStack {
+                    Button("Start new Profile") {
+                        showAlert.toggle()
+
+                        alertSring = "\(state.percentageProfiles.formatted(.number)) %, " +
+                            (
+                                state.durationProfile > 0 || !state
+                                    ._indefinite ?
+                                    (
+                                        state
+                                            .durationProfile
+                                            .formatted(.number.grouping(.never).rounded().precision(.fractionLength(0))) +
+                                            " min."
+                                    ) :
+                                    NSLocalizedString(" infinite duration.", comment: "")
+                            ) +
+                            (
+                                (state.target == 0 || !state.override_target) ? "" :
+                                    (" Target: " + state.target.formatted() + " " + state.units.rawValue + ".")
                             )
-                            Text("minutes").foregroundColor(.secondary)
-                        }
-                        HStack {
-                            Text("UAM SMB Minutes")
-                            DecimalTextField(
-                                "0",
-                                value: $state.uamMinutes,
-                                formatter: formatter,
-                                cleanInput: false
+                            +
+                            (
+                                state
+                                    .smbIsOff ?
+                                    NSLocalizedString(
+                                        " SMBs are disabled either by schedule or during the entire duration.",
+                                        comment: ""
+                                    ) : ""
+                            )
+                            +
+                            "\n\n"
+                            +
+                            NSLocalizedString(
+                                "Starting this override will change your Profiles and/or your Target Glucose used for looping during the entire selected duration. Tapping ”Start Profile” will start your new profile or edit your current active profile.",
+                                comment: ""
                             )
-                            Text("minutes").foregroundColor(.secondary)
-                        }
                     }
-
-                    HStack {
-                        Button("Start new Profile") {
-                            showAlert.toggle()
-                            alertSring = "\(state.percentage.formatted(.number)) %, " +
-                                (
-                                    state.duration > 0 || !state
-                                        ._indefinite ?
-                                        (
-                                            state
-                                                .duration
-                                                .formatted(.number.grouping(.never).rounded().precision(.fractionLength(0))) +
-                                                " min."
-                                        ) :
-                                        NSLocalizedString(" infinite duration.", comment: "")
-                                ) +
-                                (
-                                    (state.target == 0 || !state.override_target) ? "" :
-                                        (" Target: " + state.target.formatted() + " " + state.units.rawValue + ".")
-                                )
-                                +
-                                (
-                                    state
-                                        .smbIsOff ?
-                                        NSLocalizedString(
-                                            " SMBs are disabled either by schedule or during the entire duration.",
-                                            comment: ""
-                                        ) : ""
-                                )
-                                +
-                                "\n\n"
-                                +
-                                NSLocalizedString(
-                                    "Starting this override will change your Profiles and/or your Target Glucose used for looping during the entire selected duration. Tapping ”Start Profile” will start your new profile or edit your current active profile.",
-                                    comment: ""
-                                )
+                    .disabled(unChanged())
+                    .buttonStyle(BorderlessButtonStyle())
+                    .font(.callout)
+                    .controlSize(.mini)
+                    .alert(
+                        "Start Profile",
+                        isPresented: $showAlert,
+                        actions: {
+                            Button("Cancel", role: .cancel) { state.isEnabled = false }
+                            Button("Start Profile", role: .destructive) {
+                                if state._indefinite { state.durationProfile = 0 }
+                                state.isEnabled.toggle()
+                                state.saveSettings()
+                                dismiss()
+                            }
+                        },
+                        message: {
+                            Text(alertSring)
                         }
-                        .disabled(unChanged())
+                    )
+                    Button {
+                        isSheetPresented = true
+                    }
+                    label: { Text("Save as Profile") }
+                        .tint(.orange)
+                        .frame(maxWidth: .infinity, alignment: .trailing)
                         .buttonStyle(BorderlessButtonStyle())
-                        .font(.callout)
                         .controlSize(.mini)
-                        .alert(
-                            "Start Profile",
-                            isPresented: $showAlert,
-                            actions: {
-                                Button("Cancel", role: .cancel) { state.isEnabled = false }
-                                Button("Start Profile", role: .destructive) {
-                                    if state._indefinite { state.duration = 0 }
-                                    state.isEnabled.toggle()
-                                    state.saveSettings()
-                                    dismiss()
-                                }
-                            },
-                            message: {
-                                Text(alertSring)
+                        .disabled(unChanged())
+                }
+                .sheet(isPresented: $isSheetPresented) {
+                    presetPopover
+                }
+
+                // MARK: TESTING END
+            }
+
+            header: { Text("Insulin") }
+            footer: {
+                Text(
+                    "Your profile basal insulin will be adjusted with the override percentage and your profile ISF and CR will be inversly adjusted with the percentage."
+                )
+            }.listRowBackground(Color.chart)
+
+            Button(action: {
+                state.cancelProfile()
+                dismiss()
+            }, label: {
+                HStack {
+                    Spacer()
+                    Text("Cancel Profile")
+                    Spacer()
+                    Image(systemName: "xmark.app")
+                        .font(.title)
+                }
+            })
+                .frame(maxWidth: .infinity, alignment: .center)
+                .disabled(!state.isEnabled)
+                .listRowBackground(!state.isEnabled ? Color(.systemGray4) : Color(.systemRed))
+                .tint(.white)
+        }
+
+        @ViewBuilder func tempTargets() -> some View {
+            if !state.presetsTT.isEmpty {
+                Section(header: Text("Presets")) {
+                    ForEach(state.presetsTT) { preset in
+                        presetView(for: preset)
+                    }
+                }.listRowBackground(Color.chart)
+            }
+
+            HStack {
+                Text("Experimental")
+                Toggle(isOn: $state.viewPercantage) {}.controlSize(.mini)
+                Image(systemName: "figure.highintensity.intervaltraining")
+                Image(systemName: "fork.knife")
+            }.listRowBackground(Color.chart)
+
+            if state.viewPercantage {
+                Section {
+                    VStack {
+                        Text("\(state.percentageTT.formatted(.number)) % Insulin")
+                            .foregroundColor(isEditingTT ? .orange : .blue)
+                            .font(.largeTitle)
+                            .padding(.vertical)
+                        Slider(
+                            value: $state.percentageTT,
+                            in: 15 ...
+                                min(Double(state.maxValue * 100), 200),
+                            step: 1,
+                            onEditingChanged: { editing in
+                                isEditingTT = editing
                             }
                         )
-                        Button {
-                            isSheetPresented = true
+                        // Only display target slider when not 100 %
+                        if state.percentageTT != 100 {
+                            Spacer()
+                            Divider()
+                            Text(
+                                (
+                                    state
+                                        .units == .mmolL ?
+                                        "\(state.computeTarget().asMmolL.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1)))) mmol/L" :
+                                        "\(state.computeTarget().formatted(.number.grouping(.never).rounded().precision(.fractionLength(0)))) mg/dl"
+                                )
+                                    + NSLocalizedString(" Target Glucose", comment: "")
+                            )
+                            .foregroundColor(.green)
+                            .padding(.vertical)
+
+                            Slider(
+                                value: $state.hbt,
+                                in: 101 ... 295,
+                                step: 1
+                            ).accentColor(.green)
                         }
-                        label: { Text("Save as Profile") }
+                    }
+                }.listRowBackground(Color.chart)
+            } else {
+                Section(header: Text("Custom")) {
+                    HStack {
+                        Text("Target")
+                        Spacer()
+                        DecimalTextField("0", value: $state.low, formatter: formatter, cleanInput: true)
+                        Text(state.units.rawValue).foregroundColor(.secondary)
+                    }
+                    HStack {
+                        Text("Duration")
+                        Spacer()
+                        DecimalTextField("0", value: $state.durationTT, formatter: formatter, cleanInput: true)
+                        Text("minutes").foregroundColor(.secondary)
+                    }
+                    DatePicker("Date", selection: $state.date)
+                    HStack {
+                        Button { state.enact() }
+                        label: { Text("Enact") }
+                            .disabled(state.durationTT == 0)
+                            .buttonStyle(BorderlessButtonStyle())
+                            .font(.callout)
+                            .controlSize(.mini)
+
+                        Button { isPromptPresented = true }
+                        label: { Text("Save as preset") }
+                            .disabled(state.durationTT == 0)
+                            .tint(.orange)
+                            .frame(maxWidth: .infinity, alignment: .trailing)
+                            .buttonStyle(BorderlessButtonStyle())
+                            .controlSize(.mini)
+                    }
+                }.listRowBackground(Color.chart)
+            }
+            if state.viewPercantage {
+                Section {
+                    HStack {
+                        Text("Duration")
+                        Spacer()
+                        DecimalTextField("0", value: $state.durationTT, formatter: formatter, cleanInput: true)
+                        Text("minutes").foregroundColor(.secondary)
+                    }
+                    DatePicker("Date", selection: $state.date)
+                    HStack {
+                        Button { state.enact() }
+                        label: { Text("Enact") }
+                            .disabled(state.durationTT == 0)
+                            .buttonStyle(BorderlessButtonStyle())
+                            .font(.callout)
+                            .controlSize(.mini)
+
+                        Button { isPromptPresented = true }
+                        label: { Text("Save as preset") }
+                            .disabled(state.durationTT == 0)
                             .tint(.orange)
                             .frame(maxWidth: .infinity, alignment: .trailing)
                             .buttonStyle(BorderlessButtonStyle())
                             .controlSize(.mini)
-                            .disabled(unChanged())
                     }
-                    .sheet(isPresented: $isSheetPresented) {
-                        presetPopover
+                }.listRowBackground(Color.chart)
+            }
+
+            Section {
+                Button { state.cancel() }
+                label: {
+                    HStack {
+                        Spacer()
+                        Text("Cancel Temp Target")
+                        Spacer()
+                        Image(systemName: "xmark.app")
+                            .font(.title)
                     }
                 }
+                .frame(maxWidth: .infinity, alignment: .center)
+                .disabled(state.storage.current() == nil)
+                .listRowBackground(state.storage.current() == nil ? Color(.systemGray4) : Color(.systemRed))
+                .tint(.white)
+            }
+        }
 
-                header: { Text("Insulin") }
-                footer: {
-                    Text(
-                        "Your profile basal insulin will be adjusted with the override percentage and your profile ISF and CR will be inversly adjusted with the percentage."
-                    )
-                }.listRowBackground(Color.chart)
+        private func presetView(for preset: TempTarget) -> some View {
+            var low = preset.targetBottom
+            var high = preset.targetTop
+            if state.units == .mmolL {
+                low = low?.asMmolL
+                high = high?.asMmolL
+            }
+            return HStack {
+                VStack {
+                    HStack {
+                        Text(preset.displayName)
+                        Spacer()
+                    }
+                    HStack(spacing: 2) {
+                        Text(
+                            "\(formatter.string(from: (low ?? 0) as NSNumber)!) - \(formatter.string(from: (high ?? 0) as NSNumber)!)"
+                        )
+                        .foregroundColor(.secondary)
+                        .font(.caption)
 
-                Button("Return to Normal") {
-                    state.cancelProfile()
-                    dismiss()
-                }.listRowBackground(Color.chart)
-                    .frame(maxWidth: .infinity, alignment: .center)
-                    .buttonStyle(BorderlessButtonStyle())
-                    .disabled(!state.isEnabled)
-                    .tint(.red)
-            }.scrollContentBackground(.hidden).background(color)
-                .onAppear(perform: configureView)
-                .onAppear { state.savedSettings() }
-                .navigationBarTitle("Profiles")
-                .navigationBarTitleDisplayMode(.large)
+                        Text(state.units.rawValue)
+                            .foregroundColor(.secondary)
+                            .font(.caption)
+                        Text("for")
+                            .foregroundColor(.secondary)
+                            .font(.caption)
+                        Text("\(formatter.string(from: preset.duration as NSNumber)!)")
+                            .foregroundColor(.secondary)
+                            .font(.caption)
+                        Text("min")
+                            .foregroundColor(.secondary)
+                            .font(.caption)
+
+                        Spacer()
+                    }.padding(.top, 2)
+                }
+                .contentShape(Rectangle())
+                .onTapGesture {
+                    state.enactPreset(id: preset.id)
+                }
+
+                Image(systemName: "xmark.circle").foregroundColor(.secondary)
+                    .contentShape(Rectangle())
+                    .padding(.vertical)
+                    .onTapGesture {
+                        removeAlert = Alert(
+                            title: Text("Are you sure?"),
+                            message: Text("Delete preset \"\(preset.displayName)\""),
+                            primaryButton: .destructive(Text("Delete"), action: { state.removePreset(id: preset.id) }),
+                            secondaryButton: .cancel()
+                        )
+                        isRemoveAlertPresented = true
+                    }
+                    .alert(isPresented: $isRemoveAlertPresented) {
+                        removeAlert!
+                    }
+            }
         }
 
         @ViewBuilder private func profilesView(for preset: OverridePresets) -> some View {
@@ -345,10 +578,13 @@ extension OverrideProfilesConfig {
         }
 
         private func unChanged() -> Bool {
-            let isChanged = (state.percentage == 100 && !state.override_target && !state.smbIsOff && !state.advancedSettings) ||
-                (!state._indefinite && state.duration == 0) || (state.override_target && state.target == 0) ||
+            let isChanged = (
+                state.percentageProfiles == 100 && !state.override_target && !state.smbIsOff && !state
+                    .advancedSettings
+            ) ||
+                (!state._indefinite && state.durationProfile == 0) || (state.override_target && state.target == 0) ||
                 (
-                    state.percentage == 100 && !state.override_target && !state.smbIsOff && state.isf && state.cr && state
+                    state.percentageProfiles == 100 && !state.override_target && !state.smbIsOff && state.isf && state.cr && state
                         .smbMinutes == state.defaultSmbMinutes && state.uamMinutes == state.defaultUamMinutes
                 )
 

+ 38 - 14
FreeAPS/Sources/Modules/Settings/View/SettingsRootView.swift

@@ -30,7 +30,15 @@ extension Settings {
         var body: some View {
             Form {
                 Section {
-                    Toggle("Closed loop", isOn: $state.closedLoop)
+                    HStack(spacing: 15) {
+                        Image(systemName: "circle")
+                            .imageScale(.small)
+                            .font(.system(size: 32))
+                            .foregroundColor(Color.green)
+                        Toggle("Closed loop", isOn: $state.closedLoop)
+                            .font(.subheadline)
+                            .foregroundStyle(.primary)
+                    }
                 } header: {
                     Text(
                         "iAPS v\(state.versionNumber) (\(state.buildNumber))\nBranch: \(state.branch) \(state.copyrightNotice) "
@@ -38,25 +46,41 @@ extension Settings {
                 }.listRowBackground(Color.chart)
 
                 Section {
-                    Text("Pump").navigationLink(to: .pumpConfig, from: self)
-                    Text("CGM").navigationLink(to: .cgm, from: self)
-                    Text("Watch").navigationLink(to: .watch, from: self)
-                } header: { Text("Devices") }.listRowBackground(Color.chart)
+                    SettingsRowView(imageName: "chart.xyaxis.line", title: "Statistics", tint: Color.green, spacing: 10)
+                        .navigationLink(to: .statistics, from: self)
+                } header: { Text("Statistics") }.listRowBackground(Color.chart)
+
+                Section {
+                    SettingsRowViewCustomImage(imageName: "pod", title: "Pump")
+                        .navigationLink(to: .pumpConfig, from: self)
+                    SettingsRowViewCustomImage(imageName: "g6", title: "CGM")
+                        .navigationLink(to: .cgm, from: self)
+                    SettingsRowView(imageName: "applewatch.watchface", title: "Watch", tint: Color.primary, spacing: 18)
+                        .navigationLink(to: .watch, from: self)
+                } header: { Text("Select Devices") }.listRowBackground(Color.chart)
 
                 Section {
-                    Text("Nightscout").navigationLink(to: .nighscoutConfig, from: self)
+                    SettingsRowViewCustomImage(imageName: "owl", title: "Nightscout", frame: 32)
+                        .navigationLink(to: .nighscoutConfig, from: self)
                     if HKHealthStore.isHealthDataAvailable() {
-                        Text("Apple Health").navigationLink(to: .healthkit, from: self)
+                        SettingsRowView(imageName: "heart.circle.fill", title: "Apple Health", tint: Color.red)
+                            .navigationLink(to: .healthkit, from: self)
                     }
-                    Text("Notifications").navigationLink(to: .notificationsConfig, from: self)
+                    SettingsRowView(imageName: "message.circle.fill", title: "Notifications", tint: Color.blue)
+                        .navigationLink(to: .notificationsConfig, from: self)
                 } header: { Text("Services") }.listRowBackground(Color.chart)
 
                 Section {
-                    Text("Pump Settings").navigationLink(to: .pumpSettingsEditor, from: self)
-                    Text("Basal Profile").navigationLink(to: .basalProfileEditor, from: self)
-                    Text("Insulin Sensitivities").navigationLink(to: .isfEditor, from: self)
-                    Text("Carb Ratios").navigationLink(to: .crEditor, from: self)
-                    Text("Target Glucose").navigationLink(to: .targetsEditor, from: self)
+                    SettingsRowViewCustomImage(imageName: "pod", title: "Pump Settings")
+                        .navigationLink(to: .pumpSettingsEditor, from: self)
+                    SettingsRowView(imageName: "chart.bar.xaxis", title: "Basal Profile", tint: Color.insulin, spacing: 10)
+                        .navigationLink(to: .basalProfileEditor, from: self)
+                    SettingsRowView(imageName: "drop.fill", title: "Insulin Sensitivities", tint: Color.insulin, spacing: 22)
+                        .navigationLink(to: .isfEditor, from: self)
+                    SettingsRowView(imageName: "fork.knife.circle", title: "Carb Ratios", tint: Color.orange, spacing: 14)
+                        .navigationLink(to: .crEditor, from: self)
+                    SettingsRowView(imageName: "target", title: "Target Glucose", tint: Color.green, spacing: 14)
+                        .navigationLink(to: .targetsEditor, from: self)
                 } header: { Text("Configuration") }.listRowBackground(Color.chart)
 
                 Section {
@@ -151,7 +175,7 @@ extension Settings {
                     ShareSheet(activityItems: state.logItems())
                 }
                 .onAppear(perform: configureView)
-                .navigationTitle("Settings")
+                .navigationTitle("Menü")
                 .navigationBarTitleDisplayMode(.large)
                 .onDisappear(perform: { state.uploadProfileAndSettings(false) })
         }

+ 1 - 8
FreeAPS/Sources/Modules/Stat/View/StatRootView.swift

@@ -167,14 +167,7 @@ extension Stat {
             }.background(color)
                 .onAppear(perform: configureView)
                 .navigationBarTitle("Statistics")
-                .navigationBarTitleDisplayMode(.inline)
-                .toolbar {
-                    ToolbarItem(placement: .topBarLeading) {
-                        Button("Close") {
-                            state.hideModal()
-                        }
-                    }
-                }
+                .navigationBarTitleDisplayMode(.large)
         }
     }
 }