RemoteView.swift 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  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. if isLoading {
  95. ProgressView("Please wait...")
  96. .padding()
  97. }
  98. }
  99. }
  100. .navigationTitle("Remote")
  101. .navigationBarTitleDisplayMode(.inline)
  102. .alert(isPresented: $showAlert) {
  103. switch alertType {
  104. case .confirmCommand:
  105. return Alert(
  106. title: Text("Confirm Command"),
  107. message: Text("New Target: \(Localizer.formatQuantity(newHKTarget)) \(UserDefaultsRepository.getPreferredUnit().localizedShortUnitString)\nDuration: \(Int(duration.doubleValue(for: HKUnit.minute()))) minutes"),
  108. primaryButton: .default(Text("Confirm"), action: {
  109. enactTempTarget()
  110. }),
  111. secondaryButton: .cancel()
  112. )
  113. case .status:
  114. return Alert(
  115. title: Text("Status"),
  116. message: Text(statusMessage ?? ""),
  117. dismissButton: .default(Text("OK"), action: {
  118. showAlert = false
  119. })
  120. )
  121. case .confirmCancellation:
  122. return Alert(
  123. title: Text("Confirm Cancellation"),
  124. message: Text("Are you sure you want to cancel the existing temp target?"),
  125. primaryButton: .default(Text("Confirm"), action: {
  126. cancelTempTarget()
  127. }),
  128. secondaryButton: .cancel()
  129. )
  130. case .none:
  131. return Alert(title: Text("Unknown Alert"))
  132. }
  133. }
  134. }
  135. }
  136. private var isButtonDisabled: Bool {
  137. return newHKTarget.doubleValue(for: UserDefaultsRepository.getPreferredUnit()) == 0 ||
  138. duration.doubleValue(for: HKUnit.minute()) == 0 || isLoading
  139. }
  140. private func enactTempTarget() {
  141. isLoading = true
  142. sendTempTarget(newHKTarget, duration) { success in
  143. self.isLoading = false
  144. if success {
  145. self.statusMessage = "Target successfully enacted."
  146. } else {
  147. self.statusMessage = "Failed to enact target."
  148. }
  149. self.alertType = .status
  150. self.showAlert = true
  151. }
  152. }
  153. private func cancelTempTarget() {
  154. isLoading = true
  155. onCancelExistingTarget() { success in
  156. self.isLoading = false
  157. if success {
  158. self.statusMessage = "Temp target successfully cancelled."
  159. } else {
  160. self.statusMessage = "Failed to cancel temp target."
  161. }
  162. self.alertType = .status
  163. self.showAlert = true
  164. }
  165. }
  166. }
  167. struct ErrorMessageView: View {
  168. var message: String
  169. var buttonTitle: String?
  170. var buttonAction: (() -> Void)?
  171. var body: some View {
  172. VStack(spacing: 20) {
  173. Text(message)
  174. .foregroundColor(.red)
  175. .multilineTextAlignment(.center)
  176. .padding()
  177. .background(
  178. RoundedRectangle(cornerRadius: 10)
  179. .fill(Color.white)
  180. .shadow(color: .gray, radius: 5, x: 0, y: 2)
  181. )
  182. .padding()
  183. if let buttonTitle = buttonTitle, let buttonAction = buttonAction {
  184. Button(action: buttonAction) {
  185. Text(buttonTitle)
  186. .frame(maxWidth: .infinity)
  187. .padding()
  188. .background(Color.blue)
  189. .foregroundColor(.white)
  190. .cornerRadius(8)
  191. }
  192. }
  193. }
  194. .padding()
  195. .background(
  196. RoundedRectangle(cornerRadius: 10)
  197. .fill(Color(.systemBackground))
  198. .shadow(color: .gray, radius: 5, x: 0, y: 2)
  199. )
  200. .padding()
  201. }
  202. }