RemoteView.swift 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  1. //
  2. // RemoteView.swift
  3. // LoopFollow
  4. //
  5. // Created by Jonas Björkert on 2024-07-19.
  6. // Copyright © 2024 Jon Fawcett. All rights reserved.
  7. //
  8. import SwiftUI
  9. import HealthKit
  10. struct RemoteView: View {
  11. @ObservedObject var nightscoutURL = ObservableUserDefaults.shared.url
  12. @ObservedObject var device = ObservableUserDefaults.shared.device
  13. @ObservedObject var nsWriteAuth = ObservableUserDefaults.shared.nsWriteAuth
  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 isLoading: Bool = false
  20. @State private var statusMessage: String? = nil
  21. @State private var showPresetSheet: Bool = false
  22. @State private var presetName = ""
  23. @ObservedObject var presetManager = TempTargetPresetManager.shared
  24. var onCancelExistingTarget: (@escaping (Bool) -> Void) -> Void
  25. var sendTempTarget: (HKQuantity, HKQuantity, @escaping (Bool) -> Void) -> Void
  26. enum AlertType {
  27. case confirmCommand
  28. case status
  29. case confirmCancellation
  30. }
  31. var body: some View {
  32. NavigationView {
  33. VStack {
  34. if nightscoutURL.value.isEmpty {
  35. ErrorMessageView(
  36. message: "Remote commands are currently only available for Trio. It requires you to enter your Nightscout address and a token with the careportal role in the settings."
  37. )
  38. } else if device.value != "Trio" {
  39. ErrorMessageView(
  40. message: "Remote commands are currently only available for Trio."
  41. )
  42. } else if !nsWriteAuth.value {
  43. ErrorMessageView(
  44. message: "Please update your token to include the 'careportal' and 'readable' roles in order to do remote commands."
  45. )
  46. } else {
  47. Form {
  48. if let tempTargetValue = tempTarget.value {
  49. Section(header: Text("Existing Temp Target")) {
  50. HStack {
  51. Text("Current Target")
  52. Spacer()
  53. Text(Localizer.formatQuantity(tempTargetValue))
  54. Text(UserDefaultsRepository.getPreferredUnit().localizedShortUnitString).foregroundColor(.secondary)
  55. }
  56. Button {
  57. alertType = .confirmCancellation
  58. showAlert = true
  59. } label: {
  60. HStack {
  61. Text("Cancel Temp Target")
  62. Spacer()
  63. Image(systemName: "xmark.app")
  64. .font(.title)
  65. }
  66. }
  67. .tint(.red)
  68. }
  69. }
  70. Section(header: Text("Temporary Target")) {
  71. HStack {
  72. Text("Target")
  73. Spacer()
  74. TextFieldWithToolBar(quantity: $newHKTarget, maxLength: 4, unit: UserDefaultsRepository.getPreferredUnit(), minValue: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 60), maxValue: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 300))
  75. Text(UserDefaultsRepository.getPreferredUnit().localizedShortUnitString).foregroundColor(.secondary)
  76. }
  77. HStack {
  78. Text("Duration")
  79. Spacer()
  80. TextFieldWithToolBar(quantity: $duration, maxLength: 4, unit: HKUnit.minute(), minValue: HKQuantity(unit: .minute(), doubleValue: 5))
  81. Text("minutes").foregroundColor(.secondary)
  82. }
  83. HStack {
  84. Button {
  85. alertType = .confirmCommand
  86. showAlert = true
  87. } label: {
  88. Text("Enact")
  89. }
  90. .disabled(isButtonDisabled)
  91. .buttonStyle(BorderlessButtonStyle())
  92. .font(.callout)
  93. .controlSize(.mini)
  94. Spacer() // Pushes the next button to the right
  95. Button {
  96. showPresetSheet = true
  97. } label: {
  98. Text("Save as Preset")
  99. }
  100. .disabled(isButtonDisabled)
  101. .buttonStyle(BorderlessButtonStyle())
  102. .font(.callout)
  103. .controlSize(.mini)
  104. }
  105. }
  106. if !presetManager.presets.isEmpty {
  107. Section(header: Text("Presets")) {
  108. ForEach(presetManager.presets) { preset in
  109. HStack {
  110. Text(preset.name)
  111. Spacer()
  112. }
  113. .contentShape(Rectangle())
  114. .onTapGesture {
  115. alertType = .confirmCommand
  116. newHKTarget = preset.target
  117. duration = preset.duration
  118. showAlert = true
  119. }
  120. .swipeActions {
  121. Button(role: .destructive) {
  122. if let index = presetManager.presets.firstIndex(where: { $0.id == preset.id }) {
  123. presetManager.deletePreset(at: index)
  124. }
  125. } label: {
  126. Label("Delete", systemImage: "trash")
  127. }
  128. }
  129. }
  130. }
  131. }
  132. }
  133. if isLoading {
  134. ProgressView("Please wait...")
  135. .padding()
  136. }
  137. }
  138. }
  139. .navigationTitle("Remote")
  140. .navigationBarTitleDisplayMode(.inline)
  141. .alert(isPresented: $showAlert) {
  142. switch alertType {
  143. case .confirmCommand:
  144. return Alert(
  145. title: Text("Confirm Command"),
  146. message: Text("New Target: \(Localizer.formatQuantity(newHKTarget)) \(UserDefaultsRepository.getPreferredUnit().localizedShortUnitString)\nDuration: \(Int(duration.doubleValue(for: HKUnit.minute()))) minutes"),
  147. primaryButton: .default(Text("Confirm"), action: {
  148. enactTempTarget()
  149. }),
  150. secondaryButton: .cancel()
  151. )
  152. case .status:
  153. return Alert(
  154. title: Text("Status"),
  155. message: Text(statusMessage ?? ""),
  156. dismissButton: .default(Text("OK"), action: {
  157. showAlert = false
  158. })
  159. )
  160. case .confirmCancellation:
  161. return Alert(
  162. title: Text("Confirm Cancellation"),
  163. message: Text("Are you sure you want to cancel the existing temp target?"),
  164. primaryButton: .default(Text("Confirm"), action: {
  165. cancelTempTarget()
  166. }),
  167. secondaryButton: .cancel()
  168. )
  169. case .none:
  170. return Alert(title: Text("Unknown Alert"))
  171. }
  172. }
  173. .sheet(isPresented: $showPresetSheet) {
  174. VStack {
  175. Text("Save Preset")
  176. .font(.headline)
  177. .padding()
  178. TextField("Preset Name", text: $presetName)
  179. .textFieldStyle(RoundedBorderTextFieldStyle())
  180. .padding()
  181. HStack {
  182. Button("Cancel") {
  183. showPresetSheet = false
  184. }
  185. .padding()
  186. Spacer()
  187. Button("Save") {
  188. presetManager.addPreset(name: presetName, target: newHKTarget, duration: duration)
  189. presetName = "" // Clear the preset name after saving
  190. showPresetSheet = false
  191. }
  192. .disabled(presetName.isEmpty)
  193. .padding()
  194. }
  195. Spacer()
  196. }
  197. .padding()
  198. }
  199. }
  200. }
  201. private var isButtonDisabled: Bool {
  202. return newHKTarget.doubleValue(for: UserDefaultsRepository.getPreferredUnit()) == 0 ||
  203. duration.doubleValue(for: HKUnit.minute()) == 0 || isLoading
  204. }
  205. private func enactTempTarget() {
  206. isLoading = true
  207. sendTempTarget(newHKTarget, duration) { success in
  208. self.isLoading = false
  209. if success {
  210. self.statusMessage = "Target successfully enacted."
  211. } else {
  212. self.statusMessage = "Failed to enact target."
  213. }
  214. self.alertType = .status
  215. self.showAlert = true
  216. }
  217. }
  218. private func cancelTempTarget() {
  219. isLoading = true
  220. onCancelExistingTarget() { success in
  221. self.isLoading = false
  222. if success {
  223. self.statusMessage = "Temp target successfully cancelled."
  224. } else {
  225. self.statusMessage = "Failed to cancel temp target."
  226. }
  227. self.alertType = .status
  228. self.showAlert = true
  229. }
  230. }
  231. }