AdjustmentsRootView+Overrides.swift 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  1. import CoreData
  2. import SwiftUI
  3. extension Adjustments.RootView {
  4. @ViewBuilder func overrides() -> some View {
  5. if state.isOverrideEnabled, state.activeOverrideName.isNotEmpty {
  6. currentActiveAdjustment
  7. }
  8. if state.overridePresets.isNotEmpty {
  9. overridePresets
  10. } else {
  11. defaultText
  12. }
  13. }
  14. var overridePresets: some View {
  15. Section {
  16. ForEach(state.overridePresets) { preset in
  17. overridesView(for: preset, showCheckMark: showOverrideCheckmark) {
  18. enactOverridePreset(preset)
  19. }
  20. .swipeActions(edge: .trailing, allowsFullSwipe: true) {
  21. swipeActionsForOverrides(for: preset)
  22. }
  23. }
  24. .onMove(perform: state.reorderOverride)
  25. .confirmationDialog(
  26. "Delete the Override Preset \"\(selectedOverride?.name ?? "")\"?",
  27. isPresented: $isConfirmDeletePresented,
  28. titleVisibility: .visible
  29. ) {
  30. if let itemToDelete = selectedOverride {
  31. Button(
  32. state.currentActiveOverride == selectedOverride ? "Stop and Delete" : "Delete",
  33. role: .destructive
  34. ) {
  35. if state.currentActiveOverride == selectedOverride {
  36. Task {
  37. // Save cancelled Override in OverrideRunStored Entity
  38. // Cancel ALL active Override
  39. await state.disableAllActiveOverrides(createOverrideRunEntry: true)
  40. }
  41. }
  42. // Perform the delete action
  43. Task {
  44. await state.invokeOverridePresetDeletion(itemToDelete.objectID)
  45. }
  46. // Reset the selected item after deletion
  47. selectedOverride = nil
  48. }
  49. }
  50. Button("Cancel", role: .cancel) {
  51. // Dismiss the dialog without action
  52. selectedOverride = nil
  53. }
  54. } message: {
  55. if state.currentActiveOverride == selectedOverride {
  56. Text(
  57. state
  58. .currentActiveOverride == selectedOverride ?
  59. "This override preset is currently running. Deleting will stop it." : ""
  60. )
  61. }
  62. }
  63. .listRowBackground(Color.chart)
  64. } header: {
  65. Text("Override Presets")
  66. } footer: {
  67. HStack {
  68. Image(systemName: "hand.draw.fill").foregroundStyle(.primary)
  69. Text("Swipe left to edit or delete an override preset. Hold, drag and drop to reorder a preset.")
  70. }
  71. }
  72. }
  73. func enactOverridePreset(_ preset: OverrideStored) {
  74. Task {
  75. let objectID = preset.objectID
  76. await state.enactOverridePreset(withID: objectID)
  77. state.hideModal()
  78. selectedOverridePresetID = preset.id
  79. showOverrideCheckmark = true
  80. // Deactivate checkmark after 3 seconds
  81. DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
  82. showOverrideCheckmark = false
  83. }
  84. }
  85. }
  86. func swipeActionsForOverrides(for preset: OverrideStored) -> some View {
  87. Group {
  88. Button(role: .none) {
  89. selectedOverride = preset
  90. isConfirmDeletePresented = true
  91. } label: {
  92. Label("Delete", systemImage: "trash.fill")
  93. .tint(.red)
  94. }
  95. Button(action: {
  96. // Set the selected Override to the chosen Preset and pass it to the Edit Sheet
  97. selectedOverride = preset
  98. state.showOverrideEditSheet = true
  99. }, label: {
  100. Label("Edit", systemImage: "pencil")
  101. .tint(.blue)
  102. })
  103. }
  104. }
  105. var overrideLabelDivider: some View {
  106. Divider()
  107. .frame(width: 1, height: 20)
  108. }
  109. var stickyStopOverrideButton: some View {
  110. ZStack {
  111. Rectangle()
  112. .frame(width: UIScreen.main.bounds.width, height: 65)
  113. .foregroundStyle(colorScheme == .dark ? Color.bgDarkerDarkBlue : Color.white)
  114. .background(.thinMaterial)
  115. .opacity(0.8)
  116. .clipShape(Rectangle())
  117. Button(action: {
  118. Task {
  119. // Save cancelled Override in OverrideRunStored Entity
  120. // Cancel ALL active Override
  121. await state.disableAllActiveOverrides(createOverrideRunEntry: true)
  122. }
  123. }, label: {
  124. Text("Stop Override")
  125. .frame(maxWidth: .infinity, maxHeight: .infinity)
  126. .padding(10)
  127. })
  128. .frame(width: UIScreen.main.bounds.width * 0.9, height: 40, alignment: .center)
  129. .disabled(!state.isOverrideEnabled)
  130. .background(!state.isOverrideEnabled ? Color(.systemGray4) : Color(.systemRed))
  131. .tint(.white)
  132. .clipShape(RoundedRectangle(cornerRadius: 8))
  133. .padding(5)
  134. }
  135. }
  136. @ViewBuilder func overridesView(
  137. for preset: OverrideStored,
  138. showCheckMark _: Bool = false,
  139. onTap: (() -> Void)? = nil
  140. ) -> some View {
  141. let isSelected = preset.id == selectedOverridePresetID
  142. let name = preset.name ?? ""
  143. let indefinite = preset.indefinite
  144. let duration = preset.duration?.decimalValue ?? Decimal(0)
  145. let percentage = preset.percentage
  146. let smbMinutes = preset.smbMinutes?.decimalValue ?? Decimal(0)
  147. let uamMinutes = preset.uamMinutes?.decimalValue ?? Decimal(0)
  148. let target: String = {
  149. guard let targetValue = preset.target, targetValue != 0 else { return "" }
  150. return state.units == .mgdL ? targetValue.description : targetValue.decimalValue.formattedAsMmolL
  151. }()
  152. let targetString = target.isEmpty ? "" : "\(target) \(state.units.rawValue)"
  153. let durationString = indefinite ? "" : "\(state.formatHrMin(Int(duration)))"
  154. let scheduledSMBString: String = {
  155. guard preset.smbIsScheduledOff, preset.start != preset.end else { return "" }
  156. return " \(formatTimeRange(start: preset.start?.stringValue, end: preset.end?.stringValue))"
  157. }()
  158. let smbString: String = {
  159. guard preset.smbIsOff || preset.smbIsScheduledOff else { return "" }
  160. return "SMBs Off\(scheduledSMBString)"
  161. }()
  162. let maxSmbMinsString: String = {
  163. guard smbMinutes != 0, preset.advancedSettings, !preset.smbIsOff,
  164. smbMinutes != state.defaultSmbMinutes else { return "" }
  165. return "\(smbMinutes.formatted()) min SMB"
  166. }()
  167. let maxUamMinsString: String = {
  168. guard uamMinutes != 0, preset.advancedSettings, !preset.smbIsOff,
  169. uamMinutes != state.defaultUamMinutes else { return "" }
  170. return "\(uamMinutes.formatted()) min UAM"
  171. }()
  172. let isfAndCrString: String = {
  173. switch (preset.isfAndCr, preset.isf, preset.cr) {
  174. case (_, true, true),
  175. (true, _, _):
  176. return " ISF/CR"
  177. case (false, true, false):
  178. return " ISF"
  179. case (false, false, true):
  180. return " CR"
  181. default:
  182. return ""
  183. }
  184. }()
  185. let percentageString = percentage != 100 ? "\(Int(percentage))%\(isfAndCrString)" : ""
  186. // Combine all labels into a single array, filtering out empty strings
  187. let labels: [String] = [
  188. durationString,
  189. percentageString,
  190. targetString,
  191. smbString,
  192. maxSmbMinsString,
  193. maxUamMinsString
  194. ].filter { !$0.isEmpty }
  195. if !name.isEmpty {
  196. ZStack(alignment: .trailing) {
  197. HStack {
  198. VStack {
  199. HStack {
  200. Text(name)
  201. Spacer()
  202. }
  203. HStack(spacing: 5) {
  204. ForEach(labels, id: \.self) { label in
  205. Text(label)
  206. if label != labels.last { // Add divider between labels
  207. overrideLabelDivider
  208. }
  209. }
  210. Spacer()
  211. }
  212. .padding(.top, 2)
  213. .foregroundColor(.secondary)
  214. .font(.caption)
  215. }
  216. .contentShape(Rectangle())
  217. .onTapGesture {
  218. onTap?()
  219. }
  220. }
  221. // show checkmark to indicate if the preset was actually pressed
  222. if showOverrideCheckmark && isSelected {
  223. Image(systemName: "checkmark.circle.fill")
  224. .imageScale(.large)
  225. .fontWeight(.bold)
  226. .foregroundStyle(Color.green)
  227. } else {
  228. Image(systemName: "line.3.horizontal")
  229. .imageScale(.medium)
  230. .foregroundStyle(.secondary)
  231. }
  232. }
  233. }
  234. }
  235. }