AddOverrideForm.swift 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437
  1. import Foundation
  2. import SwiftUI
  3. struct AddOverrideForm: View {
  4. @Environment(\.presentationMode) var presentationMode
  5. @StateObject var state: OverrideConfig.StateModel
  6. @State private var isEditing = false
  7. @State private var displayPickerStart: Bool = false
  8. @State private var displayPickerEnd: Bool = false
  9. @State private var displayPickerSmbMinutes: Bool = false
  10. @State private var displayPickerUamMinutes: Bool = false
  11. @State private var overrideTarget = false
  12. @Environment(\.colorScheme) var colorScheme
  13. @State private var showAlert = false
  14. @State private var alertString = ""
  15. @Environment(\.dismiss) var dismiss
  16. var color: LinearGradient {
  17. colorScheme == .dark ? LinearGradient(
  18. gradient: Gradient(colors: [
  19. Color.bgDarkBlue,
  20. Color.bgDarkerDarkBlue
  21. ]),
  22. startPoint: .top,
  23. endPoint: .bottom
  24. )
  25. :
  26. LinearGradient(
  27. gradient: Gradient(colors: [Color.gray.opacity(0.1)]),
  28. startPoint: .top,
  29. endPoint: .bottom
  30. )
  31. }
  32. private var formatter: NumberFormatter {
  33. let formatter = NumberFormatter()
  34. formatter.numberStyle = .decimal
  35. formatter.maximumFractionDigits = 0
  36. return formatter
  37. }
  38. private var glucoseFormatter: NumberFormatter {
  39. let formatter = NumberFormatter()
  40. formatter.numberStyle = .decimal
  41. formatter.maximumFractionDigits = 0
  42. if state.units == .mmolL {
  43. formatter.maximumFractionDigits = 1
  44. }
  45. formatter.roundingMode = .halfUp
  46. return formatter
  47. }
  48. private var alertMessage: String {
  49. let target: String = state.units == .mgdL ? "70-270 mg/dl" : "4-15 mmol/l"
  50. return "Please enter a valid target between" + " \(target)."
  51. }
  52. var body: some View {
  53. NavigationView {
  54. Form {
  55. addOverride()
  56. }.scrollContentBackground(.hidden).background(color)
  57. .navigationTitle("Add Override")
  58. .navigationBarItems(trailing: Button("Cancel") {
  59. presentationMode.wrappedValue.dismiss()
  60. })
  61. }
  62. }
  63. @ViewBuilder private func addOverride() -> some View {
  64. Section {
  65. VStack {
  66. TextField("Name", text: $state.overrideName)
  67. }
  68. } header: {
  69. Text("Name")
  70. }.listRowBackground(Color.chart)
  71. Section {
  72. VStack {
  73. Spacer()
  74. Text("\(state.overrideSliderPercentage.formatted(.number)) %")
  75. .foregroundColor(
  76. state
  77. .overrideSliderPercentage >= 130 ? .red :
  78. (isEditing ? .orange : Color.tabBar)
  79. )
  80. .font(.largeTitle)
  81. Slider(
  82. value: $state.overrideSliderPercentage,
  83. in: 10 ... 200,
  84. step: 1,
  85. onEditingChanged: { editing in
  86. isEditing = editing
  87. }
  88. )
  89. Spacer()
  90. Toggle(isOn: $state.indefinite) {
  91. Text("Enable Indefinitely")
  92. }
  93. }
  94. if !state.indefinite {
  95. HStack {
  96. Text("Duration")
  97. TextFieldWithToolBar(text: $state.overrideDuration, placeholder: "0", numberFormatter: formatter)
  98. Text("minutes").foregroundColor(.secondary)
  99. }
  100. }
  101. HStack {
  102. Toggle(isOn: $state.shouldOverrideTarget) {
  103. Text("Override Profile Target")
  104. }
  105. }
  106. if state.shouldOverrideTarget {
  107. HStack {
  108. Text("Target Glucose")
  109. TextFieldWithToolBar(text: $state.target, placeholder: "0", numberFormatter: glucoseFormatter)
  110. Text(state.units.rawValue).foregroundColor(.secondary)
  111. }
  112. }
  113. HStack {
  114. Toggle(isOn: $state.advancedSettings) {
  115. Text("More Options")
  116. }
  117. }
  118. if state.advancedSettings {
  119. HStack {
  120. Toggle(isOn: Binding(
  121. get: { state.smbIsOff },
  122. set: { newValue in
  123. state.smbIsOff = newValue
  124. if newValue {
  125. state.smbIsScheduledOff = false
  126. }
  127. }
  128. )) {
  129. Text("Disable SMBs")
  130. }
  131. }
  132. HStack {
  133. Toggle(isOn: Binding(
  134. get: { state.smbIsScheduledOff },
  135. set: { newValue in
  136. state.smbIsScheduledOff = newValue
  137. if newValue {
  138. state.smbIsOff = false
  139. }
  140. }
  141. )) {
  142. Text("Schedule When SMBs Are Disabled")
  143. }
  144. }
  145. if state.smbIsScheduledOff {
  146. // First Hour SMBs Are Disabled
  147. VStack {
  148. HStack {
  149. Text("First Hour SMBs Are Disabled")
  150. Spacer()
  151. // Display current selection based on format
  152. Text(
  153. is24HourFormat() ? format24Hour(Int(truncating: state.start as NSNumber)) + ":00" :
  154. convertTo12HourFormat(Int(truncating: state.start as NSNumber))
  155. )
  156. .foregroundColor(!displayPickerStart ? .primary : .accentColor)
  157. }
  158. .onTapGesture {
  159. displayPickerStart.toggle() // Toggle the picker visibility
  160. }
  161. // Show picker if toggled
  162. if displayPickerStart {
  163. Picker(selection: Binding(
  164. get: { Int(truncating: state.start as NSNumber) },
  165. set: { state.start = Decimal($0) }
  166. ), label: Text("")) {
  167. ForEach(0 ..< 24, id: \.self) { hour in
  168. Text(is24HourFormat() ? format24Hour(hour) + ":00" : convertTo12HourFormat(hour)).tag(hour)
  169. }
  170. }
  171. .pickerStyle(WheelPickerStyle()) // Use wheel style
  172. .frame(maxWidth: .infinity)
  173. }
  174. }
  175. .padding(.top)
  176. // First Hour SMBs Are Resumed
  177. VStack {
  178. HStack {
  179. Text("First Hour SMBs Are Resumed")
  180. Spacer()
  181. // Display current selection based on format
  182. Text(
  183. is24HourFormat() ? format24Hour(Int(truncating: state.end as NSNumber)) + ":00" :
  184. convertTo12HourFormat(Int(truncating: state.end as NSNumber))
  185. )
  186. .foregroundColor(!displayPickerEnd ? .primary : .accentColor)
  187. }
  188. .onTapGesture {
  189. displayPickerEnd.toggle() // Toggle the picker visibility
  190. }
  191. // Show picker if toggled
  192. if displayPickerEnd {
  193. Picker(selection: Binding(
  194. get: { Int(truncating: state.end as NSNumber) },
  195. set: { state.end = Decimal($0) }
  196. ), label: Text("")) {
  197. ForEach(0 ..< 24, id: \.self) { hour in
  198. Text(is24HourFormat() ? format24Hour(hour) + ":00" : convertTo12HourFormat(hour)).tag(hour)
  199. }
  200. }
  201. .pickerStyle(WheelPickerStyle()) // Use wheel style
  202. .frame(maxWidth: .infinity)
  203. }
  204. }
  205. .padding(.top)
  206. }
  207. HStack {
  208. Toggle(isOn: $state.isfAndCr) {
  209. Text("Change ISF and CR")
  210. }
  211. }
  212. if !state.isfAndCr {
  213. HStack {
  214. Toggle(isOn: $state.isf) {
  215. Text("Change ISF")
  216. }
  217. }
  218. HStack {
  219. Toggle(isOn: $state.cr) {
  220. Text("Change CR")
  221. }
  222. }
  223. }
  224. if !state.smbIsOff {
  225. // SMB Minutes Picker
  226. VStack {
  227. HStack {
  228. Text("Max SMB Minutes")
  229. Spacer()
  230. // Display current selection based on format
  231. Text("\(state.smbMinutes.formatted(.number)) min")
  232. .foregroundColor(!displayPickerSmbMinutes ? .primary : .accentColor)
  233. }
  234. .onTapGesture {
  235. displayPickerSmbMinutes.toggle() // Toggle the picker visibility
  236. }
  237. // Show picker if toggled
  238. if displayPickerSmbMinutes {
  239. Picker(selection: Binding(
  240. get: { Int(truncating: state.smbMinutes as NSNumber) },
  241. set: { state.smbMinutes = Decimal($0) }
  242. ), label: Text("")) {
  243. ForEach(Array(stride(from: 0, through: 180, by: 5)), id: \.self) { minute in
  244. Text("\(minute) min").tag(minute)
  245. }
  246. }
  247. .pickerStyle(WheelPickerStyle()) // Use wheel style
  248. .frame(maxWidth: .infinity)
  249. }
  250. }
  251. .padding(.top)
  252. // UAM SMB Minutes Picker
  253. VStack {
  254. HStack {
  255. Text("Max UAM SMB Minutes")
  256. Spacer()
  257. Text("\(state.uamMinutes.formatted(.number)) min")
  258. .foregroundColor(!displayPickerUamMinutes ? .primary : .accentColor)
  259. }
  260. .onTapGesture {
  261. displayPickerUamMinutes.toggle() // Toggle picker visibility
  262. }
  263. // Show picker if toggled
  264. if displayPickerUamMinutes {
  265. Picker(selection: Binding(
  266. get: { Int(truncating: state.uamMinutes as NSNumber) },
  267. set: { state.uamMinutes = Decimal($0) }
  268. ), label: Text("")) {
  269. ForEach(Array(stride(from: 0, through: 180, by: 5)), id: \.self) { minute in
  270. Text("\(minute) min").tag(minute)
  271. }
  272. }
  273. .pickerStyle(WheelPickerStyle()) // Use wheel style
  274. .frame(maxWidth: .infinity)
  275. }
  276. }
  277. .padding(.top)
  278. }
  279. }
  280. startAndSaveProfiles
  281. }
  282. header: { Text("Add custom Override") }
  283. footer: {
  284. Text(
  285. "Your profile ISF and CR will be inversly adjusted with the override percentage."
  286. )
  287. }.listRowBackground(Color.chart)
  288. }
  289. private var startAndSaveProfiles: some View {
  290. HStack {
  291. Button("Start New Override") {
  292. if !state.isInputInvalid(target: state.target) {
  293. showAlert.toggle()
  294. alertString = "\(state.overrideSliderPercentage.formatted(.number)) %, " +
  295. (
  296. state.overrideDuration > 0 || !state
  297. .indefinite ?
  298. (
  299. state
  300. .overrideDuration
  301. .formatted(.number.grouping(.never).rounded().precision(.fractionLength(0))) +
  302. " min."
  303. ) :
  304. NSLocalizedString(" infinite duration.", comment: "")
  305. ) +
  306. (
  307. (state.target == 0 || !state.shouldOverrideTarget) ? "" :
  308. (" Target: " + state.target.formatted() + " " + state.units.rawValue + ".")
  309. )
  310. +
  311. (
  312. state
  313. .smbIsOff ?
  314. NSLocalizedString(
  315. " SMBs are disabled either by schedule or during the entire duration.",
  316. comment: ""
  317. ) : ""
  318. )
  319. +
  320. "\n\n"
  321. +
  322. NSLocalizedString(
  323. "Starting this override will change your profiles and/or your Target Glucose used for looping during the entire selected duration. Tapping ”Start Override” will start your new Override or edit your current active Override.",
  324. comment: ""
  325. )
  326. }
  327. }
  328. .disabled(unChanged())
  329. .buttonStyle(BorderlessButtonStyle())
  330. .font(.callout)
  331. .controlSize(.mini)
  332. .alert(
  333. "Start Override",
  334. isPresented: $showAlert,
  335. actions: {
  336. Button("Cancel", role: .cancel) { state.isEnabled = false }
  337. Button("Start Override", role: .destructive) {
  338. Task {
  339. if state.indefinite { state.overrideDuration = 0 }
  340. state.isEnabled.toggle()
  341. await state.saveCustomOverride()
  342. await state.resetStateVariables()
  343. dismiss()
  344. }
  345. }
  346. },
  347. message: {
  348. Text(alertString)
  349. }
  350. )
  351. .alert(isPresented: $state.showInvalidTargetAlert) {
  352. Alert(
  353. title: Text("Invalid Input"),
  354. message: Text("\(state.alertMessage)"),
  355. dismissButton: .default(Text("OK")) { state.showInvalidTargetAlert = false }
  356. )
  357. }
  358. Button {
  359. Task {
  360. if !state.isInputInvalid(target: state.target) {
  361. await state.saveOverridePreset()
  362. dismiss()
  363. }
  364. }
  365. }
  366. label: { Text("Save as Preset") }
  367. .tint(.orange)
  368. .frame(maxWidth: .infinity, alignment: .trailing)
  369. .buttonStyle(BorderlessButtonStyle())
  370. .controlSize(.mini)
  371. .disabled(unChanged())
  372. }
  373. }
  374. private func unChanged() -> Bool {
  375. let defaultProfile = state.overrideSliderPercentage == 100 && !state.shouldOverrideTarget && !state.advancedSettings
  376. let noDurationSpecified = !state.indefinite && state.overrideDuration == 0
  377. let targetZeroWithOverride = state.shouldOverrideTarget && state.target == 0
  378. let allSettingsDefault = state.overrideSliderPercentage == 100 && !state.shouldOverrideTarget && !state.smbIsOff && !state
  379. .smbIsScheduledOff && state.smbMinutes == state.defaultSmbMinutes && state.uamMinutes == state.defaultUamMinutes
  380. return defaultProfile || noDurationSpecified || targetZeroWithOverride || allSettingsDefault
  381. }
  382. }
  383. // Function to check if the phone is using 24-hour format
  384. func is24HourFormat() -> Bool {
  385. let formatter = DateFormatter()
  386. formatter.locale = Locale.current
  387. formatter.dateStyle = .none
  388. formatter.timeStyle = .short
  389. let dateString = formatter.string(from: Date())
  390. return !dateString.contains("AM") && !dateString.contains("PM")
  391. }
  392. // Helper function to convert hours to AM/PM format
  393. func convertTo12HourFormat(_ hour: Int) -> String {
  394. let formatter = DateFormatter()
  395. formatter.dateFormat = "h a"
  396. // Create a date from the hour and format it to AM/PM
  397. let calendar = Calendar.current
  398. let components = DateComponents(hour: hour)
  399. let date = calendar.date(from: components) ?? Date()
  400. return formatter.string(from: date)
  401. }
  402. // Helper function to format 24-hour numbers as two digits
  403. func format24Hour(_ hour: Int) -> String {
  404. String(format: "%02d", hour)
  405. }