AdjustmentsRootView.swift 14 KB

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