RemoteSettingsView.swift 10 KB

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