RemoteView.swift 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  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 newTarget: Double = 0
  17. @State private var newHKTarget = HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 0.0)
  18. @State private var duration: Double = 0
  19. @State private var intDuration: Int = 0
  20. @State private var showConfirmation: Bool = false
  21. @State private var showCheckmark: Bool = false
  22. var onRefreshStatus: () -> Void
  23. var onCancelExistingTarget: () -> Void
  24. var sendTempTarget: (HKQuantity, Int) -> 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: \(Localizer.formatQuantity(tempTargetValue)) mg/dL")
  46. Spacer()
  47. Button(action: onCancelExistingTarget) {
  48. Text("Cancel")
  49. .foregroundColor(.red)
  50. }
  51. }
  52. }
  53. }
  54. Section(header: Text("Temporary Target")) {
  55. HStack {
  56. Text("Target")
  57. Spacer()
  58. TextFieldWithToolBar(text: $newTarget, placeholder: "0", numberFormatter: glucoseFormatter)
  59. Text(UserDefaultsRepository.getPreferredUnit().localizedShortUnitString).foregroundColor(.secondary)
  60. }
  61. HStack {
  62. Text("Duration")
  63. Spacer()
  64. TextFieldWithToolBar(text: $duration, placeholder: "0", numberFormatter: formatter)
  65. Text("minutes").foregroundColor(.secondary)
  66. }
  67. HStack {
  68. Button {
  69. newHKTarget = HKQuantity(unit: UserDefaultsRepository.getPreferredUnit(), doubleValue: newTarget)
  70. intDuration = Int(duration)
  71. showConfirmation = true
  72. }
  73. label: { Text("Enact") }
  74. .disabled(newTarget == 0 || duration == 0)
  75. .buttonStyle(BorderlessButtonStyle())
  76. .font(.callout)
  77. .controlSize(.mini)
  78. }
  79. }
  80. .alert(isPresented: $showConfirmation) {
  81. Alert(
  82. title: Text("Confirm Command"),
  83. message: Text("New Target: \(Localizer.formatQuantity(newHKTarget)) \(UserDefaultsRepository.getPreferredUnit().localizedShortUnitString)\nDuration: \(intDuration) minutes"),
  84. primaryButton: .default(Text("Confirm"), action: {
  85. sendTempTarget(newHKTarget , intDuration)
  86. }),
  87. secondaryButton: .cancel()
  88. )
  89. }
  90. }
  91. .navigationBarItems(trailing: Button(action: onRefreshStatus) {
  92. Image(systemName: "arrow.clockwise")
  93. })
  94. .padding()
  95. if !statusMessage.value.isEmpty {
  96. Text(statusMessage.value)
  97. .foregroundColor(.green)
  98. .padding()
  99. }
  100. }
  101. }
  102. .padding()
  103. .navigationTitle("Remote")
  104. .navigationBarTitleDisplayMode(.inline)
  105. }
  106. }
  107. private var formatter: NumberFormatter {
  108. let formatter = NumberFormatter()
  109. formatter.numberStyle = .decimal
  110. formatter.maximumFractionDigits = 0
  111. return formatter
  112. }
  113. private var glucoseFormatter: NumberFormatter {
  114. let formatter = NumberFormatter()
  115. formatter.numberStyle = .decimal
  116. formatter.maximumFractionDigits = 0
  117. if UserDefaultsRepository.getPreferredUnit() == .millimolesPerLiter {
  118. formatter.maximumFractionDigits = 1
  119. }
  120. formatter.roundingMode = .halfUp
  121. return formatter
  122. }
  123. }
  124. struct ErrorMessageView: View {
  125. var message: String
  126. var buttonTitle: String?
  127. var buttonAction: (() -> Void)?
  128. var body: some View {
  129. VStack(spacing: 20) {
  130. Text(message)
  131. .foregroundColor(.red)
  132. .multilineTextAlignment(.center)
  133. .padding()
  134. .background(
  135. RoundedRectangle(cornerRadius: 10)
  136. .fill(Color.white)
  137. .shadow(color: .gray, radius: 5, x: 0, y: 2)
  138. )
  139. .padding()
  140. if let buttonTitle = buttonTitle, let buttonAction = buttonAction {
  141. Button(action: buttonAction) {
  142. Text(buttonTitle)
  143. .frame(maxWidth: .infinity)
  144. .padding()
  145. .background(Color.blue)
  146. .foregroundColor(.white)
  147. .cornerRadius(8)
  148. }
  149. }
  150. }
  151. .padding()
  152. .background(
  153. RoundedRectangle(cornerRadius: 10)
  154. .fill(Color(.systemBackground))
  155. .shadow(color: .gray, radius: 5, x: 0, y: 2)
  156. )
  157. .padding()
  158. }
  159. }