RemoteSettingsView.swift 10.0 KB

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