RemoteSettingsView.swift 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. // LoopFollow
  2. // RemoteSettingsView.swift
  3. // Created by Jonas Björkert.
  4. import HealthKit
  5. import SwiftUI
  6. struct RemoteSettingsView: View {
  7. @ObservedObject var viewModel: RemoteSettingsViewModel
  8. @State private var showAlert: Bool = false
  9. @State private var alertType: AlertType? = nil
  10. @State private var alertMessage: String? = nil
  11. enum AlertType {
  12. case validation
  13. }
  14. var body: some View {
  15. NavigationView {
  16. Form {
  17. // MARK: - Remote Type Section (Custom Rows)
  18. Section(header: Text("Remote Type")) {
  19. remoteTypeRow(type: .none, label: "None", isEnabled: true)
  20. remoteTypeRow(type: .nightscout, label: "Nightscout", isEnabled: true)
  21. remoteTypeRow(
  22. type: .trc,
  23. label: "Trio Remote Control",
  24. isEnabled: viewModel.isTrioDevice
  25. )
  26. Text("Nightscout is the only option for Loop.\nNightscout should be used for Trio 0.2.x or older.")
  27. .font(.footnote)
  28. .foregroundColor(.secondary)
  29. }
  30. // MARK: - User Information Section
  31. if viewModel.remoteType != .none {
  32. Section(header: Text("User Information")) {
  33. HStack {
  34. Text("User")
  35. TextField("Enter User", text: $viewModel.user)
  36. .autocapitalization(.none)
  37. .disableAutocorrection(true)
  38. .multilineTextAlignment(.trailing)
  39. }
  40. }
  41. }
  42. // MARK: - Trio Remote Control Settings
  43. if viewModel.remoteType == .trc {
  44. Section(header: Text("Trio Remote Control Settings")) {
  45. HStack {
  46. Text("Shared Secret")
  47. TogglableSecureInput(
  48. placeholder: "Enter Shared Secret",
  49. text: $viewModel.sharedSecret,
  50. style: .singleLine
  51. )
  52. }
  53. HStack {
  54. Text("APNS Key ID")
  55. TogglableSecureInput(
  56. placeholder: "Enter APNS Key ID",
  57. text: $viewModel.keyId,
  58. style: .singleLine
  59. )
  60. }
  61. VStack(alignment: .leading) {
  62. Text("APNS Key")
  63. TogglableSecureInput(
  64. placeholder: "Paste APNS Key",
  65. text: $viewModel.apnsKey,
  66. style: .multiLine
  67. )
  68. .frame(minHeight: 110)
  69. }
  70. }
  71. // MARK: - Guardrails
  72. Section(header: Text("Guardrails")) {
  73. HStack {
  74. Text("Max Bolus")
  75. Spacer()
  76. TextFieldWithToolBar(
  77. quantity: $viewModel.maxBolus,
  78. maxLength: 4,
  79. unit: HKUnit.internationalUnit(),
  80. allowDecimalSeparator: true,
  81. minValue: HKQuantity(unit: .internationalUnit(), doubleValue: 0.0),
  82. maxValue: HKQuantity(unit: .internationalUnit(), doubleValue: 10.0),
  83. onValidationError: { message in
  84. handleValidationError(message)
  85. }
  86. )
  87. .frame(width: 100)
  88. Text("U")
  89. .foregroundColor(.secondary)
  90. }
  91. HStack {
  92. Text("Max Carbs")
  93. Spacer()
  94. TextFieldWithToolBar(
  95. quantity: $viewModel.maxCarbs,
  96. maxLength: 4,
  97. unit: HKUnit.gram(),
  98. allowDecimalSeparator: true,
  99. minValue: HKQuantity(unit: .gram(), doubleValue: 0),
  100. maxValue: HKQuantity(unit: .gram(), doubleValue: 100),
  101. onValidationError: { message in
  102. handleValidationError(message)
  103. }
  104. )
  105. .frame(width: 100)
  106. Text("g")
  107. .foregroundColor(.secondary)
  108. }
  109. HStack {
  110. Text("Max Protein")
  111. Spacer()
  112. TextFieldWithToolBar(
  113. quantity: $viewModel.maxProtein,
  114. maxLength: 4,
  115. unit: HKUnit.gram(),
  116. allowDecimalSeparator: true,
  117. minValue: HKQuantity(unit: .gram(), doubleValue: 0),
  118. maxValue: HKQuantity(unit: .gram(), doubleValue: 100),
  119. onValidationError: { message in
  120. handleValidationError(message)
  121. }
  122. )
  123. .frame(width: 100)
  124. Text("g")
  125. .foregroundColor(.secondary)
  126. }
  127. HStack {
  128. Text("Max Fat")
  129. Spacer()
  130. TextFieldWithToolBar(
  131. quantity: $viewModel.maxFat,
  132. maxLength: 4,
  133. unit: HKUnit.gram(),
  134. allowDecimalSeparator: true,
  135. minValue: HKQuantity(unit: .gram(), doubleValue: 0),
  136. maxValue: HKQuantity(unit: .gram(), doubleValue: 100),
  137. onValidationError: { message in
  138. handleValidationError(message)
  139. }
  140. )
  141. .frame(width: 100)
  142. Text("g")
  143. .foregroundColor(.secondary)
  144. }
  145. }
  146. // MARK: - Meal Section
  147. Section(header: Text("Meal Settings")) {
  148. Toggle("Meal with Bolus", isOn: $viewModel.mealWithBolus)
  149. .toggleStyle(SwitchToggleStyle())
  150. Toggle("Meal with Fat/Protein", isOn: $viewModel.mealWithFatProtein)
  151. .toggleStyle(SwitchToggleStyle())
  152. }
  153. // MARK: - Debug / Info
  154. Section(header: Text("Debug / Info")) {
  155. Text("Device Token: \(Storage.shared.deviceToken.value)")
  156. Text("Production Env.: \(Storage.shared.productionEnvironment.value ? "True" : "False")")
  157. Text("Team ID: \(Storage.shared.teamId.value ?? "")")
  158. Text("Bundle ID: \(Storage.shared.bundleId.value)")
  159. }
  160. }
  161. }
  162. .alert(isPresented: $showAlert) {
  163. switch alertType {
  164. case .validation:
  165. return Alert(
  166. title: Text("Validation Error"),
  167. message: Text(alertMessage ?? "Invalid input."),
  168. dismissButton: .default(Text("OK"))
  169. )
  170. case .none:
  171. return Alert(title: Text("Unknown Alert"))
  172. }
  173. }
  174. }
  175. .preferredColorScheme(Storage.shared.forceDarkMode.value ? .dark : nil)
  176. .navigationBarTitle("Remote Settings", displayMode: .inline)
  177. }
  178. // MARK: - Custom Row for Remote Type Selection
  179. private func remoteTypeRow(type: RemoteType, label: String, isEnabled: Bool) -> some View {
  180. Button(action: {
  181. if isEnabled {
  182. viewModel.remoteType = type
  183. }
  184. }) {
  185. HStack {
  186. Text(label)
  187. Spacer()
  188. if viewModel.remoteType == type {
  189. Image(systemName: "checkmark")
  190. .foregroundColor(.accentColor)
  191. }
  192. }
  193. }
  194. // If isEnabled is false, user can see the row but not tap it.
  195. .disabled(!isEnabled)
  196. .foregroundColor(isEnabled ? .primary : .gray)
  197. }
  198. // MARK: - Validation Error Handler
  199. private func handleValidationError(_ message: String) {
  200. alertMessage = message
  201. alertType = .validation
  202. showAlert = true
  203. }
  204. }