TempTargetView.swift 14 KB

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