RemoteView.swift 6.9 KB

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