CGMRootView.swift 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. import LoopKitUI
  2. import SwiftUI
  3. import Swinject
  4. extension CGM {
  5. struct RootView: BaseView {
  6. let resolver: Resolver
  7. let displayClose: Bool
  8. @StateObject var state = StateModel()
  9. @State private var setupCGM = false
  10. @State private var shouldDisplayHint: Bool = false
  11. @State var hintDetent = PresentationDetent.large
  12. @State var selectedVerboseHint: AnyView?
  13. @State var hintLabel: String?
  14. @State private var decimalPlaceholder: Decimal = 0.0
  15. @State private var booleanPlaceholder: Bool = false
  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. var body: some View {
  34. NavigationView {
  35. Form {
  36. Section(
  37. header: Text("CGM Integration to Trio"),
  38. content: {
  39. VStack {
  40. Picker("Type", selection: $state.cgmCurrent) {
  41. ForEach(state.listOfCGM) { type in
  42. VStack(alignment: .leading) {
  43. Text(type.displayName)
  44. Text(type.subtitle).font(.caption).foregroundColor(.secondary)
  45. }.tag(type)
  46. }
  47. }.padding(.top)
  48. HStack(alignment: .top) {
  49. Text(
  50. "Select your CGM"
  51. )
  52. .font(.footnote)
  53. .foregroundColor(.secondary)
  54. .lineLimit(nil)
  55. Spacer()
  56. Button(
  57. action: {
  58. hintLabel = "Available CGM Types for Trio"
  59. selectedVerboseHint =
  60. AnyView(
  61. Text(
  62. "• Dexcom G5 \n• Dexcom G6/ONE \n• Dexcom G7/ONE+ \n• Dexcom Share \n• Freestyle Libre \n• Freestyle Libre Demo \n• Glucose Simulator \n• Medtronic Enlite \n• Nightscout \n• xDrip4iOS"
  63. )
  64. )
  65. shouldDisplayHint.toggle()
  66. },
  67. label: {
  68. HStack {
  69. Image(systemName: "questionmark.circle")
  70. }
  71. }
  72. ).buttonStyle(BorderlessButtonStyle())
  73. }.padding(.top)
  74. }.padding(.bottom)
  75. if let link = state.cgmCurrent.type.externalLink {
  76. Button {
  77. UIApplication.shared.open(link, options: [:], completionHandler: nil)
  78. } label: {
  79. HStack {
  80. Text("About this source")
  81. Spacer()
  82. Image(systemName: "chevron.right")
  83. }
  84. }
  85. .frame(maxWidth: .infinity, alignment: .leading)
  86. }
  87. if state.cgmCurrent.type == .plugin {
  88. Button {
  89. setupCGM.toggle()
  90. } label: {
  91. HStack {
  92. Text("CGM Configuration")
  93. Spacer()
  94. Image(systemName: "chevron.right")
  95. }
  96. }
  97. .frame(maxWidth: .infinity, alignment: .leading)
  98. }
  99. }
  100. ).listRowBackground(Color.chart)
  101. if let appURL = state.urlOfApp() {
  102. Section {
  103. Button {
  104. UIApplication.shared.open(appURL, options: [:]) { success in
  105. if !success {
  106. self.router.alertMessage
  107. .send(MessageContent(content: "Unable to open the app", type: .warning))
  108. }
  109. }
  110. }
  111. label: {
  112. Label(state.displayNameOfApp() ?? "-", systemImage: "waveform.path.ecg.rectangle").font(.title3)
  113. .padding() }
  114. .frame(maxWidth: .infinity, alignment: .center)
  115. .buttonStyle(.bordered)
  116. }
  117. .listRowBackground(Color.clear)
  118. } else if state.cgmCurrent.type == .nightscout {
  119. if let url = state.url {
  120. Section {
  121. Button {
  122. UIApplication.shared.open(url, options: [:]) { success in
  123. if !success {
  124. self.router.alertMessage
  125. .send(MessageContent(content: "No URL available", type: .warning))
  126. }
  127. }
  128. }
  129. label: { Label("Open URL", systemImage: "waveform.path.ecg.rectangle").font(.title3).padding() }
  130. .frame(maxWidth: .infinity, alignment: .center)
  131. .buttonStyle(.bordered)
  132. }
  133. .listRowBackground(Color.clear)
  134. } else {
  135. Section {
  136. Button {
  137. state.showModal(for: .nighscoutConfigDirect)
  138. }
  139. label: {
  140. Label("Config Nightscout", systemImage: "waveform.path.ecg.rectangle").font(.title3).padding()
  141. }
  142. .frame(maxWidth: .infinity, alignment: .center)
  143. .buttonStyle(.bordered)
  144. }
  145. .listRowBackground(Color.clear)
  146. }
  147. }
  148. if state.cgmCurrent.type == .xdrip {
  149. Section(header: Text("Heartbeat")) {
  150. VStack(alignment: .leading) {
  151. if let cgmTransmitterDeviceAddress = state.cgmTransmitterDeviceAddress {
  152. Text("CGM address :").padding(.top)
  153. Text(cgmTransmitterDeviceAddress)
  154. } else {
  155. Text("CGM is not used as heartbeat.").padding(.top)
  156. }
  157. HStack(alignment: .top) {
  158. Text(
  159. "A heartbeat tells Trio to start a loop cycle. \nThis is required to keep looping."
  160. )
  161. .font(.footnote)
  162. .foregroundColor(.secondary)
  163. .lineLimit(nil)
  164. Spacer()
  165. Button(
  166. action: {
  167. hintLabel = "CGM Heartbeat"
  168. selectedVerboseHint =
  169. AnyView(
  170. Text(
  171. "The CGM Heartbeat can come from either a CGM or a pump to wake up Trio when phone is locked or in the background. If CGM is on the same phone as Trio and xDrip4iOS is configured to use the same AppGroup as Trio and the heartbeat feature is turned on in xDrip4iOS, then the CGM can provide a heartbeat to wake up Trio when phone is locked or app is in the background."
  172. )
  173. )
  174. shouldDisplayHint.toggle()
  175. },
  176. label: {
  177. HStack {
  178. Image(systemName: "questionmark.circle")
  179. }
  180. }
  181. ).buttonStyle(BorderlessButtonStyle())
  182. }.padding(.vertical)
  183. }
  184. }.listRowBackground(Color.chart)
  185. }
  186. if state.cgmCurrent.type == .plugin && state.cgmCurrent.id.contains("Libre") {
  187. Section {
  188. Text("Libre Calibrations").navigationLink(to: .calibrations, from: self)
  189. }.listRowBackground(Color.chart)
  190. }
  191. SettingInputSection(
  192. decimalValue: $decimalPlaceholder,
  193. booleanValue: $state.smoothGlucose,
  194. shouldDisplayHint: $shouldDisplayHint,
  195. selectedVerboseHint: Binding(
  196. get: { selectedVerboseHint },
  197. set: {
  198. selectedVerboseHint = $0.map { AnyView($0) }
  199. hintLabel = "Smooth Glucose Value"
  200. }
  201. ),
  202. units: state.units,
  203. type: .boolean,
  204. label: "Smooth Glucose Value",
  205. miniHint: "Smooth CGM readings using Savitzky-Golay filtering",
  206. verboseHint: VStack {
  207. Text("Default: OFF").bold()
  208. VStack(alignment: .leading, spacing: 10) {
  209. Text(
  210. "This filter looks at small groups of nearby readings and fits them to a simple mathematical curve. This process doesn't change the overall pattern of your glucose data but helps smooth out the \"noise\" or irregular fluctuations that could lead to false highs or lows."
  211. )
  212. Text(
  213. "Because your glucose readings are taken at regular intervals, the filter can use a set of pre-calculated \"weights\" to adjust each group of readings, making the calculations fast and efficient. It's designed to keep the important trends in your data while minimizing those small, misleading variations, giving you and Trio a clearer sense of where your blood sugar is really headed."
  214. )
  215. Text(
  216. "This type of filtering is useful in Trio, as it can help prevent over-corrections based on inaccurate glucose readings. This can help reduce the impact of sudden spikes or dips that might not reflect your true blood glucose levels."
  217. )
  218. }
  219. }
  220. )
  221. }
  222. .scrollContentBackground(.hidden).background(color)
  223. .onAppear(perform: configureView)
  224. .navigationTitle("CGM")
  225. .navigationBarTitleDisplayMode(.automatic)
  226. .sheet(isPresented: $shouldDisplayHint) {
  227. SettingInputHintView(
  228. hintDetent: $hintDetent,
  229. shouldDisplayHint: $shouldDisplayHint,
  230. hintLabel: hintLabel ?? "",
  231. hintText: selectedVerboseHint ?? AnyView(EmptyView()),
  232. sheetTitle: "Help"
  233. )
  234. }
  235. .sheet(isPresented: $setupCGM) {
  236. if let cgmFetchManager = state.cgmManager,
  237. let cgmManager = cgmFetchManager.cgmManager,
  238. state.cgmCurrent.type == cgmFetchManager.cgmGlucoseSourceType,
  239. state.cgmCurrent.id == cgmFetchManager.cgmGlucosePluginId
  240. {
  241. CGMSettingsView(
  242. cgmManager: cgmManager,
  243. bluetoothManager: state.provider.apsManager.bluetoothManager!,
  244. unit: state.settingsManager.settings.units,
  245. completionDelegate: state
  246. )
  247. } else {
  248. CGMSetupView(
  249. CGMType: state.cgmCurrent,
  250. bluetoothManager: state.provider.apsManager.bluetoothManager!,
  251. unit: state.settingsManager.settings.units,
  252. completionDelegate: state,
  253. setupDelegate: state,
  254. pluginCGMManager: self.state.pluginCGMManager
  255. )
  256. }
  257. }
  258. .onChange(of: setupCGM) { setupCGM in
  259. state.setupCGM = setupCGM
  260. }
  261. .onChange(of: state.setupCGM) { setupCGM in
  262. self.setupCGM = setupCGM
  263. }
  264. .screenNavigation(self)
  265. }
  266. }
  267. }
  268. }