AdjustmentsRootView+Overrides.swift 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  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. showCancelOverrideConfirmDialog = true
  119. }, label: {
  120. Text("Stop Override")
  121. .frame(maxWidth: .infinity, maxHeight: .infinity)
  122. .padding(10)
  123. })
  124. .frame(width: UIScreen.main.bounds.width * 0.9, height: 40, alignment: .center)
  125. .disabled(!state.isOverrideEnabled)
  126. .background(!state.isOverrideEnabled ? Color(.systemGray4) : Color(.systemRed))
  127. .tint(.white)
  128. .clipShape(RoundedRectangle(cornerRadius: 8))
  129. .padding(5)
  130. }
  131. }
  132. @ViewBuilder func overridesView(
  133. for preset: OverrideStored,
  134. showCheckMark _: Bool = false,
  135. onTap: (() -> Void)? = nil
  136. ) -> some View {
  137. let isSelected = preset.id == selectedOverridePresetID
  138. let name = preset.name ?? ""
  139. let indefinite = preset.indefinite
  140. let duration = preset.duration?.decimalValue ?? Decimal(0)
  141. let percentage = preset.percentage
  142. let smbMinutes = preset.smbMinutes?.decimalValue ?? Decimal(0)
  143. let uamMinutes = preset.uamMinutes?.decimalValue ?? Decimal(0)
  144. let target: String = {
  145. guard let targetValue = preset.target, targetValue != 0 else { return "" }
  146. return state.units == .mgdL ? targetValue.description : targetValue.decimalValue.formattedAsMmolL
  147. }()
  148. let targetString = target.isEmpty ? "" : "\(target) \(state.units.rawValue)"
  149. let durationString = indefinite ? "" : "\(state.formatHoursAndMinutes(Int(duration)))"
  150. let scheduledSMBString: String = {
  151. guard preset.smbIsScheduledOff, preset.start != preset.end else { return "" }
  152. return " \(formatTimeRange(start: preset.start?.stringValue, end: preset.end?.stringValue))"
  153. }()
  154. let smbString: String = {
  155. guard preset.smbIsOff || preset.smbIsScheduledOff else { return "" }
  156. return "SMBs Off\(scheduledSMBString)"
  157. }()
  158. let maxSmbMinsString: String = {
  159. guard smbMinutes != 0, preset.advancedSettings, !preset.smbIsOff,
  160. smbMinutes != state.defaultSmbMinutes else { return "" }
  161. return "\(smbMinutes.formatted()) min SMB"
  162. }()
  163. let maxUamMinsString: String = {
  164. guard uamMinutes != 0, preset.advancedSettings, !preset.smbIsOff,
  165. uamMinutes != state.defaultUamMinutes else { return "" }
  166. return "\(uamMinutes.formatted()) min UAM"
  167. }()
  168. let isfAndCrString: String = {
  169. switch (preset.isfAndCr, preset.isf, preset.cr) {
  170. case (_, true, true),
  171. (true, _, _):
  172. return " ISF/CR"
  173. case (false, true, false):
  174. return " ISF"
  175. case (false, false, true):
  176. return " CR"
  177. default:
  178. return ""
  179. }
  180. }()
  181. let percentageString = percentage != 100 ? "\(Int(percentage))%\(isfAndCrString)" : ""
  182. // Combine all labels into a single array, filtering out empty strings
  183. let labels: [String] = [
  184. durationString,
  185. percentageString,
  186. targetString,
  187. smbString,
  188. maxSmbMinsString,
  189. maxUamMinsString
  190. ].filter { !$0.isEmpty }
  191. if !name.isEmpty {
  192. ZStack(alignment: .trailing) {
  193. HStack {
  194. VStack {
  195. HStack {
  196. Text(name)
  197. Spacer()
  198. }
  199. HStack(spacing: 5) {
  200. ForEach(labels, id: \.self) { label in
  201. Text(label)
  202. if label != labels.last { // Add divider between labels
  203. overrideLabelDivider
  204. }
  205. }
  206. Spacer()
  207. }
  208. .padding(.top, 2)
  209. .foregroundColor(.secondary)
  210. .font(.caption)
  211. }
  212. .contentShape(Rectangle())
  213. .onTapGesture {
  214. onTap?()
  215. }
  216. }
  217. // show checkmark to indicate if the preset was actually pressed
  218. if showOverrideCheckmark && isSelected {
  219. Image(systemName: "checkmark.circle.fill")
  220. .imageScale(.large)
  221. .fontWeight(.bold)
  222. .foregroundStyle(Color.green)
  223. } else {
  224. Image(systemName: "line.3.horizontal")
  225. .imageScale(.medium)
  226. .foregroundStyle(.secondary)
  227. }
  228. }
  229. }
  230. }
  231. }