RemoteView.swift 12 KB

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