UserInterfaceSettingsRootView.swift 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488
  1. import SwiftUI
  2. import Swinject
  3. extension UserInterfaceSettings {
  4. struct RootView: BaseView {
  5. let resolver: Resolver
  6. @StateObject var state = StateModel()
  7. @State private var shouldDisplayHint: Bool = false
  8. @State var hintDetent = PresentationDetent.large
  9. @State var selectedVerboseHint: AnyView?
  10. @State var hintLabel: String?
  11. @State private var decimalPlaceholder: Decimal = 0.0
  12. @State private var booleanPlaceholder: Bool = false
  13. @State private var displayPickerLowThreshold: Bool = false
  14. @State private var displayPickerHighThreshold: Bool = false
  15. @AppStorage("colorSchemePreference") private var colorSchemePreference: ColorSchemeOption = .systemDefault
  16. @Environment(\.colorScheme) var colorScheme
  17. var color: LinearGradient {
  18. colorScheme == .dark ? LinearGradient(
  19. gradient: Gradient(colors: [
  20. Color.bgDarkBlue,
  21. Color.bgDarkerDarkBlue
  22. ]),
  23. startPoint: .top,
  24. endPoint: .bottom
  25. )
  26. :
  27. LinearGradient(
  28. gradient: Gradient(colors: [Color.gray.opacity(0.1)]),
  29. startPoint: .top,
  30. endPoint: .bottom
  31. )
  32. }
  33. private var glucoseFormatter: NumberFormatter {
  34. let formatter = NumberFormatter()
  35. formatter.numberStyle = .decimal
  36. formatter.maximumFractionDigits = 0
  37. if state.units == .mmolL {
  38. formatter.maximumFractionDigits = 1
  39. }
  40. formatter.roundingMode = .halfUp
  41. return formatter
  42. }
  43. private var carbsFormatter: NumberFormatter {
  44. let formatter = NumberFormatter()
  45. formatter.numberStyle = .decimal
  46. formatter.maximumFractionDigits = 0
  47. return formatter
  48. }
  49. var body: some View {
  50. Form {
  51. Section(
  52. header: Text("General Appearance"),
  53. content: {
  54. VStack {
  55. Picker(
  56. selection: $colorSchemePreference,
  57. label: Text("Trio Color Scheme")
  58. ) {
  59. ForEach(ColorSchemeOption.allCases) { selection in
  60. Text(selection.displayName).tag(selection)
  61. }
  62. }.padding(.top)
  63. HStack(alignment: .top) {
  64. Text(
  65. "Choose between Light, Dark, or System Default for the app color scheme"
  66. )
  67. .font(.footnote)
  68. .foregroundColor(.secondary)
  69. .lineLimit(nil)
  70. Spacer()
  71. Button(
  72. action: {
  73. hintLabel = "Color Scheme Preference"
  74. selectedVerboseHint =
  75. AnyView(VStack(spacing: 10) {
  76. Text("Set the app color scheme using the following options")
  77. VStack(alignment: .leading, spacing: 5) {
  78. Text("System Default: Follows the phone's current color scheme setting at that time")
  79. Text("Light: Always in Light mode")
  80. Text("Dark: Always in Dark mode")
  81. }
  82. })
  83. shouldDisplayHint.toggle()
  84. },
  85. label: {
  86. HStack {
  87. Image(systemName: "questionmark.circle")
  88. }
  89. }
  90. ).buttonStyle(BorderlessButtonStyle())
  91. }.padding(.top)
  92. }.padding(.bottom)
  93. }
  94. ).listRowBackground(Color.chart)
  95. Section {
  96. VStack {
  97. Picker(
  98. selection: $state.glucoseColorScheme,
  99. label: Text("Glucose Color Scheme")
  100. ) {
  101. ForEach(GlucoseColorScheme.allCases) { selection in
  102. Text(selection.displayName).tag(selection)
  103. }
  104. }.padding(.top)
  105. HStack(alignment: .top) {
  106. Text(
  107. "Choose between Static or Dynamic coloring for glucose readings"
  108. )
  109. .font(.footnote)
  110. .foregroundColor(.secondary)
  111. .lineLimit(nil)
  112. Spacer()
  113. Button(
  114. action: {
  115. hintLabel = "Glucose Color Scheme"
  116. selectedVerboseHint =
  117. AnyView(VStack(spacing: 10) {
  118. Text("Set the color scheme for glucose readings on the main glucose graph, live activities, and bolus calculatorusing the following options:")
  119. Text("Static: Below-Range Target readings will be in RED, In-Range will be GREEN, Above-Range will be YELLOW")
  120. Text("Dynamic: Readings on Target will be GREEN. As readings approach and exceed below target, they become more RED. As readings approach and exceed above targer, they become more PURPLE.")
  121. })
  122. shouldDisplayHint.toggle()
  123. },
  124. label: {
  125. HStack {
  126. Image(systemName: "questionmark.circle")
  127. }
  128. }
  129. ).buttonStyle(BorderlessButtonStyle())
  130. }.padding(.top)
  131. }.padding(.bottom)
  132. }.listRowBackground(Color.chart)
  133. Section(
  134. header: Text("Home View Settings"),
  135. content: {
  136. VStack {
  137. Toggle("Show X-Axis Grid Lines", isOn: $state.xGridLines)
  138. Toggle("Show Y-Axis Grid Lines", isOn: $state.yGridLines)
  139. HStack(alignment: .top) {
  140. Text(
  141. "Display the grid lines behind the glucose graph"
  142. )
  143. .font(.footnote)
  144. .foregroundColor(.secondary)
  145. .lineLimit(nil)
  146. Spacer()
  147. Button(
  148. action: {
  149. hintLabel = "Show Main Chart X- and Y-Axis Grid Lines"
  150. selectedVerboseHint =
  151. AnyView(VStack(spacing: 10) {
  152. Text("Choose whether or not to display one or both X- and Y-Axis grid lines.")
  153. })
  154. shouldDisplayHint.toggle()
  155. },
  156. label: {
  157. HStack {
  158. Image(systemName: "questionmark.circle")
  159. }
  160. }
  161. ).buttonStyle(BorderlessButtonStyle())
  162. }.padding(.top)
  163. }.padding(.vertical)
  164. }
  165. ).listRowBackground(Color.chart)
  166. SettingInputSection(
  167. decimalValue: $decimalPlaceholder,
  168. booleanValue: $state.rulerMarks,
  169. shouldDisplayHint: $shouldDisplayHint,
  170. selectedVerboseHint: Binding(
  171. get: { selectedVerboseHint },
  172. set: {
  173. selectedVerboseHint = $0.map { AnyView($0) }
  174. hintLabel = "Show Low and High Thresholds"
  175. }
  176. ),
  177. units: state.units,
  178. type: .boolean,
  179. label: "Show Low and High Thresholds",
  180. miniHint: "Display the Low and High glucose Thresholds set below",
  181. verboseHint: VStack(alignment: .leading, spacing: 10) {
  182. Text("This setting displays the upper and lower values for your glucose target range.")
  183. Text("This range is for display and statistical purposes only and does not influence insulin dosing.")
  184. }
  185. )
  186. if state.rulerMarks {
  187. Section {
  188. VStack {
  189. VStack {
  190. HStack {
  191. Text("Low Threshold")
  192. Spacer()
  193. Group {
  194. Text(state.units == .mgdL ? state.low.description : state.low.asMmolL.description)
  195. .foregroundColor(!displayPickerLowThreshold ? .primary : .accentColor)
  196. Text(state.units == .mgdL ? " mg/dL" : " mmol/L").foregroundColor(.secondary)
  197. }
  198. }
  199. .onTapGesture {
  200. displayPickerLowThreshold.toggle()
  201. }
  202. }
  203. .padding(.top)
  204. if displayPickerLowThreshold {
  205. let setting = PickerSettingsProvider.shared.settings.low
  206. Picker(selection: $state.low, label: Text("")) {
  207. ForEach(
  208. PickerSettingsProvider.shared.generatePickerValues(from: setting, units: state.units),
  209. id: \.self
  210. ) { value in
  211. let displayValue = state.units == .mgdL ? value : value.asMmolL
  212. Text("\(displayValue.description)").tag(value)
  213. }
  214. }
  215. .pickerStyle(WheelPickerStyle())
  216. .frame(maxWidth: .infinity)
  217. }
  218. VStack {
  219. HStack {
  220. Text("High Threshold")
  221. Spacer()
  222. Group {
  223. Text(state.units == .mgdL ? state.high.description : state.high.asMmolL.description)
  224. .foregroundColor(!displayPickerHighThreshold ? .primary : .accentColor)
  225. Text(state.units == .mgdL ? " mg/dL" : " mmol/L").foregroundColor(.secondary)
  226. }
  227. }
  228. .onTapGesture {
  229. displayPickerHighThreshold.toggle()
  230. }
  231. }
  232. .padding(.top)
  233. if displayPickerHighThreshold {
  234. let setting = PickerSettingsProvider.shared.settings.high
  235. Picker(selection: $state.high, label: Text("")) {
  236. ForEach(
  237. PickerSettingsProvider.shared.generatePickerValues(from: setting, units: state.units),
  238. id: \.self
  239. ) { value in
  240. let displayValue = state.units == .mgdL ? value : value.asMmolL
  241. Text("\(displayValue.description)").tag(value)
  242. }
  243. }
  244. .pickerStyle(WheelPickerStyle())
  245. .frame(maxWidth: .infinity)
  246. }
  247. HStack(alignment: .top) {
  248. Text("Set low and high glucose values for the main screen glucose graph and statistics /nLow Default: 70 /nHigh Default: 180")
  249. .lineLimit(nil)
  250. .font(.footnote)
  251. .foregroundColor(.secondary)
  252. Spacer()
  253. Button(
  254. action: {
  255. hintLabel = "Low and High Thresholds"
  256. selectedVerboseHint =
  257. AnyView(
  258. VStack(alignment: .leading, spacing: 10) {
  259. Text("Default values are based on internationally accepted Time in Range values of 70-180 mg/dL (5.5-10 mmol/L)")
  260. Text("Set the values used in the main screen glucose graph and to determine Time in Range for Statistics.")
  261. Text("Note: These values are not used to calculate insulin dosing.").italic()
  262. })
  263. shouldDisplayHint.toggle()
  264. },
  265. label: {
  266. HStack {
  267. Image(systemName: "questionmark.circle")
  268. }
  269. }
  270. ).buttonStyle(BorderlessButtonStyle())
  271. }.padding(.top)
  272. }.padding(.bottom)
  273. }.listRowBackground(Color.chart)
  274. }
  275. Section {
  276. VStack {
  277. Picker(
  278. selection: $state.forecastDisplayType,
  279. label: Text("Forecast Display Type")
  280. ) {
  281. ForEach(ForecastDisplayType.allCases) { selection in
  282. Text(selection.displayName).tag(selection)
  283. }
  284. }.padding(.top)
  285. HStack(alignment: .top) {
  286. Text("Choose between the OpenAPS colored "Lines" or the "Cone" of Uncertainty for the Forecast Lines \nDefault: Cone"
  287. )
  288. .font(.footnote)
  289. .foregroundColor(.secondary)
  290. .lineLimit(nil)
  291. Spacer()
  292. Button(
  293. action: {
  294. hintLabel = "Forecast Display Type"
  295. selectedVerboseHint =
  296. AnyView(VStack(spacing: 10) {
  297. Text("Default: Cone").bold()
  298. VStack(alignment: .leading, spacing: 10) {
  299. Text("This setting allows you to choose between the following two options for the Forecast lines (previously: Prediction Lines).")
  300. Text("Lines: Uses the IOB, COB, UAM, and ZT forecast lines from OpenAPS")
  301. Text("Cone: Uses a combined range of all possible forecasts from the OpenAPS lines and provides you with a range of possible forecasts. This option has shown to reduce confusion and stress around algorithm forecasts by providing a less concerning visual representation.")
  302. }
  303. })
  304. shouldDisplayHint.toggle()
  305. },
  306. label: {
  307. HStack {
  308. Image(systemName: "questionmark.circle")
  309. }
  310. }
  311. ).buttonStyle(BorderlessButtonStyle())
  312. }.padding(.top)
  313. }.padding(.bottom)
  314. }.listRowBackground(Color.chart)
  315. SettingInputSection(
  316. decimalValue: $state.hours,
  317. booleanValue: $booleanPlaceholder,
  318. shouldDisplayHint: $shouldDisplayHint,
  319. selectedVerboseHint: Binding(
  320. get: { selectedVerboseHint },
  321. set: {
  322. selectedVerboseHint = $0.map { AnyView($0) }
  323. hintLabel = "X-Axis Interval Step"
  324. }
  325. ),
  326. units: state.units,
  327. type: .decimal("hours"),
  328. label: "X-Axis Interval Step",
  329. miniHint: "Determines how many hours are shown in the main graph",
  330. verboseHint: VStack(spacing: 10) {
  331. Text("Default: 6 hours").bold()
  332. Text("This setting determines how many hours are shown in the primary view of the main graph.")
  333. }
  334. )
  335. Section {
  336. VStack {
  337. Picker(
  338. selection: $state.totalInsulinDisplayType,
  339. label: Text("Total Insulin Display Type")
  340. ) {
  341. ForEach(TotalInsulinDisplayType.allCases) { selection in
  342. Text(selection.displayName).tag(selection)
  343. }
  344. }.padding(.top)
  345. HStack(alignment: .top) {
  346. Text(
  347. "Choose between Total Daily Dose (TDD) or Total Insulin in Scope (TINS) to be displayed above the main glucose graph"
  348. )
  349. .font(.footnote)
  350. .foregroundColor(.secondary)
  351. .lineLimit(nil)
  352. Spacer()
  353. Button(
  354. action: {
  355. hintLabel = "Total Insulin Display Type"
  356. selectedVerboseHint =
  357. AnyView(VStack(alignment: .leading, spacing: 10) {
  358. Text("Choose between Total Daily Dose (TDD) or Total Insulin in Scope (TINS) to be displayed above the main glucose graph.")
  359. Text("Total Daily Dose: Displays the last 24 hours of total insulin administered, both basal and bolus.")
  360. Text("Total Insulin in Scope: Displays the total insulin administered since midnight, both basal and bolus.")
  361. })
  362. shouldDisplayHint.toggle()
  363. },
  364. label: {
  365. HStack {
  366. Image(systemName: "questionmark.circle")
  367. }
  368. }
  369. ).buttonStyle(BorderlessButtonStyle())
  370. }.padding(.top)
  371. }.padding(.bottom)
  372. }.listRowBackground(Color.chart)
  373. // TODO: this needs to be a picker: mmol/L or %
  374. SettingInputSection(
  375. decimalValue: $decimalPlaceholder,
  376. booleanValue: $state.overrideHbA1cUnit,
  377. shouldDisplayHint: $shouldDisplayHint,
  378. selectedVerboseHint: Binding(
  379. get: { selectedVerboseHint },
  380. set: {
  381. selectedVerboseHint = $0.map { AnyView($0) }
  382. hintLabel = "Override HbA1c Unit"
  383. }
  384. ),
  385. units: state.units,
  386. type: .boolean,
  387. label: "Override HbA1c Unit",
  388. miniHint: "Display HbA1c in mmol/mol or %",
  389. verboseHint: VStack(spacing: 10) {
  390. Text(
  391. "Choose which format you'd prefer the HbA1c value in the statistics view as a percentage (Example: 6.5%) or mmol/mol (Example: 48 mmol/mol)"
  392. )
  393. },
  394. headerText: "Trio Statistics"
  395. )
  396. // TODO: this needs to be a picker: choose bar chart or progress bar
  397. SettingInputSection(
  398. decimalValue: $decimalPlaceholder,
  399. booleanValue: $state.oneDimensionalGraph,
  400. shouldDisplayHint: $shouldDisplayHint,
  401. selectedVerboseHint: Binding(
  402. get: { selectedVerboseHint },
  403. set: {
  404. selectedVerboseHint = $0.map { AnyView($0) }
  405. hintLabel = "Standing / Laying TIR Chart"
  406. }
  407. ),
  408. units: state.units,
  409. type: .boolean,
  410. label: "Standing / Laying TIR Chart",
  411. miniHint: "Lorem ipsum dolor sit amet, consetetur sadipscing elitr.",
  412. verboseHint: VStack(spacing: 10) {
  413. Text("Standing / Laying TIR Chart… bla bla bla")
  414. }
  415. )
  416. SettingInputSection(
  417. decimalValue: $state.carbsRequiredThreshold,
  418. booleanValue: $state.showCarbsRequiredBadge,
  419. shouldDisplayHint: $shouldDisplayHint,
  420. selectedVerboseHint: Binding(
  421. get: { selectedVerboseHint },
  422. set: {
  423. selectedVerboseHint = $0.map { AnyView($0) }
  424. hintLabel = "Show Carbs Required Badge"
  425. }
  426. ),
  427. units: state.units,
  428. type: .conditionalDecimal("carbsRequiredThreshold"),
  429. label: "Show Carbs Required Badge",
  430. conditionalLabel: "Carbs Required Threshold",
  431. miniHint: "Lorem ipsum dolor sit amet, consetetur sadipscing elitr.",
  432. verboseHint: VStack(spacing: 10) {
  433. Text("Show Carbs Required Badge… bla bla bla")
  434. },
  435. headerText: "Carbs Required Badge"
  436. )
  437. }
  438. .sheet(isPresented: $shouldDisplayHint) {
  439. SettingInputHintView(
  440. hintDetent: $hintDetent,
  441. shouldDisplayHint: $shouldDisplayHint,
  442. hintLabel: hintLabel ?? "",
  443. hintText: selectedVerboseHint ?? AnyView(EmptyView()),
  444. sheetTitle: "Help"
  445. )
  446. }
  447. .scrollContentBackground(.hidden).background(color)
  448. .onAppear(perform: configureView)
  449. .navigationBarTitle("User Interface")
  450. .navigationBarTitleDisplayMode(.automatic)
  451. }
  452. }
  453. }