RemoteView.swift 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  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. var onCancelExistingTarget: (@escaping (Bool) -> Void) -> Void
  22. var sendTempTarget: (HKQuantity, HKQuantity, @escaping (Bool) -> Void) -> Void
  23. enum AlertType {
  24. case confirmCommand
  25. case status
  26. case confirmCancellation
  27. }
  28. var body: some View {
  29. NavigationView {
  30. VStack {
  31. if nightscoutURL.value.isEmpty {
  32. ErrorMessageView(
  33. 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."
  34. )
  35. } else if device.value != "Trio" {
  36. ErrorMessageView(
  37. message: "Remote commands are currently only available for Trio."
  38. )
  39. } else if !nsWriteAuth.value {
  40. ErrorMessageView(
  41. message: "Please enter a valid token with appropriate permissions."
  42. )
  43. } else {
  44. Form {
  45. if let tempTargetValue = tempTarget.value {
  46. Section(header: Text("Existing Temp Target")) {
  47. HStack {
  48. Text("Current Target")
  49. Spacer()
  50. Text(Localizer.formatQuantity(tempTargetValue))
  51. Text(UserDefaultsRepository.getPreferredUnit().localizedShortUnitString).foregroundColor(.secondary)
  52. }
  53. Button {
  54. alertType = .confirmCancellation
  55. showAlert = true
  56. } label: {
  57. HStack {
  58. Text("Cancel Temp Target")
  59. Spacer()
  60. Image(systemName: "xmark.app")
  61. .font(.title)
  62. }
  63. }
  64. .tint(.red)
  65. }
  66. }
  67. Section(header: Text("Temporary Target")) {
  68. HStack {
  69. Text("Target")
  70. Spacer()
  71. TextFieldWithToolBar(quantity: $newHKTarget, unit: UserDefaultsRepository.getPreferredUnit())
  72. Text(UserDefaultsRepository.getPreferredUnit().localizedShortUnitString).foregroundColor(.secondary)
  73. }
  74. HStack {
  75. Text("Duration")
  76. Spacer()
  77. TextFieldWithToolBar(quantity: $duration, unit: HKUnit.minute())
  78. Text("minutes").foregroundColor(.secondary)
  79. }
  80. HStack {
  81. Button {
  82. alertType = .confirmCommand
  83. showAlert = true
  84. } label: {
  85. Text("Enact")
  86. }
  87. .disabled(isButtonDisabled)
  88. .buttonStyle(BorderlessButtonStyle())
  89. .font(.callout)
  90. .controlSize(.mini)
  91. }
  92. }
  93. }
  94. .disabled(isLoading)
  95. if isLoading {
  96. ProgressView("Please wait...")
  97. .padding()
  98. }
  99. }
  100. }
  101. .navigationTitle("Remote")
  102. .navigationBarTitleDisplayMode(.inline)
  103. .alert(isPresented: $showAlert) {
  104. switch alertType {
  105. case .confirmCommand:
  106. return Alert(
  107. title: Text("Confirm Command"),
  108. message: Text("New Target: \(Localizer.formatQuantity(newHKTarget)) \(UserDefaultsRepository.getPreferredUnit().localizedShortUnitString)\nDuration: \(Int(duration.doubleValue(for: HKUnit.minute()))) minutes"),
  109. primaryButton: .default(Text("Confirm"), action: {
  110. enactTempTarget()
  111. }),
  112. secondaryButton: .cancel()
  113. )
  114. case .status:
  115. return Alert(
  116. title: Text("Status"),
  117. message: Text(statusMessage ?? ""),
  118. dismissButton: .default(Text("OK"), action: {
  119. showAlert = false
  120. })
  121. )
  122. case .confirmCancellation:
  123. return Alert(
  124. title: Text("Confirm Cancellation"),
  125. message: Text("Are you sure you want to cancel the existing temp target?"),
  126. primaryButton: .default(Text("Confirm"), action: {
  127. cancelTempTarget()
  128. }),
  129. secondaryButton: .cancel()
  130. )
  131. case .none:
  132. return Alert(title: Text("Unknown Alert"))
  133. }
  134. }
  135. }
  136. }
  137. private var isButtonDisabled: Bool {
  138. return newHKTarget.doubleValue(for: UserDefaultsRepository.getPreferredUnit()) == 0 ||
  139. duration.doubleValue(for: HKUnit.minute()) == 0
  140. }
  141. private func enactTempTarget() {
  142. isLoading = true
  143. print("Enacting Temp Target with target: \(newHKTarget) and duration: \(duration)")
  144. sendTempTarget(newHKTarget, duration) { success in
  145. self.isLoading = false
  146. if success {
  147. print("Target successfully enacted.")
  148. self.statusMessage = "Target successfully enacted."
  149. } else {
  150. print("Failed to enact target.")
  151. self.statusMessage = "Failed to enact target."
  152. }
  153. DispatchQueue.main.async {
  154. self.alertType = .status
  155. self.showAlert = true
  156. }
  157. }
  158. }
  159. private func cancelTempTarget() {
  160. isLoading = true
  161. print("Cancelling Temp Target...")
  162. onCancelExistingTarget() { success in
  163. self.isLoading = false
  164. if success {
  165. print("Temp target successfully cancelled.")
  166. self.statusMessage = "Temp target successfully cancelled."
  167. } else {
  168. print("Failed to cancel temp target.")
  169. self.statusMessage = "Failed to cancel temp target."
  170. }
  171. DispatchQueue.main.async {
  172. self.alertType = .status
  173. self.showAlert = true
  174. }
  175. }
  176. }
  177. }
  178. struct ErrorMessageView: View {
  179. var message: String
  180. var buttonTitle: String?
  181. var buttonAction: (() -> Void)?
  182. var body: some View {
  183. VStack(spacing: 20) {
  184. Text(message)
  185. .foregroundColor(.red)
  186. .multilineTextAlignment(.center)
  187. .padding()
  188. .background(
  189. RoundedRectangle(cornerRadius: 10)
  190. .fill(Color.white)
  191. .shadow(color: .gray, radius: 5, x: 0, y: 2)
  192. )
  193. .padding()
  194. if let buttonTitle = buttonTitle, let buttonAction = buttonAction {
  195. Button(action: buttonAction) {
  196. Text(buttonTitle)
  197. .frame(maxWidth: .infinity)
  198. .padding()
  199. .background(Color.blue)
  200. .foregroundColor(.white)
  201. .cornerRadius(8)
  202. }
  203. }
  204. }
  205. .padding()
  206. .background(
  207. RoundedRectangle(cornerRadius: 10)
  208. .fill(Color(.systemBackground))
  209. .shadow(color: .gray, radius: 5, x: 0, y: 2)
  210. )
  211. .padding()
  212. }
  213. }