AdjustmentsRootView+TempTargets.swift 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. import CoreData
  2. import SwiftUI
  3. extension Adjustments.RootView {
  4. @ViewBuilder func tempTargets() -> some View {
  5. if state.isTempTargetEnabled, state.activeTempTargetName.isNotEmpty {
  6. currentActiveAdjustment
  7. }
  8. if state.scheduledTempTargets.isNotEmpty {
  9. scheduledTempTargets
  10. }
  11. if state.tempTargetPresets.isNotEmpty {
  12. tempTargetPresets
  13. } else {
  14. defaultText
  15. }
  16. }
  17. private var scheduledTempTargets: some View {
  18. Section {
  19. ForEach(state.scheduledTempTargets) { tempTarget in
  20. tempTargetView(for: tempTarget)
  21. .swipeActions(edge: .trailing, allowsFullSwipe: true) {
  22. swipeActionsForTempTargets(for: tempTarget)
  23. }
  24. }
  25. .listRowBackground(Color.chart)
  26. } header: {
  27. Text("Scheduled Temp Targets")
  28. }
  29. }
  30. private var tempTargetPresets: some View {
  31. Section {
  32. ForEach(state.tempTargetPresets) { preset in
  33. tempTargetView(for: preset, showCheckmark: showTempTargetCheckmark) {
  34. enactTempTargetPreset(preset)
  35. }
  36. .swipeActions(edge: .trailing, allowsFullSwipe: true) {
  37. swipeActionsForTempTargets(for: preset)
  38. }
  39. }
  40. .onMove(perform: state.reorderTempTargets)
  41. .confirmationDialog(
  42. deleteConfirmationTitle,
  43. isPresented: $isConfirmDeletePresented,
  44. titleVisibility: .visible
  45. ) {
  46. deleteConfirmationButtons()
  47. } message: {
  48. deleteConfirmationMessage
  49. }
  50. .listRowBackground(Color.chart)
  51. } header: {
  52. Text("Temporary Target Presets")
  53. } footer: {
  54. HStack {
  55. Image(systemName: "hand.draw.fill").foregroundStyle(.primary)
  56. Text("Swipe left to edit or delete a temporary target preset. Hold, drag and drop to reorder a preset.")
  57. }
  58. }
  59. }
  60. private func enactTempTargetPreset(_ preset: TempTargetStored) {
  61. Task {
  62. let objectID = preset.objectID
  63. await state.enactTempTargetPreset(withID: objectID)
  64. selectedTempTargetPresetID = preset.id?.uuidString
  65. showTempTargetCheckmark = true
  66. DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
  67. showTempTargetCheckmark = false
  68. }
  69. }
  70. }
  71. private func swipeActionsForTempTargets(for tempTarget: TempTargetStored) -> some View {
  72. Group {
  73. Button {
  74. Task {
  75. selectedTempTarget = tempTarget
  76. isConfirmDeletePresented = true
  77. }
  78. } label: {
  79. Label("Delete", systemImage: "trash")
  80. .tint(.red)
  81. }
  82. Button(action: {
  83. selectedTempTarget = tempTarget
  84. state.showTempTargetEditSheet = true
  85. }, label: {
  86. Label("Edit", systemImage: "pencil")
  87. .tint(.blue)
  88. })
  89. }
  90. }
  91. private var deleteConfirmationTitle: String {
  92. "Delete the Temp Target Preset \"\(selectedTempTarget?.name ?? "")\"?"
  93. }
  94. private func deleteConfirmationButtons() -> some View {
  95. Group {
  96. if let itemToDelete = selectedTempTarget {
  97. Button(
  98. state.currentActiveTempTarget == selectedTempTarget ? "Stop and Delete" : "Delete",
  99. role: .destructive
  100. ) {
  101. if state.currentActiveTempTarget == selectedTempTarget {
  102. Task {
  103. await state.disableAllActiveTempTargets(createTempTargetRunEntry: true)
  104. }
  105. }
  106. Task {
  107. await state.invokeTempTargetPresetDeletion(itemToDelete.objectID)
  108. }
  109. selectedTempTarget = nil
  110. }
  111. }
  112. Button("Cancel", role: .cancel) {
  113. selectedTempTarget = nil
  114. }
  115. }
  116. }
  117. private var deleteConfirmationMessage: Text? {
  118. if state.currentActiveTempTarget == selectedTempTarget {
  119. return Text("This Temp Target preset is currently running. Deleting will stop it.")
  120. }
  121. return nil
  122. }
  123. var stickyStopTempTargetButton: some View {
  124. ZStack {
  125. Rectangle()
  126. .frame(width: UIScreen.main.bounds.width, height: 65)
  127. .foregroundStyle(colorScheme == .dark ? Color.bgDarkerDarkBlue : Color.white)
  128. .background(.thinMaterial)
  129. .opacity(0.8)
  130. .clipShape(Rectangle())
  131. Button(action: {
  132. Task {
  133. // Save cancelled Temp Targets in TempTargetRunStored Entity
  134. // Cancel ALL active Temp Targets
  135. await state.disableAllActiveTempTargets(createTempTargetRunEntry: true)
  136. // Update View
  137. state.updateLatestTempTargetConfiguration()
  138. }
  139. }, label: {
  140. Text("Stop Temp Target")
  141. .frame(maxWidth: .infinity, maxHeight: .infinity)
  142. .padding(10)
  143. })
  144. .frame(width: UIScreen.main.bounds.width * 0.9, height: 40, alignment: .center)
  145. .disabled(!state.isTempTargetEnabled)
  146. .background(!state.isTempTargetEnabled ? Color(.systemGray4) : Color(.systemRed))
  147. .tint(.white)
  148. .clipShape(RoundedRectangle(cornerRadius: 8))
  149. .padding(5)
  150. }
  151. }
  152. private func tempTargetView(
  153. for tempTarget: TempTargetStored,
  154. showCheckmark: Bool = false,
  155. onTap: (() -> Void)? = nil
  156. ) -> some View {
  157. let target = tempTarget.target ?? 100
  158. let tempTargetValue = Decimal(target as! Double.RawValue)
  159. let isSelected = tempTarget.id?.uuidString == selectedTempTargetPresetID
  160. let tempTargetHalfBasal = Decimal(
  161. tempTarget.halfBasalTarget as? Double
  162. .RawValue ?? Double(state.settingHalfBasalTarget)
  163. )
  164. let percentage = Int(
  165. state.computeAdjustedPercentage(usingHBT: tempTargetHalfBasal, usingTarget: tempTargetValue)
  166. )
  167. let remainingTime = tempTarget.date?.timeIntervalSinceNow ?? 0
  168. return ZStack(alignment: .trailing) {
  169. HStack {
  170. VStack(alignment: .leading) {
  171. HStack {
  172. Text(tempTarget.name ?? "")
  173. Spacer()
  174. if remainingTime > 0 {
  175. Text("Starts in \(formattedTimeRemaining(remainingTime))")
  176. .foregroundColor(colorScheme == .dark ? .orange : .accentColor)
  177. }
  178. }
  179. HStack(spacing: 2) {
  180. Text(formattedGlucose(glucose: target as Decimal))
  181. .foregroundColor(.secondary)
  182. .font(.caption)
  183. Text("for")
  184. .foregroundColor(.secondary)
  185. .font(.caption)
  186. Text("\(Formatter.integerFormatter.string(from: (tempTarget.duration ?? 0) as NSNumber)!)")
  187. .foregroundColor(.secondary)
  188. .font(.caption)
  189. Text("min")
  190. .foregroundColor(.secondary)
  191. .font(.caption)
  192. if state.isAdjustSensEnabled(usingTarget: tempTargetValue) {
  193. Text(", \(percentage)%")
  194. .foregroundColor(.secondary)
  195. .font(.caption)
  196. }
  197. Spacer()
  198. }
  199. .padding(.top, 2)
  200. }
  201. .contentShape(Rectangle())
  202. .onTapGesture {
  203. onTap?()
  204. }
  205. }
  206. if showCheckmark && isSelected {
  207. Image(systemName: "checkmark.circle.fill")
  208. .imageScale(.large)
  209. .fontWeight(.bold)
  210. .foregroundStyle(Color.green)
  211. } else if onTap != nil {
  212. Image(systemName: "line.3.horizontal")
  213. .imageScale(.medium)
  214. .foregroundStyle(.secondary)
  215. }
  216. }
  217. }
  218. }