AdjustmentsRootView.swift 13 KB

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