AdjustmentsRootView+Overrides.swift 9.4 KB

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