TrioNightscoutRemoteView.swift 14 KB

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