TempTargetView.swift 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  1. //
  2. // TempTargetView.swift
  3. // LoopFollow
  4. //
  5. // Created by Jonas Björkert on 2024-08-25.
  6. // Copyright © 2024 Jon Fawcett. All rights reserved.
  7. //
  8. import SwiftUI
  9. import HealthKit
  10. struct TempTargetView: View {
  11. @Environment(\.presentationMode) private var presentationMode
  12. private let pushNotificationManager = PushNotificationManager()
  13. @ObservedObject var device = ObservableUserDefaults.shared.device
  14. @ObservedObject var tempTarget = Observable.shared.tempTarget
  15. @State private var newHKTarget = HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 0.0)
  16. @State private var duration = HKQuantity(unit: .minute(), doubleValue: 0.0)
  17. @State private var showAlert: Bool = false
  18. @State private var alertType: AlertType? = nil
  19. @State private var alertMessage: String? = nil
  20. @State private var isLoading: Bool = false
  21. @State private var statusMessage: String? = nil
  22. @State private var showPresetSheet: Bool = false
  23. @State private var presetName = ""
  24. @ObservedObject var presetManager = TempTargetPresetManager.shared
  25. @FocusState private var targetFieldIsFocused: Bool
  26. @FocusState private var durationFieldIsFocused: Bool
  27. enum AlertType {
  28. case confirmCommand
  29. case statusSuccess
  30. case statusFailure
  31. case validation
  32. case confirmCancellation
  33. }
  34. var body: some View {
  35. NavigationView {
  36. VStack {
  37. if device.value != "Trio" {
  38. ErrorMessageView(
  39. message: "Remote commands are currently only available for Trio."
  40. )
  41. } else {
  42. Form {
  43. if let tempTargetValue = tempTarget.value {
  44. Section(header: Text("Existing Temp Target")) {
  45. HStack {
  46. Text("Current Target")
  47. Spacer()
  48. Text(Localizer.formatQuantity(tempTargetValue))
  49. Text(UserDefaultsRepository.getPreferredUnit().localizedShortUnitString).foregroundColor(.secondary)
  50. }
  51. Button {
  52. alertType = .confirmCancellation
  53. showAlert = true
  54. } label: {
  55. HStack {
  56. Text("Cancel Temp Target")
  57. Spacer()
  58. Image(systemName: "xmark.app")
  59. .font(.title)
  60. }
  61. }
  62. .tint(.red)
  63. }
  64. }
  65. Section(header: Text("Temporary Target")) {
  66. HStack {
  67. Text("Target")
  68. Spacer()
  69. TextFieldWithToolBar(
  70. quantity: $newHKTarget,
  71. maxLength: 4,
  72. unit: UserDefaultsRepository.getPreferredUnit(),
  73. minValue: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 80),
  74. maxValue: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 200),
  75. onValidationError: { message in
  76. handleValidationError(message)
  77. }
  78. )
  79. .focused($targetFieldIsFocused)
  80. Text(UserDefaultsRepository.getPreferredUnit().localizedShortUnitString).foregroundColor(.secondary)
  81. }
  82. HStack {
  83. Text("Duration")
  84. Spacer()
  85. TextFieldWithToolBar(
  86. quantity: $duration,
  87. maxLength: 4,
  88. unit: HKUnit.minute(),
  89. minValue: HKQuantity(unit: .minute(), doubleValue: 5),
  90. onValidationError: { message in
  91. handleValidationError(message)
  92. }
  93. )
  94. .focused($durationFieldIsFocused)
  95. Text("minutes").foregroundColor(.secondary)
  96. }
  97. HStack {
  98. Button {
  99. alertType = .confirmCommand
  100. showAlert = true
  101. targetFieldIsFocused = false
  102. durationFieldIsFocused = false
  103. } label: {
  104. Text("Enact")
  105. }
  106. .disabled(isButtonDisabled)
  107. .buttonStyle(BorderlessButtonStyle())
  108. .font(.callout)
  109. .controlSize(.mini)
  110. Spacer()
  111. Button {
  112. showPresetSheet = true
  113. targetFieldIsFocused = false
  114. durationFieldIsFocused = false
  115. } label: {
  116. Text("Save as Preset")
  117. }
  118. .disabled(isButtonDisabled)
  119. .buttonStyle(BorderlessButtonStyle())
  120. .font(.callout)
  121. .controlSize(.mini)
  122. }
  123. }
  124. if !presetManager.presets.isEmpty {
  125. Section(header: Text("Presets")) {
  126. ForEach(presetManager.presets) { preset in
  127. HStack {
  128. Text(preset.name)
  129. Spacer()
  130. }
  131. .contentShape(Rectangle())
  132. .onTapGesture {
  133. alertType = .confirmCommand
  134. newHKTarget = preset.target
  135. duration = preset.duration
  136. showAlert = true
  137. targetFieldIsFocused = false
  138. durationFieldIsFocused = false
  139. }
  140. .swipeActions {
  141. Button(role: .destructive) {
  142. if let index = presetManager.presets.firstIndex(where: { $0.id == preset.id }) {
  143. presetManager.deletePreset(at: index)
  144. }
  145. targetFieldIsFocused = false
  146. durationFieldIsFocused = false
  147. } label: {
  148. Label("Delete", systemImage: "trash")
  149. }
  150. }
  151. }
  152. }
  153. }
  154. }
  155. if isLoading {
  156. ProgressView("Please wait...")
  157. .padding()
  158. }
  159. }
  160. }
  161. .navigationTitle("Remote")
  162. .navigationBarTitleDisplayMode(.inline)
  163. .alert(isPresented: $showAlert) {
  164. switch alertType {
  165. case .confirmCommand:
  166. return Alert(
  167. title: Text("Confirm Command"),
  168. message: Text("New Target: \(Localizer.formatQuantity(newHKTarget)) \(UserDefaultsRepository.getPreferredUnit().localizedShortUnitString)\nDuration: \(Int(duration.doubleValue(for: HKUnit.minute()))) minutes"),
  169. primaryButton: .default(Text("Confirm"), action: {
  170. enactTempTarget()
  171. }),
  172. secondaryButton: .cancel()
  173. )
  174. case .statusSuccess:
  175. return Alert(
  176. title: Text("Status"),
  177. message: Text(statusMessage ?? ""),
  178. dismissButton: .default(Text("OK"), action: {
  179. presentationMode.wrappedValue.dismiss()
  180. })
  181. )
  182. case .statusFailure:
  183. return Alert(
  184. title: Text("Status"),
  185. message: Text(statusMessage ?? ""),
  186. dismissButton: .default(Text("OK"))
  187. )
  188. case .confirmCancellation:
  189. return Alert(
  190. title: Text("Confirm Cancellation"),
  191. message: Text("Are you sure you want to cancel the existing temp target?"),
  192. primaryButton: .default(Text("Confirm"), action: {
  193. cancelTempTarget()
  194. }),
  195. secondaryButton: .cancel()
  196. )
  197. case .validation:
  198. return Alert(
  199. title: Text("Validation Error"),
  200. message: Text(alertMessage ?? "Invalid input."),
  201. dismissButton: .default(Text("OK"))
  202. )
  203. case .none:
  204. return Alert(title: Text("Unknown Alert"))
  205. }
  206. }
  207. .sheet(isPresented: $showPresetSheet) {
  208. VStack {
  209. Text("Save Preset")
  210. .font(.headline)
  211. .padding()
  212. TextField("Preset Name", text: $presetName)
  213. .textFieldStyle(RoundedBorderTextFieldStyle())
  214. .padding()
  215. HStack {
  216. Button("Cancel") {
  217. showPresetSheet = false
  218. }
  219. .padding()
  220. Spacer()
  221. Button("Save") {
  222. presetManager.addPreset(name: presetName, target: newHKTarget, duration: duration)
  223. presetName = ""
  224. showPresetSheet = false
  225. }
  226. .disabled(presetName.isEmpty)
  227. .padding()
  228. }
  229. Spacer()
  230. }
  231. .padding()
  232. }
  233. }
  234. }
  235. private var isButtonDisabled: Bool {
  236. return newHKTarget.doubleValue(for: UserDefaultsRepository.getPreferredUnit()) == 0 ||
  237. duration.doubleValue(for: HKUnit.minute()) == 0 || isLoading
  238. }
  239. private func enactTempTarget() {
  240. isLoading = true
  241. pushNotificationManager.sendTempTargetPushNotification(target: newHKTarget, duration: duration) { success, errorMessage in
  242. DispatchQueue.main.async {
  243. self.isLoading = false
  244. if success {
  245. self.statusMessage = "Temp target command successfully sent."
  246. self.alertType = .statusSuccess
  247. } else {
  248. self.statusMessage = errorMessage ?? "Failed to send temp target command."
  249. self.alertType = .statusFailure
  250. }
  251. self.showAlert = true
  252. }
  253. }
  254. }
  255. private func cancelTempTarget() {
  256. isLoading = true
  257. pushNotificationManager.sendCancelTempTargetPushNotification { success, errorMessage in
  258. DispatchQueue.main.async {
  259. self.isLoading = false
  260. if success {
  261. self.statusMessage = "Cancel temp target command successfully sent."
  262. self.alertType = .statusSuccess
  263. } else {
  264. self.statusMessage = errorMessage ?? "Failed to send cancel temp target command."
  265. self.alertType = .statusFailure
  266. }
  267. self.showAlert = true
  268. }
  269. }
  270. }
  271. private func handleValidationError(_ message: String) {
  272. alertMessage = message
  273. alertType = .validation
  274. showAlert = true
  275. }
  276. }