RemoteView.swift 7.2 KB

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