AdjustmentsRootView.swift 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404
  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. @State var pendingPresetActivation: PendingPresetActivation?
  26. private var shouldDisplayStickyOverrideStopButton: Bool {
  27. state.isOverrideEnabled && state.activeOverrideName.isNotEmpty
  28. }
  29. private var shouldDisplayStickyTempTargetStopButton: Bool {
  30. state.isTempTargetEnabled && state.activeTempTargetName.isNotEmpty
  31. }
  32. @Environment(\.colorScheme) var colorScheme
  33. @Environment(AppState.self) var appState
  34. func formattedGlucose(glucose: Decimal) -> String {
  35. let formattedValue: String
  36. if state.units == .mgdL {
  37. formattedValue = Formatter.glucoseFormatter(for: state.units)
  38. .string(from: glucose as NSDecimalNumber) ?? "\(glucose)"
  39. } else {
  40. formattedValue = glucose.formattedAsMmolL
  41. }
  42. return "\(formattedValue) \(state.units.rawValue)"
  43. }
  44. var body: some View {
  45. ZStack(alignment: .center, content: {
  46. VStack {
  47. Picker("Adjustment Tabs", selection: $state.selectedTab) {
  48. ForEach(Adjustments.Tab.allCases.indexed(), id: \.1) { index, item in
  49. Text(item.name).tag(index)
  50. }
  51. }
  52. .pickerStyle(SegmentedPickerStyle())
  53. .padding(.horizontal)
  54. List {
  55. switch state.selectedTab {
  56. case .overrides: overrides()
  57. case .tempTargets: tempTargets() }
  58. }
  59. .scrollContentBackground(.hidden)
  60. .background(appState.trioBackgroundColor(for: colorScheme))
  61. }
  62. .listSectionSpacing(10)
  63. .safeAreaInset(
  64. edge: .bottom,
  65. spacing: shouldDisplayStickyOverrideStopButton || shouldDisplayStickyTempTargetStopButton ? 30 : 0
  66. ) {
  67. if shouldDisplayStickyOverrideStopButton, state.selectedTab == .overrides {
  68. stickyStopOverrideButton
  69. } else if shouldDisplayStickyTempTargetStopButton, state.selectedTab == .tempTargets {
  70. stickyStopTempTargetButton
  71. } else {
  72. EmptyView()
  73. }
  74. }
  75. .scrollContentBackground(.hidden)
  76. .background(appState.trioBackgroundColor(for: colorScheme))
  77. .onAppear(perform: configureView)
  78. .navigationBarTitle("Adjustments")
  79. .navigationBarTitleDisplayMode(.large)
  80. .toolbar {
  81. ToolbarItem(placement: .topBarTrailing) {
  82. switch state.selectedTab {
  83. case .overrides:
  84. Button(action: {
  85. showOverrideCreationSheet = true
  86. }, label: {
  87. HStack {
  88. Text("Add Override")
  89. Image(systemName: "plus")
  90. }
  91. })
  92. case .tempTargets:
  93. Button(action: {
  94. showTempTargetCreationSheet = true
  95. }, label: {
  96. HStack {
  97. Text("Add Temp Target")
  98. Image(systemName: "plus")
  99. }
  100. })
  101. }
  102. }
  103. }
  104. .sheet(isPresented: $state.showOverrideEditSheet, onDismiss: {
  105. Task {
  106. await state.resetStateVariables()
  107. state.showOverrideEditSheet = false
  108. }
  109. }) {
  110. if let override = selectedOverride {
  111. EditOverrideForm(overrideToEdit: override, state: state)
  112. }
  113. }
  114. .sheet(isPresented: $showOverrideCreationSheet, onDismiss: {
  115. Task {
  116. await state.resetStateVariables()
  117. showOverrideCreationSheet = false
  118. }
  119. }) {
  120. AddOverrideForm(state: state)
  121. }
  122. .sheet(isPresented: $showTempTargetCreationSheet, onDismiss: {
  123. Task {
  124. await state.resetTempTargetState()
  125. showTempTargetCreationSheet = false
  126. }
  127. }) {
  128. AddTempTargetForm(state: state)
  129. }
  130. .sheet(isPresented: $state.showTempTargetEditSheet, onDismiss: {
  131. Task {
  132. await state.resetTempTargetState()
  133. state.showTempTargetEditSheet = false
  134. }
  135. }) {
  136. if let tempTarget = selectedTempTarget {
  137. EditTempTargetForm(tempTargetToEdit: tempTarget, state: state)
  138. }
  139. }
  140. .confirmationDialog("Override to Stop", isPresented: $showCancelOverrideConfirmDialog) {
  141. Button("Stop", role: .destructive) {
  142. Task {
  143. // Save cancelled Override in OverrideRunStored Entity
  144. // Cancel ALL active Override
  145. await state.disableAllActiveOverrides(createOverrideRunEntry: true)
  146. }
  147. }
  148. Button("Cancel", role: .cancel) {}
  149. } message: {
  150. Text("Stop the Override \"\(state.currentActiveOverride?.name ?? "")\"?")
  151. }
  152. .confirmationDialog("Temp Target to Stop", isPresented: $showCancelTempTargetConfirmDialog) {
  153. Button("Stop", role: .destructive) {
  154. Task {
  155. // Save cancelled Temp Targets in TempTargetRunStored Entity
  156. // Cancel ALL active Temp Targets
  157. await state.disableAllActiveTempTargets(createTempTargetRunEntry: true)
  158. // Update View
  159. state.updateLatestTempTargetConfiguration()
  160. }
  161. }
  162. Button("Cancel", role: .cancel) {}
  163. } message: {
  164. Text("Stop the Temp Target \"\(state.currentActiveTempTarget?.name ?? "")\"?")
  165. }
  166. .confirmationDialog(
  167. "Activate Preset",
  168. isPresented: presetActivationConfirmationBinding
  169. ) {
  170. Button("Activate") {
  171. if let activation = pendingPresetActivation {
  172. activatePreset(activation)
  173. }
  174. }
  175. Button("Cancel", role: .cancel) {
  176. state.shouldDisplayPresetStartConfirmDialog = false
  177. pendingPresetActivation = nil
  178. }
  179. } message: {
  180. if let activation = pendingPresetActivation {
  181. Text(activation.confirmationMessage)
  182. }
  183. }
  184. }).background(appState.trioBackgroundColor(for: colorScheme))
  185. }
  186. var defaultText: some View {
  187. switch state.selectedTab {
  188. case .overrides:
  189. Section {} header: {
  190. Text("Add Preset or Override by tapping 'Add Override +' in the top right-hand corner of the screen.")
  191. .textCase(nil)
  192. .foregroundStyle(.secondary)
  193. }
  194. case .tempTargets:
  195. Section {} header: {
  196. Text(
  197. "Add Preset or Temp Target by tapping 'Add Temp Target +' in the top right-hand corner of the screen."
  198. )
  199. .textCase(nil)
  200. .foregroundStyle(.secondary)
  201. }
  202. }
  203. }
  204. var currentActiveAdjustment: some View {
  205. switch state.selectedTab {
  206. case .overrides:
  207. Section {
  208. HStack {
  209. Text("\(state.activeOverrideName) is running")
  210. Spacer()
  211. Image(systemName: "square.and.pencil")
  212. .foregroundStyle(Color.primary)
  213. }
  214. .contentShape(Rectangle())
  215. .onTapGesture {
  216. Task {
  217. /// To avoid editing the Preset when a Preset-Override is running we first duplicate the Preset-Override as a non-Preset Override
  218. /// The currentActiveOverride variable in the State will update automatically via MOC notification
  219. await state.duplicateOverridePresetAndCancelPreviousOverride()
  220. /// selectedOverride is used for passing the chosen Override to the EditSheet so we have to set the updated currentActiveOverride to be the selectedOverride
  221. selectedOverride = state.currentActiveOverride
  222. /// Now we can show the Edit sheet
  223. state.showOverrideEditSheet = true
  224. }
  225. }
  226. }
  227. .listRowBackground(Color.purple.opacity(0.8))
  228. case .tempTargets:
  229. Section {
  230. HStack {
  231. Text("\(state.activeTempTargetName) is running")
  232. Spacer()
  233. Image(systemName: "square.and.pencil")
  234. .foregroundStyle(Color.primary)
  235. }
  236. .contentShape(Rectangle())
  237. .onTapGesture {
  238. Task {
  239. /// To avoid editing the Preset when a Preset-Override is running we first duplicate the Preset-Override as a non-Preset Override
  240. /// The currentActiveOverride variable in the State will update automatically via MOC notification
  241. await state.duplicateTempTargetPresetAndCancelPreviousTempTarget()
  242. /// selectedOverride is used for passing the chosen Override to the EditSheet so we have to set the updated currentActiveOverride to be the selectedOverride
  243. selectedTempTarget = state.currentActiveTempTarget
  244. /// Now we can show the Edit sheet
  245. state.showTempTargetEditSheet = true
  246. }
  247. }
  248. }
  249. .listRowBackground(Color.loopGreen.opacity(0.8))
  250. }
  251. }
  252. var cancelAdjustmentButton: some View {
  253. switch state.selectedTab {
  254. case .overrides:
  255. Button(action: {
  256. showCancelOverrideConfirmDialog = true
  257. }, label: {
  258. Text("Stop Override")
  259. })
  260. .frame(maxWidth: .infinity, alignment: .center)
  261. .disabled(!state.isOverrideEnabled)
  262. .listRowBackground(!state.isOverrideEnabled ? Color(.systemGray4) : Color(.systemRed))
  263. .tint(.white)
  264. case .tempTargets:
  265. Button(action: {
  266. showCancelTempTargetConfirmDialog = true
  267. }, label: {
  268. Text("Stop Temp Target")
  269. })
  270. .frame(maxWidth: .infinity, alignment: .center)
  271. .disabled(!state.isTempTargetEnabled)
  272. .listRowBackground(!state.isTempTargetEnabled ? Color(.systemGray4) : Color(.systemRed))
  273. .tint(.white)
  274. }
  275. }
  276. func formattedTimeRemaining(_ timeInterval: TimeInterval) -> String {
  277. let totalSeconds = Int(timeInterval)
  278. let hours = totalSeconds / 3600
  279. let minutes = (totalSeconds % 3600) / 60
  280. let seconds = totalSeconds % 60
  281. if hours > 0 {
  282. return "\(hours)h \(minutes)m \(seconds)s"
  283. } else if minutes > 0 {
  284. return "\(minutes)m \(seconds)s"
  285. } else {
  286. return "<1m"
  287. }
  288. }
  289. }
  290. }
  291. // MARK: Preset Activation Handling
  292. extension Adjustments.RootView: View {
  293. enum PendingPresetActivation {
  294. case override(objectID: NSManagedObjectID, presetID: String?, name: String)
  295. case tempTarget(objectID: NSManagedObjectID, presetID: String?, name: String)
  296. var name: String {
  297. switch self {
  298. case let .override(_, _, name),
  299. let .tempTarget(_, _, name):
  300. return name
  301. }
  302. }
  303. var adjustmentType: String {
  304. switch self {
  305. case .override:
  306. return String(localized: "Override")
  307. case .tempTarget:
  308. return String(localized: "Temp Target")
  309. }
  310. }
  311. var confirmationMessage: String {
  312. String(localized: "Start the \(adjustmentType) \"\(name)\"?", comment: "Confirmation message for starting a preset")
  313. }
  314. }
  315. private var presetActivationConfirmationBinding: Binding<Bool> {
  316. Binding(
  317. get: {
  318. state.requireAdjustmentsConfirmation &&
  319. state.shouldDisplayPresetStartConfirmDialog &&
  320. pendingPresetActivation != nil
  321. },
  322. set: { isPresented in
  323. if !isPresented {
  324. state.shouldDisplayPresetStartConfirmDialog = false
  325. pendingPresetActivation = nil
  326. }
  327. }
  328. )
  329. }
  330. func requestPresetActivation(_ activation: PendingPresetActivation) {
  331. if state.requireAdjustmentsConfirmation {
  332. pendingPresetActivation = activation
  333. state.shouldDisplayPresetStartConfirmDialog = true
  334. } else {
  335. activatePreset(activation)
  336. }
  337. }
  338. func activatePreset(_ activation: PendingPresetActivation) {
  339. Task {
  340. switch activation {
  341. case let .override(objectID, presetID, _):
  342. await state.enactOverridePreset(withID: objectID)
  343. await MainActor.run {
  344. state.hideModal()
  345. selectedOverridePresetID = presetID
  346. showOverrideCheckmark = true
  347. state.shouldDisplayPresetStartConfirmDialog = false
  348. pendingPresetActivation = nil
  349. }
  350. DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
  351. showOverrideCheckmark = false
  352. }
  353. case let .tempTarget(objectID, presetID, _):
  354. await state.enactTempTargetPreset(withID: objectID)
  355. await MainActor.run {
  356. selectedTempTargetPresetID = presetID
  357. showTempTargetCheckmark = true
  358. state.shouldDisplayPresetStartConfirmDialog = false
  359. pendingPresetActivation = nil
  360. }
  361. DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
  362. showTempTargetCheckmark = false
  363. }
  364. }
  365. }
  366. }
  367. }