AdjustmentsRootView+TempTargets.swift 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  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.fill")
  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. let presetName = selectedTempTarget?.name ?? ""
  93. return String(
  94. localized: "Delete the Temp Target Preset \"\(presetName)\"?",
  95. comment: "Delete confirmation title for temporary target presets"
  96. )
  97. }
  98. private func deleteConfirmationButtons() -> some View {
  99. Group {
  100. if let itemToDelete = selectedTempTarget {
  101. Button(
  102. state.currentActiveTempTarget == selectedTempTarget ? "Stop and Delete" : "Delete",
  103. role: .destructive
  104. ) {
  105. if state.currentActiveTempTarget == selectedTempTarget {
  106. Task {
  107. await state.disableAllActiveTempTargets(createTempTargetRunEntry: true)
  108. }
  109. }
  110. Task {
  111. await state.invokeTempTargetPresetDeletion(itemToDelete.objectID)
  112. }
  113. selectedTempTarget = nil
  114. }
  115. }
  116. Button("Cancel", role: .cancel) {
  117. selectedTempTarget = nil
  118. }
  119. }
  120. }
  121. private var deleteConfirmationMessage: Text? {
  122. if state.currentActiveTempTarget == selectedTempTarget {
  123. return Text("This Temp Target preset is currently running. Deleting will stop it.")
  124. }
  125. return nil
  126. }
  127. var stickyStopTempTargetButton: some View {
  128. ZStack {
  129. Rectangle()
  130. .frame(width: UIScreen.main.bounds.width, height: 65)
  131. .foregroundStyle(colorScheme == .dark ? Color.bgDarkerDarkBlue : Color.white)
  132. .background(.thinMaterial)
  133. .opacity(0.8)
  134. .clipShape(Rectangle())
  135. Button(action: {
  136. Task {
  137. // Save cancelled Temp Targets in TempTargetRunStored Entity
  138. // Cancel ALL active Temp Targets
  139. await state.disableAllActiveTempTargets(createTempTargetRunEntry: true)
  140. // Update View
  141. state.updateLatestTempTargetConfiguration()
  142. }
  143. }, label: {
  144. Text("Stop Temp Target")
  145. .frame(maxWidth: .infinity, maxHeight: .infinity)
  146. .padding(10)
  147. })
  148. .frame(width: UIScreen.main.bounds.width * 0.9, height: 40, alignment: .center)
  149. .disabled(!state.isTempTargetEnabled)
  150. .background(!state.isTempTargetEnabled ? Color(.systemGray4) : Color(.systemRed))
  151. .tint(.white)
  152. .clipShape(RoundedRectangle(cornerRadius: 8))
  153. .padding(5)
  154. }
  155. }
  156. private func tempTargetView(
  157. for tempTarget: TempTargetStored,
  158. showCheckmark: Bool = false,
  159. onTap: (() -> Void)? = nil
  160. ) -> some View {
  161. let target = tempTarget.target ?? 100
  162. let tempTargetValue = Decimal(target as! Double.RawValue)
  163. let isSelected = tempTarget.id?.uuidString == selectedTempTargetPresetID
  164. let tempTargetHalfBasal = Decimal(
  165. tempTarget.halfBasalTarget as? Double
  166. .RawValue ?? Double(state.settingHalfBasalTarget)
  167. )
  168. let percentage = Int(
  169. state.computeAdjustedPercentage(usingHBT: tempTargetHalfBasal, usingTarget: tempTargetValue)
  170. )
  171. let remainingTime = tempTarget.date?.timeIntervalSinceNow ?? 0
  172. return ZStack(alignment: .trailing) {
  173. HStack {
  174. VStack(alignment: .leading) {
  175. HStack {
  176. Text(tempTarget.name ?? "")
  177. Spacer()
  178. if remainingTime > 0 {
  179. Text("Starts in \(formattedTimeRemaining(remainingTime))")
  180. .foregroundColor(colorScheme == .dark ? .orange : .accentColor)
  181. }
  182. }
  183. HStack(spacing: 2) {
  184. Text(formattedGlucose(glucose: target as Decimal))
  185. .foregroundColor(.secondary)
  186. .font(.caption)
  187. Text("for")
  188. .foregroundColor(.secondary)
  189. .font(.caption)
  190. Text("\(Formatter.integerFormatter.string(from: (tempTarget.duration ?? 0) as NSNumber)!)")
  191. .foregroundColor(.secondary)
  192. .font(.caption)
  193. Text("min")
  194. .foregroundColor(.secondary)
  195. .font(.caption)
  196. if state.isAdjustSensEnabled(usingTarget: tempTargetValue) {
  197. Text(", \(percentage)%")
  198. .foregroundColor(.secondary)
  199. .font(.caption)
  200. }
  201. Spacer()
  202. }
  203. .padding(.top, 2)
  204. }
  205. .contentShape(Rectangle())
  206. .onTapGesture {
  207. onTap?()
  208. }
  209. }
  210. if showCheckmark && isSelected {
  211. Image(systemName: "checkmark.circle.fill")
  212. .imageScale(.large)
  213. .fontWeight(.bold)
  214. .foregroundStyle(Color.green)
  215. } else if onTap != nil {
  216. Image(systemName: "line.3.horizontal")
  217. .imageScale(.medium)
  218. .foregroundStyle(.secondary)
  219. }
  220. }
  221. }
  222. }