TrioNightscoutRemoteView.swift 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  1. // LoopFollow
  2. // TrioNightscoutRemoteView.swift
  3. // Created by Jonas Björkert.
  4. import HealthKit
  5. import SwiftUI
  6. struct TrioNightscoutRemoteView: View {
  7. private let remoteController = TrioNightscoutRemoteController()
  8. @ObservedObject var nightscoutURL = Storage.shared.url
  9. @ObservedObject var device = Storage.shared.device
  10. @ObservedObject var nsWriteAuth = Storage.shared.nsWriteAuth
  11. @ObservedObject var tempTarget = Observable.shared.tempTarget
  12. @State private var newHKTarget = HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 0.0)
  13. @State private var duration = HKQuantity(unit: .minute(), doubleValue: 0.0)
  14. @State private var showAlert: Bool = false
  15. @State private var alertType: AlertType? = nil
  16. @State private var alertMessage: String? = nil
  17. @State private var isLoading: Bool = false
  18. @State private var statusMessage: String? = nil
  19. @State private var showPresetSheet: Bool = false
  20. @State private var presetName = ""
  21. @ObservedObject var presetManager = TempTargetPresetManager.shared
  22. @FocusState private var targetFieldIsFocused: Bool
  23. @FocusState private var durationFieldIsFocused: Bool
  24. enum AlertType {
  25. case confirmCommand
  26. case status
  27. case validation
  28. case confirmCancellation
  29. }
  30. var body: some View {
  31. NavigationView {
  32. VStack {
  33. if nightscoutURL.value.isEmpty {
  34. ErrorMessageView(
  35. 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."
  36. )
  37. } else if device.value != "Trio" {
  38. ErrorMessageView(
  39. message: "Remote commands are currently only available for Trio."
  40. )
  41. } else if !nsWriteAuth.value {
  42. ErrorMessageView(
  43. message: "Please update your token to include the 'careportal' and 'readable' roles in order to do remote commands with Trio."
  44. )
  45. } else {
  46. Form {
  47. if let tempTargetValue = tempTarget.value {
  48. Section(header: Text("Existing Temp Target")) {
  49. HStack {
  50. Text("Current Target")
  51. Spacer()
  52. Text(Localizer.formatQuantity(tempTargetValue))
  53. Text(Localizer.getPreferredUnit().localizedShortUnitString).foregroundColor(.secondary)
  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 .status:
  179. return Alert(
  180. title: Text("Status"),
  181. message: Text(statusMessage ?? ""),
  182. dismissButton: .default(Text("OK"), action: {
  183. showAlert = false
  184. })
  185. )
  186. case .confirmCancellation:
  187. return Alert(
  188. title: Text("Confirm Cancellation"),
  189. message: Text("Are you sure you want to cancel the existing temp target?"),
  190. primaryButton: .default(Text("Confirm"), action: {
  191. cancelTempTarget()
  192. }),
  193. secondaryButton: .cancel()
  194. )
  195. case .validation:
  196. return Alert(
  197. title: Text("Validation Error"),
  198. message: Text(alertMessage ?? "Invalid input."),
  199. dismissButton: .default(Text("OK"))
  200. )
  201. case .none:
  202. return Alert(title: Text("Unknown Alert"))
  203. }
  204. }
  205. .sheet(isPresented: $showPresetSheet) {
  206. VStack {
  207. Text("Save Preset")
  208. .font(.headline)
  209. .padding()
  210. TextField("Preset Name", text: $presetName)
  211. .textFieldStyle(RoundedBorderTextFieldStyle())
  212. .padding()
  213. HStack {
  214. Button("Cancel") {
  215. showPresetSheet = false
  216. }
  217. .padding()
  218. Spacer()
  219. Button("Save") {
  220. presetManager.addPreset(name: presetName, target: newHKTarget, duration: duration)
  221. presetName = ""
  222. showPresetSheet = false
  223. }
  224. .disabled(presetName.isEmpty)
  225. .padding()
  226. }
  227. Spacer()
  228. }
  229. .padding()
  230. }
  231. }
  232. }
  233. private var isButtonDisabled: Bool {
  234. return newHKTarget.doubleValue(for: Localizer.getPreferredUnit()) == 0 ||
  235. duration.doubleValue(for: HKUnit.minute()) == 0 || isLoading
  236. }
  237. private func enactTempTarget() {
  238. isLoading = true
  239. remoteController.sendTempTarget(newTarget: newHKTarget, duration: duration) { success in
  240. DispatchQueue.main.async {
  241. self.isLoading = false
  242. if success {
  243. self.statusMessage = "Command successfully sent to Nightscout."
  244. LogManager.shared.log(
  245. category: .nightscout,
  246. message: "sendTempTarget succeeded - New Target: \(Localizer.formatQuantity(newHKTarget)) \(Localizer.getPreferredUnit().localizedShortUnitString), Duration: \(Int(duration.doubleValue(for: HKUnit.minute()))) minutes"
  247. )
  248. } else {
  249. self.statusMessage = "Failed to enact target."
  250. LogManager.shared.log(
  251. category: .nightscout,
  252. message: "sendTempTarget failed - New Target: \(Localizer.formatQuantity(newHKTarget)) \(Localizer.getPreferredUnit().localizedShortUnitString), Duration: \(Int(duration.doubleValue(for: HKUnit.minute()))) minutes"
  253. )
  254. }
  255. self.alertType = .status
  256. self.showAlert = true
  257. }
  258. }
  259. }
  260. private func cancelTempTarget() {
  261. isLoading = true
  262. remoteController.cancelExistingTarget { success in
  263. DispatchQueue.main.async {
  264. self.isLoading = false
  265. if success {
  266. self.statusMessage = "Cancellation request successfully sent to Nightscout."
  267. LogManager.shared.log(
  268. category: .nightscout,
  269. message: "cancelExistingTarget succeeded"
  270. )
  271. } else {
  272. self.statusMessage = "Failed to cancel temp target."
  273. LogManager.shared.log(
  274. category: .nightscout,
  275. message: "cancelExistingTarget failed"
  276. )
  277. }
  278. self.alertType = .status
  279. self.showAlert = true
  280. }
  281. }
  282. }
  283. private func handleValidationError(_ message: String) {
  284. alertMessage = message
  285. alertType = .validation
  286. showAlert = true
  287. }
  288. }