RemoteView.swift 8.5 KB

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