AdjustmentsRootView+TempTargets.swift 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  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. showCancelTempTargetConfirmDialog = true
  137. }, label: {
  138. Text("Stop Temp Target")
  139. .frame(maxWidth: .infinity, maxHeight: .infinity)
  140. .padding(10)
  141. })
  142. .frame(width: UIScreen.main.bounds.width * 0.9, height: 40, alignment: .center)
  143. .disabled(!state.isTempTargetEnabled)
  144. .background(!state.isTempTargetEnabled ? Color(.systemGray4) : Color(.systemRed))
  145. .tint(.white)
  146. .clipShape(RoundedRectangle(cornerRadius: 8))
  147. .padding(5)
  148. }
  149. }
  150. private func tempTargetView(
  151. for tempTarget: TempTargetStored,
  152. showCheckmark: Bool = false,
  153. onTap: (() -> Void)? = nil
  154. ) -> some View {
  155. let target = tempTarget.target ?? 100
  156. let tempTargetValue = Decimal(target as! Double.RawValue)
  157. let isSelected = tempTarget.id?.uuidString == selectedTempTargetPresetID
  158. let tempTargetHalfBasal = Decimal(
  159. tempTarget.halfBasalTarget as? Double
  160. .RawValue ?? Double(state.settingHalfBasalTarget)
  161. )
  162. let percentage = Int(
  163. TempTargetCalculations.computeAdjustedPercentage(
  164. halfBasalTarget: tempTargetHalfBasal,
  165. target: tempTargetValue,
  166. autosensMax: state.autosensMax
  167. )
  168. )
  169. let remainingTime = tempTarget.date?.timeIntervalSinceNow ?? 0
  170. return ZStack(alignment: .trailing) {
  171. HStack {
  172. VStack(alignment: .leading) {
  173. HStack {
  174. Text(tempTarget.name ?? "")
  175. Spacer()
  176. if remainingTime > 0 {
  177. Text("Starts in \(formattedTimeRemaining(remainingTime))")
  178. .foregroundColor(colorScheme == .dark ? .orange : .accentColor)
  179. }
  180. }
  181. HStack(spacing: 2) {
  182. Text(formattedGlucose(glucose: target as Decimal))
  183. .foregroundColor(.secondary)
  184. .font(.caption)
  185. Text("for")
  186. .foregroundColor(.secondary)
  187. .font(.caption)
  188. Text("\(Formatter.integerFormatter.string(from: (tempTarget.duration ?? 0) as NSNumber)!)")
  189. .foregroundColor(.secondary)
  190. .font(.caption)
  191. Text("min")
  192. .foregroundColor(.secondary)
  193. .font(.caption)
  194. if state.isAdjustSensEnabled(usingTarget: tempTargetValue) {
  195. Text(", \(percentage)%")
  196. .foregroundColor(.secondary)
  197. .font(.caption)
  198. }
  199. Spacer()
  200. }
  201. .padding(.top, 2)
  202. }
  203. .contentShape(Rectangle())
  204. .onTapGesture {
  205. onTap?()
  206. }
  207. }
  208. if showCheckmark && isSelected {
  209. Image(systemName: "checkmark.circle.fill")
  210. .imageScale(.large)
  211. .fontWeight(.bold)
  212. .foregroundStyle(Color.green)
  213. } else if onTap != nil {
  214. Image(systemName: "line.3.horizontal")
  215. .imageScale(.medium)
  216. .foregroundStyle(.secondary)
  217. }
  218. }
  219. }
  220. }