AdjustmentsRootView+TempTargets.swift 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  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. actionButtonsForTempTargets(for: tempTarget, deleteRole: nil)
  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. requestTempTargetPresetActivation(preset)
  35. }
  36. .contextMenu {
  37. actionButtonsForTempTargets(for: preset)
  38. }
  39. .swipeActions(edge: .trailing, allowsFullSwipe: true) {
  40. actionButtonsForTempTargets(for: preset, deleteRole: nil)
  41. }
  42. }
  43. .onMove(perform: state.reorderTempTargets)
  44. .listRowBackground(Color.chart)
  45. } header: {
  46. Text("Temporary Target Presets")
  47. } footer: {
  48. HStack {
  49. Image(systemName: "hand.draw.fill").foregroundStyle(.primary)
  50. Text("Swipe left to edit or delete a temporary target preset. Hold, drag and drop to reorder a preset.")
  51. }
  52. }
  53. }
  54. private func requestTempTargetPresetActivation(_ preset: TempTargetStored) {
  55. let activation = PendingPresetActivation.tempTarget(
  56. objectID: preset.objectID,
  57. presetID: preset.id?.uuidString,
  58. name: preset.name ?? ""
  59. )
  60. requestPresetActivation(activation)
  61. }
  62. private func actionButtonsForTempTargets(
  63. for tempTarget: TempTargetStored,
  64. deleteRole: ButtonRole? = .destructive
  65. ) -> some View {
  66. Group {
  67. Button(role: deleteRole) {
  68. tempTargetToDelete = tempTarget
  69. } label: {
  70. Label("Delete", systemImage: "trash.fill")
  71. }
  72. .tint(.red)
  73. Button {
  74. selectedTempTarget = tempTarget
  75. state.showTempTargetEditSheet = true
  76. } label: {
  77. Label("Edit", systemImage: "pencil")
  78. }
  79. .tint(.blue)
  80. }
  81. }
  82. func tempTargetDeleteConfirmation(_ content: some View) -> some View {
  83. content.confirmationDialog(
  84. String(
  85. localized: "Delete the Temp Target Preset \"\(tempTargetToDelete?.name ?? "")\"?",
  86. comment: "Delete confirmation title for temporary target presets"
  87. ),
  88. isPresented: Binding(
  89. get: { tempTargetToDelete != nil },
  90. set: { if !$0 { tempTargetToDelete = nil } }
  91. ),
  92. titleVisibility: .visible,
  93. presenting: tempTargetToDelete
  94. ) { target in
  95. Button(
  96. state.currentActiveTempTarget == target ? "Stop and Delete" : "Delete",
  97. role: .destructive
  98. ) {
  99. if state.currentActiveTempTarget == target {
  100. Task {
  101. await state.disableAllActiveTempTargets(createTempTargetRunEntry: true)
  102. }
  103. }
  104. Task {
  105. await state.invokeTempTargetPresetDeletion(target.objectID)
  106. }
  107. }
  108. Button("Cancel", role: .cancel) {}
  109. } message: { target in
  110. if state.currentActiveTempTarget == target {
  111. Text("This Temp Target preset is currently running. Deleting will stop it.")
  112. }
  113. }
  114. }
  115. var stickyStopTempTargetButton: some View {
  116. ZStack {
  117. Rectangle()
  118. .frame(width: UIScreen.main.bounds.width, height: 65)
  119. .foregroundStyle(colorScheme == .dark ? Color.bgDarkerDarkBlue : Color.white)
  120. .background(.thinMaterial)
  121. .opacity(0.8)
  122. .clipShape(Rectangle())
  123. Button(action: {
  124. showCancelTempTargetConfirmDialog = true
  125. }, label: {
  126. Text("Stop Temp Target")
  127. .frame(maxWidth: .infinity, maxHeight: .infinity)
  128. .padding(10)
  129. })
  130. .frame(width: UIScreen.main.bounds.width * 0.9, height: 40, alignment: .center)
  131. .disabled(!state.isTempTargetEnabled)
  132. .background(!state.isTempTargetEnabled ? Color(.systemGray4) : Color(.systemRed))
  133. .tint(.white)
  134. .clipShape(RoundedRectangle(cornerRadius: 8))
  135. .padding(5)
  136. }
  137. }
  138. private func tempTargetView(
  139. for tempTarget: TempTargetStored,
  140. showCheckmark: Bool = false,
  141. onTap: (() -> Void)? = nil
  142. ) -> some View {
  143. let target = tempTarget.target ?? 100
  144. let tempTargetValue = Decimal(target as! Double.RawValue)
  145. let isSelected = tempTarget.id?.uuidString == selectedTempTargetPresetID
  146. let tempTargetHalfBasal = Decimal(
  147. tempTarget.halfBasalTarget as? Double
  148. .RawValue ?? Double(state.settingHalfBasalTarget)
  149. )
  150. let percentage = Int(
  151. TempTargetCalculations.computeAdjustedPercentage(
  152. halfBasalTarget: tempTargetHalfBasal,
  153. target: tempTargetValue,
  154. autosensMax: state.autosensMax
  155. )
  156. )
  157. let remainingTime = tempTarget.date?.timeIntervalSinceNow ?? 0
  158. return ZStack(alignment: .trailing) {
  159. HStack {
  160. VStack(alignment: .leading) {
  161. HStack {
  162. Text(tempTarget.name ?? "")
  163. Spacer()
  164. if remainingTime > 0 {
  165. Text("Starts in \(formattedTimeRemaining(remainingTime))")
  166. .foregroundColor(colorScheme == .dark ? .orange : .accentColor)
  167. }
  168. }
  169. HStack(spacing: 2) {
  170. Text(formattedGlucose(glucose: target as Decimal))
  171. .foregroundColor(.secondary)
  172. .font(.caption)
  173. Text("for")
  174. .foregroundColor(.secondary)
  175. .font(.caption)
  176. Text("\(Formatter.integerFormatter.string(from: (tempTarget.duration ?? 0) as NSNumber)!)")
  177. .foregroundColor(.secondary)
  178. .font(.caption)
  179. Text("min")
  180. .foregroundColor(.secondary)
  181. .font(.caption)
  182. if state.isAdjustSensEnabled(usingTarget: tempTargetValue) {
  183. Text(", \(percentage)%")
  184. .foregroundColor(.secondary)
  185. .font(.caption)
  186. }
  187. Spacer()
  188. }
  189. .padding(.top, 2)
  190. }
  191. .contentShape(Rectangle())
  192. .onTapGesture {
  193. onTap?()
  194. }
  195. }
  196. if showCheckmark && isSelected {
  197. Image(systemName: "checkmark.circle.fill")
  198. .imageScale(.large)
  199. .fontWeight(.bold)
  200. .foregroundStyle(Color.green)
  201. } else if onTap != nil {
  202. Image(systemName: "line.3.horizontal")
  203. .imageScale(.medium)
  204. .foregroundStyle(.secondary)
  205. }
  206. }
  207. }
  208. }