AdjustmentsRootView.swift 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277
  1. import CoreData
  2. import SwiftUI
  3. import Swinject
  4. extension Adjustments {
  5. struct RootView: BaseView {
  6. let resolver: Resolver
  7. @State var state = StateModel()
  8. @State var isEditing = false
  9. @State var showOverrideCreationSheet = false
  10. @State var showTempTargetCreationSheet = false
  11. @State var showingDetail = false
  12. @State var showOverrideCheckmark: Bool = false
  13. @State var showTempTargetCheckmark: Bool = false
  14. @State var selectedOverridePresetID: String?
  15. @State var selectedTempTargetPresetID: String?
  16. @State var selectedOverride: OverrideStored?
  17. @State var selectedTempTarget: TempTargetStored?
  18. @State var isConfirmDeletePresented = false
  19. @State var isPromptPresented = false
  20. @State var isRemoveAlertPresented = false
  21. @State var removeAlert: Alert?
  22. @State var isEditingTT = false
  23. private var shouldDisplayStickyOverrideStopButton: Bool {
  24. state.isOverrideEnabled && state.activeOverrideName.isNotEmpty
  25. }
  26. private var shouldDisplayStickyTempTargetStopButton: Bool {
  27. state.isTempTargetEnabled && state.activeTempTargetName.isNotEmpty
  28. }
  29. @Environment(\.colorScheme) var colorScheme
  30. @Environment(AppState.self) var appState
  31. func formattedGlucose(glucose: Decimal) -> String {
  32. let formattedValue: String
  33. if state.units == .mgdL {
  34. formattedValue = Formatter.glucoseFormatter(for: state.units)
  35. .string(from: glucose as NSDecimalNumber) ?? "\(glucose)"
  36. } else {
  37. formattedValue = glucose.formattedAsMmolL
  38. }
  39. return "\(formattedValue) \(state.units.rawValue)"
  40. }
  41. var body: some View {
  42. ZStack(alignment: .center, content: {
  43. VStack {
  44. Picker("Adjustment Tabs", selection: $state.selectedTab) {
  45. ForEach(Adjustments.Tab.allCases.indexed(), id: \.1) { index, item in
  46. Text(item.name).tag(index)
  47. }
  48. }
  49. .pickerStyle(SegmentedPickerStyle())
  50. .padding(.horizontal)
  51. List {
  52. switch state.selectedTab {
  53. case .overrides: overrides()
  54. case .tempTargets: tempTargets() }
  55. }
  56. .scrollContentBackground(.hidden)
  57. .background(appState.trioBackgroundColor(for: colorScheme))
  58. }
  59. .listSectionSpacing(10)
  60. .safeAreaInset(
  61. edge: .bottom,
  62. spacing: shouldDisplayStickyOverrideStopButton || shouldDisplayStickyTempTargetStopButton ? 30 : 0
  63. ) {
  64. if shouldDisplayStickyOverrideStopButton, state.selectedTab == .overrides {
  65. stickyStopOverrideButton
  66. } else if shouldDisplayStickyTempTargetStopButton, state.selectedTab == .tempTargets {
  67. stickyStopTempTargetButton
  68. } else {
  69. EmptyView()
  70. }
  71. }
  72. .scrollContentBackground(.hidden)
  73. .background(appState.trioBackgroundColor(for: colorScheme))
  74. .onAppear(perform: configureView)
  75. .navigationBarTitle("Adjustments")
  76. .navigationBarTitleDisplayMode(.large)
  77. .toolbar {
  78. ToolbarItem(placement: .topBarTrailing) {
  79. switch state.selectedTab {
  80. case .overrides:
  81. Button(action: {
  82. showOverrideCreationSheet = true
  83. }, label: {
  84. HStack {
  85. Text("Add Override")
  86. Image(systemName: "plus")
  87. }
  88. })
  89. case .tempTargets:
  90. Button(action: {
  91. showTempTargetCreationSheet = true
  92. }, label: {
  93. HStack {
  94. Text("Add Temp Target")
  95. Image(systemName: "plus")
  96. }
  97. })
  98. }
  99. }
  100. }
  101. .sheet(isPresented: $state.showOverrideEditSheet, onDismiss: {
  102. Task {
  103. await state.resetStateVariables()
  104. state.showOverrideEditSheet = false
  105. }
  106. }) {
  107. if let override = selectedOverride {
  108. EditOverrideForm(overrideToEdit: override, state: state)
  109. }
  110. }
  111. .sheet(isPresented: $showOverrideCreationSheet, onDismiss: {
  112. Task {
  113. await state.resetStateVariables()
  114. showOverrideCreationSheet = false
  115. }
  116. }) {
  117. AddOverrideForm(state: state)
  118. }
  119. .sheet(isPresented: $showTempTargetCreationSheet, onDismiss: {
  120. Task {
  121. await state.resetTempTargetState()
  122. showTempTargetCreationSheet = false
  123. }
  124. }) {
  125. AddTempTargetForm(state: state)
  126. }
  127. .sheet(isPresented: $state.showTempTargetEditSheet, onDismiss: {
  128. Task {
  129. await state.resetTempTargetState()
  130. state.showTempTargetEditSheet = false
  131. }
  132. }) {
  133. if let tempTarget = selectedTempTarget {
  134. EditTempTargetForm(tempTargetToEdit: tempTarget, state: state)
  135. }
  136. }
  137. }).background(appState.trioBackgroundColor(for: colorScheme))
  138. }
  139. var defaultText: some View {
  140. switch state.selectedTab {
  141. case .overrides:
  142. Section {} header: {
  143. Text("Add Preset or Override by tapping 'Add Override +' in the top right-hand corner of the screen.")
  144. .textCase(nil)
  145. .foregroundStyle(.secondary)
  146. }
  147. case .tempTargets:
  148. Section {} header: {
  149. Text(
  150. "Add Preset or Temp Target by tapping 'Add Temp Target +' in the top right-hand corner of the screen."
  151. )
  152. .textCase(nil)
  153. .foregroundStyle(.secondary)
  154. }
  155. }
  156. }
  157. var currentActiveAdjustment: some View {
  158. switch state.selectedTab {
  159. case .overrides:
  160. Section {
  161. HStack {
  162. Text("\(state.activeOverrideName) is running")
  163. Spacer()
  164. Image(systemName: "square.and.pencil")
  165. .foregroundStyle(Color.primary)
  166. }
  167. .contentShape(Rectangle())
  168. .onTapGesture {
  169. Task {
  170. /// To avoid editing the Preset when a Preset-Override is running we first duplicate the Preset-Override as a non-Preset Override
  171. /// The currentActiveOverride variable in the State will update automatically via MOC notification
  172. await state.duplicateOverridePresetAndCancelPreviousOverride()
  173. /// selectedOverride is used for passing the chosen Override to the EditSheet so we have to set the updated currentActiveOverride to be the selectedOverride
  174. selectedOverride = state.currentActiveOverride
  175. /// Now we can show the Edit sheet
  176. state.showOverrideEditSheet = true
  177. }
  178. }
  179. }
  180. .listRowBackground(Color.purple.opacity(0.8))
  181. case .tempTargets:
  182. Section {
  183. HStack {
  184. Text("\(state.activeTempTargetName) is running")
  185. Spacer()
  186. Image(systemName: "square.and.pencil")
  187. .foregroundStyle(Color.primary)
  188. }
  189. .contentShape(Rectangle())
  190. .onTapGesture {
  191. Task {
  192. /// To avoid editing the Preset when a Preset-Override is running we first duplicate the Preset-Override as a non-Preset Override
  193. /// The currentActiveOverride variable in the State will update automatically via MOC notification
  194. await state.duplicateTempTargetPresetAndCancelPreviousTempTarget()
  195. /// selectedOverride is used for passing the chosen Override to the EditSheet so we have to set the updated currentActiveOverride to be the selectedOverride
  196. selectedTempTarget = state.currentActiveTempTarget
  197. /// Now we can show the Edit sheet
  198. state.showTempTargetEditSheet = true
  199. }
  200. }
  201. }
  202. .listRowBackground(Color.loopGreen.opacity(0.8))
  203. }
  204. }
  205. var cancelAdjustmentButton: some View {
  206. switch state.selectedTab {
  207. case .overrides:
  208. Button(action: {
  209. Task {
  210. // Save cancelled Override in OverrideRunStored Entity
  211. // Cancel ALL active Override
  212. await state.disableAllActiveOverrides(createOverrideRunEntry: true)
  213. }
  214. }, label: {
  215. Text("Stop Override")
  216. })
  217. .frame(maxWidth: .infinity, alignment: .center)
  218. .disabled(!state.isOverrideEnabled)
  219. .listRowBackground(!state.isOverrideEnabled ? Color(.systemGray4) : Color(.systemRed))
  220. .tint(.white)
  221. case .tempTargets:
  222. Button(action: {
  223. Task {
  224. // Save cancelled Temp Targets in TempTargetRunStored Entity
  225. // Cancel ALL active Temp Targets
  226. await state.disableAllActiveTempTargets(createTempTargetRunEntry: true)
  227. // Update View
  228. state.updateLatestTempTargetConfiguration()
  229. }
  230. }, label: {
  231. Text("Stop Temp Target")
  232. })
  233. .frame(maxWidth: .infinity, alignment: .center)
  234. .disabled(!state.isTempTargetEnabled)
  235. .listRowBackground(!state.isTempTargetEnabled ? Color(.systemGray4) : Color(.systemRed))
  236. .tint(.white)
  237. }
  238. }
  239. func formattedTimeRemaining(_ timeInterval: TimeInterval) -> String {
  240. let totalSeconds = Int(timeInterval)
  241. let hours = totalSeconds / 3600
  242. let minutes = (totalSeconds % 3600) / 60
  243. let seconds = totalSeconds % 60
  244. if hours > 0 {
  245. return "\(hours)h \(minutes)m \(seconds)s"
  246. } else if minutes > 0 {
  247. return "\(minutes)m \(seconds)s"
  248. } else {
  249. return "<1m"
  250. }
  251. }
  252. }
  253. }