LoopOverrideView.swift 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. //
  2. // LoopOverrideView.swift
  3. // LoopFollow
  4. //
  5. // Created by Jonas Björkert on 2025-01-14.
  6. // Copyright © 2025 Jon Fawcett. All rights reserved.
  7. //
  8. import SwiftUI
  9. import HealthKit
  10. struct LoopOverrideView: View {
  11. @Environment(\.presentationMode) private var presentationMode
  12. @ObservedObject var device = ObservableUserDefaults.shared.device
  13. @ObservedObject var overrideNote = Observable.shared.override
  14. @ObservedObject var nsAdmin = ObservableUserDefaults.shared.nsWriteAuth
  15. @StateObject private var viewModel = LoopOverrideViewModel()
  16. @State private var showAlert: Bool = false
  17. @State private var alertType: AlertType? = nil
  18. @State private var alertMessage: String? = nil
  19. @State private var isLoading: Bool = false
  20. @State private var statusMessage: String? = nil
  21. @State private var selectedOverride: ProfileManager.LoopOverride? = nil
  22. @State private var showConfirmation: Bool = false
  23. @FocusState private var noteFieldIsFocused: Bool
  24. private let pushNotificationManager = PushNotificationManager()
  25. private var profileManager = ProfileManager.shared
  26. enum AlertType {
  27. case confirmActivation
  28. case confirmCancellation
  29. case statusSuccess
  30. case statusFailure
  31. case validation
  32. }
  33. var body: some View {
  34. NavigationView {
  35. VStack {
  36. if device.value != "Loop" {
  37. ErrorMessageView(
  38. message: "Remote commands are currently only available for Loop."
  39. )
  40. } else if !nsAdmin.value {
  41. ErrorMessageView(
  42. message: "Please update your token to include the 'admin' role in order to do remote commands with Loop."
  43. )
  44. } else {
  45. Form {
  46. if let activeNote = overrideNote.value {
  47. Section(header: Text("Active Override")) {
  48. HStack {
  49. Text("Override")
  50. Spacer()
  51. Text(activeNote)
  52. .foregroundColor(.secondary)
  53. }
  54. Button {
  55. alertType = .confirmCancellation
  56. showAlert = true
  57. } label: {
  58. HStack {
  59. Text("Cancel Override")
  60. Spacer()
  61. Image(systemName: "xmark.app")
  62. .font(.title)
  63. }
  64. }
  65. .tint(.red)
  66. }
  67. Section {
  68. Text("Setting a new override while one is active will cancel the active override and set the new one.")
  69. .foregroundColor(.secondary)
  70. }
  71. }
  72. Section(header: Text("Available Overrides")) {
  73. if profileManager.loopOverrides.isEmpty {
  74. Text("No overrides available.")
  75. .foregroundColor(.secondary)
  76. } else {
  77. ForEach(profileManager.loopOverrides, id: \.name) { override in
  78. Button(action: {
  79. selectedOverride = override
  80. alertType = .confirmActivation
  81. showAlert = true
  82. }) {
  83. HStack {
  84. VStack(alignment: .leading) {
  85. Text("\(override.symbol) \(override.name)")
  86. .font(.headline)
  87. Text("Duration: \(formattedDuration(from: override.duration))")
  88. .font(.subheadline)
  89. .foregroundColor(.secondary)
  90. Text("Insulin Scale Factor: \(String(format: "%.2f", override.insulinNeedsScaleFactor))")
  91. .font(.subheadline)
  92. .foregroundColor(.secondary)
  93. if !override.targetRange.isEmpty {
  94. let range = override.targetRange.map { Localizer.formatQuantity($0) }.joined(separator: " - ")
  95. Text("Target Range: \(range) \(UserDefaultsRepository.getPreferredUnit().localizedShortUnitString)")
  96. .font(.subheadline)
  97. .foregroundColor(.secondary)
  98. }
  99. }
  100. Spacer()
  101. Image(systemName: "arrow.right.circle")
  102. .foregroundColor(.blue)
  103. }
  104. }
  105. }
  106. }
  107. }
  108. }
  109. if isLoading {
  110. ProgressView("Please wait...")
  111. .padding()
  112. }
  113. }
  114. }
  115. .navigationTitle("Loop Overrides")
  116. .navigationBarTitleDisplayMode(.inline)
  117. .alert(isPresented: $showAlert) {
  118. switch alertType {
  119. case .confirmActivation:
  120. return Alert(
  121. title: Text("Activate Override"),
  122. message: Text("Do you want to activate the override '\(selectedOverride?.name ?? "")'?"),
  123. primaryButton: .default(Text("Confirm"), action: {
  124. if let override = selectedOverride {
  125. activateOverride(override: override)
  126. }
  127. }),
  128. secondaryButton: .cancel()
  129. )
  130. case .confirmCancellation:
  131. return Alert(
  132. title: Text("Cancel Override"),
  133. message: Text("Are you sure you want to cancel the active override?"),
  134. primaryButton: .default(Text("Confirm"), action: {
  135. cancelOverride()
  136. }),
  137. secondaryButton: .cancel()
  138. )
  139. case .statusSuccess:
  140. return Alert(
  141. title: Text("Success"),
  142. message: Text(statusMessage ?? ""),
  143. dismissButton: .default(Text("OK"), action: {
  144. presentationMode.wrappedValue.dismiss()
  145. })
  146. )
  147. case .statusFailure:
  148. return Alert(
  149. title: Text("Error"),
  150. message: Text(statusMessage ?? "An error occurred."),
  151. dismissButton: .default(Text("OK"))
  152. )
  153. case .validation:
  154. return Alert(
  155. title: Text("Validation Error"),
  156. message: Text(alertMessage ?? "Invalid input."),
  157. dismissButton: .default(Text("OK"))
  158. )
  159. case .none:
  160. return Alert(title: Text("Unknown Alert"))
  161. }
  162. }
  163. }
  164. }
  165. // MARK: - Functions
  166. private func formattedDuration(from duration: Int?) -> String {
  167. guard let duration = duration, duration != 0 else {
  168. return "Indefinitely"
  169. }
  170. let hours = duration / 3600
  171. let minutes = (duration % 3600) / 60
  172. if hours > 0 && minutes > 0 {
  173. return "\(hours) hr, \(minutes) min"
  174. } else if hours > 0 {
  175. return "\(hours) hr"
  176. } else {
  177. return "\(minutes) min"
  178. }
  179. }
  180. private func activateOverride(override: ProfileManager.LoopOverride) {
  181. isLoading = true
  182. viewModel.sendActivateOverrideRequest(override: override) { success, message in
  183. self.isLoading = false
  184. if success {
  185. self.statusMessage = "Override command sent successfully."
  186. self.alertType = .statusSuccess
  187. } else {
  188. self.statusMessage = message ?? "Failed to send override command."
  189. self.alertType = .statusFailure
  190. }
  191. self.showAlert = true
  192. }
  193. }
  194. private func cancelOverride() {
  195. isLoading = true
  196. viewModel.sendCancelOverrideRequest { success, message in
  197. self.isLoading = false
  198. if success {
  199. self.statusMessage = "Cancellation request successfully sent to Nightscout."
  200. self.alertType = .statusSuccess
  201. } else {
  202. self.statusMessage = message ?? "Failed to cancel temp target."
  203. self.alertType = .statusFailure
  204. }
  205. self.showAlert = true
  206. }
  207. }
  208. }