SettingsRootView.swift 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326
  1. import HealthKit
  2. import LoopKit
  3. import LoopKitUI
  4. import SwiftUI
  5. import Swinject
  6. extension Settings {
  7. struct RootView: BaseView {
  8. let resolver: Resolver
  9. @StateObject var state = StateModel()
  10. @State private var showShareSheet = false
  11. @State private var searchText: String = ""
  12. @State private var shouldDisplayHint: Bool = false
  13. @State var hintDetent = PresentationDetent.large
  14. @State var selectedVerboseHint: AnyView?
  15. @State var hintLabel: String?
  16. @State private var decimalPlaceholder: Decimal = 0.0
  17. @State private var booleanPlaceholder: Bool = false
  18. @Environment(\.colorScheme) var colorScheme
  19. @EnvironmentObject var appIcons: Icons
  20. @Environment(AppState.self) var appState
  21. private var filteredItems: [FilteredSettingItem] {
  22. SettingItems.filteredItems(searchText: searchText)
  23. }
  24. var body: some View {
  25. List {
  26. if searchText.isEmpty {
  27. let buildDetails = BuildDetails.default
  28. Section(
  29. header: Text("BRANCH: \(buildDetails.branchAndSha)").textCase(nil),
  30. content: {
  31. let versionNumber = Bundle.main.releaseVersionNumber ?? "Unknown"
  32. let buildNumber = Bundle.main.buildVersionNumber ?? "Unknown"
  33. Group {
  34. HStack {
  35. Image(appIcons.appIcon.rawValue)
  36. .resizable()
  37. .aspectRatio(contentMode: .fit)
  38. .frame(width: 50, height: 50)
  39. .cornerRadius(10)
  40. .padding(.trailing, 10)
  41. VStack(alignment: .leading) {
  42. Text("Trio v\(versionNumber) (\(buildNumber))")
  43. .font(.headline)
  44. if let expirationDate = buildDetails.calculateExpirationDate() {
  45. let formattedDate = DateFormatter.localizedString(
  46. from: expirationDate,
  47. dateStyle: .medium,
  48. timeStyle: .none
  49. )
  50. Text("\(buildDetails.expirationHeaderString): \(formattedDate)")
  51. .font(.footnote)
  52. .foregroundColor(.secondary)
  53. } else {
  54. Text("Simulator Build has no expiry")
  55. .font(.footnote)
  56. .foregroundColor(.secondary)
  57. }
  58. }
  59. }
  60. }
  61. }
  62. ).listRowBackground(Color.chart)
  63. SettingInputSection(
  64. decimalValue: $decimalPlaceholder,
  65. booleanValue: $state.closedLoop,
  66. shouldDisplayHint: $shouldDisplayHint,
  67. selectedVerboseHint: Binding(
  68. get: { selectedVerboseHint },
  69. set: {
  70. selectedVerboseHint = $0.map { AnyView($0) }
  71. hintLabel = "Closed Loop"
  72. }
  73. ),
  74. units: state.units,
  75. type: .boolean,
  76. label: "Closed Loop",
  77. miniHint: "Enable automated insulin delivery.",
  78. verboseHint: VStack(alignment: .leading, spacing: 10) {
  79. Text(
  80. "Running Trio in closed loop mode requires an active CGM sensor session and a connected pump. This enables automated insulin delivery."
  81. )
  82. Text(
  83. "Before enabling, dial in your settings (basal / insulin sensitivity / carb ratio), and familiarize yourself with the app."
  84. )
  85. },
  86. headerText: "Automated Insulin Delivery"
  87. )
  88. Section(
  89. header: Text("Trio Configuration"),
  90. content: {
  91. ForEach(SettingItems.trioConfig) { item in
  92. Text(item.title).navigationLink(to: item.view, from: self)
  93. }
  94. }
  95. )
  96. .listRowBackground(Color.chart)
  97. Section(
  98. header: Text("Support & Community"),
  99. content: {
  100. Button {
  101. showShareSheet.toggle()
  102. } label: {
  103. HStack {
  104. Text("Share Logs")
  105. .foregroundColor(.primary)
  106. Spacer()
  107. Image(systemName: "chevron.right")
  108. .foregroundColor(.secondary)
  109. .font(.footnote)
  110. }
  111. }
  112. .frame(maxWidth: .infinity, alignment: .leading)
  113. Button {
  114. if let url = URL(string: "https://github.com/nightscout/Trio/issues/new/choose") {
  115. UIApplication.shared.open(url)
  116. }
  117. } label: {
  118. HStack {
  119. Text("Submit Ticket on GitHub")
  120. .foregroundColor(.primary)
  121. Spacer()
  122. Image(systemName: "chevron.right")
  123. .foregroundColor(.secondary)
  124. .font(.footnote)
  125. }
  126. }
  127. .frame(maxWidth: .infinity, alignment: .leading)
  128. Button {
  129. if let url = URL(string: "https://discord.gg/FnwFEFUwXE") {
  130. UIApplication.shared.open(url)
  131. }
  132. } label: {
  133. HStack {
  134. Text("Trio Discord")
  135. .foregroundColor(.primary)
  136. Spacer()
  137. Image(systemName: "chevron.right")
  138. .foregroundColor(.secondary)
  139. .font(.footnote)
  140. }
  141. }
  142. .frame(maxWidth: .infinity, alignment: .leading)
  143. Button {
  144. if let url = URL(string: "https://m.facebook.com/groups/1351938092206709/") {
  145. UIApplication.shared.open(url)
  146. }
  147. } label: {
  148. HStack {
  149. Text("Trio Facebook")
  150. .foregroundColor(.primary)
  151. Spacer()
  152. Image(systemName: "chevron.right")
  153. .foregroundColor(.secondary)
  154. .font(.footnote)
  155. }
  156. }
  157. .frame(maxWidth: .infinity, alignment: .leading)
  158. Button {
  159. if let url = URL(string: "https://diy-trio.org/") {
  160. UIApplication.shared.open(url)
  161. }
  162. } label: {
  163. HStack {
  164. Text("Trio Website")
  165. .foregroundColor(.primary)
  166. Spacer()
  167. Image(systemName: "chevron.right")
  168. .foregroundColor(.secondary)
  169. .font(.footnote)
  170. }
  171. }
  172. .frame(maxWidth: .infinity, alignment: .leading)
  173. }
  174. ).listRowBackground(Color.chart)
  175. } else {
  176. Section(
  177. header: Text("Search Results"),
  178. content: {
  179. ForEach(filteredItems) { filteredItem in
  180. VStack(alignment: .leading) {
  181. Text(filteredItem.matchedContent).bold()
  182. if let path = filteredItem.settingItem.path {
  183. Text(path.map(\.stringValue).joined(separator: " > "))
  184. .font(.caption)
  185. .foregroundColor(.secondary)
  186. }
  187. }.navigationLink(to: filteredItem.settingItem.view, from: self)
  188. }
  189. }
  190. ).listRowBackground(Color.chart)
  191. }
  192. // Section {
  193. // Text("Targets")
  194. // .navigationLink(to: .configEditor(file: OpenAPS.Settings.bgTargets), from: self)
  195. // Text("Sensitivities")
  196. // .navigationLink(to: .configEditor(file: OpenAPS.Settings.insulinSensitivities), from: self)
  197. // Text("Profile")
  198. // .navigationLink(to: .configEditor(file: OpenAPS.Settings.profile), from: self)
  199. // Text("Preferences")
  200. // .navigationLink(
  201. // to: .configEditor(file: OpenAPS.Settings.preferences),
  202. // from: self
  203. // )
  204. // }.listRowBackground(Color.chart)
  205. // TODO: remove this more or less entirely; add build-time flag to enable Middleware; add settings export feature
  206. // Section {
  207. // Toggle("Developer Options", isOn: $state.debugOptions)
  208. // if state.debugOptions {
  209. // Group {
  210. // HStack {
  211. // Text("NS Upload Profile and Settings")
  212. // Button("Upload") { state.uploadProfileAndSettings(true) }
  213. // .frame(maxWidth: .infinity, alignment: .trailing)
  214. // .buttonStyle(.borderedProminent)
  215. // }
  216. // // Commenting this out for now, as not needed and possibly dangerous for users to be able to nuke their pump pairing informations via the debug menu
  217. // // Leaving it in here, as it may be a handy functionality for further testing or developers.
  218. // // See https://github.com/nightscout/Trio/pull/277 for more information
  219. // //
  220. // // HStack {
  221. // // Text("Delete Stored Pump State Binary Files")
  222. // // Button("Delete") { state.resetLoopDocuments() }
  223. // // .frame(maxWidth: .infinity, alignment: .trailing)
  224. // // .buttonStyle(.borderedProminent)
  225. // // }
  226. // }
  227. // Group {
  228. // Text("Preferences")
  229. // .navigationLink(to: .configEditor(file: OpenAPS.Settings.preferences), from: self)
  230. // Text("Pump Settings")
  231. // .navigationLink(to: .configEditor(file: OpenAPS.Settings.settings), from: self)
  232. // Text("Autosense")
  233. // .navigationLink(to: .configEditor(file: OpenAPS.Settings.autosense), from: self)
  234. // // Text("Pump History")
  235. // // .navigationLink(to: .configEditor(file: OpenAPS.Monitor.pumpHistory), from: self)
  236. // Text("Basal profile")
  237. // .navigationLink(to: .configEditor(file: OpenAPS.Settings.basalProfile), from: self)
  238. // Text("Targets ranges")
  239. // .navigationLink(to: .configEditor(file: OpenAPS.Settings.bgTargets), from: self)
  240. // Text("Temp targets")
  241. // .navigationLink(to: .configEditor(file: OpenAPS.Settings.tempTargets), from: self)
  242. // }
  243. //
  244. // Group {
  245. // Text("Pump profile")
  246. // .navigationLink(to: .configEditor(file: OpenAPS.Settings.pumpProfile), from: self)
  247. // Text("Profile")
  248. // .navigationLink(to: .configEditor(file: OpenAPS.Settings.profile), from: self)
  249. // // Text("Carbs")
  250. // // .navigationLink(to: .configEditor(file: OpenAPS.Monitor.carbHistory), from: self)
  251. // Text("Autotune")
  252. // .navigationLink(to: .configEditor(file: OpenAPS.Settings.autotune), from: self)
  253. // }
  254. //
  255. // Group {
  256. // Text("Target presets")
  257. // .navigationLink(to: .configEditor(file: OpenAPS.FreeAPS.tempTargetsPresets), from: self)
  258. // Text("Calibrations")
  259. // .navigationLink(to: .configEditor(file: OpenAPS.FreeAPS.calibrations), from: self)
  260. // Text("Middleware")
  261. // .navigationLink(to: .configEditor(file: OpenAPS.Middleware.determineBasal), from: self)
  262. // // Text("Statistics")
  263. // // .navigationLink(to: .configEditor(file: OpenAPS.Monitor.statistics), from: self)
  264. // Text("Edit settings json")
  265. // .navigationLink(to: .configEditor(file: OpenAPS.FreeAPS.settings), from: self)
  266. // }
  267. // }
  268. // }.listRowBackground(Color.chart)
  269. }
  270. .scrollContentBackground(.hidden).background(appState.trioBackgroundColor(for: colorScheme))
  271. .sheet(isPresented: $shouldDisplayHint) {
  272. SettingInputHintView(
  273. hintDetent: $hintDetent,
  274. shouldDisplayHint: $shouldDisplayHint,
  275. hintLabel: hintLabel ?? "",
  276. hintText: selectedVerboseHint ?? AnyView(EmptyView()),
  277. sheetTitle: "Help"
  278. )
  279. }
  280. .sheet(isPresented: $showShareSheet) {
  281. ShareSheet(activityItems: state.logItems())
  282. }
  283. .onAppear(perform: configureView)
  284. .navigationTitle("Settings")
  285. .navigationBarTitleDisplayMode(.automatic)
  286. .toolbar {
  287. ToolbarItem(placement: .topBarTrailing) {
  288. Button(
  289. action: {
  290. if let url = URL(string: "https://triodocs.org/") {
  291. UIApplication.shared.open(url)
  292. }
  293. },
  294. label: {
  295. HStack {
  296. Text("Trio Docs")
  297. Image(systemName: "questionmark.circle")
  298. }
  299. }
  300. )
  301. }
  302. }
  303. .searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always))
  304. .screenNavigation(self)
  305. }
  306. }
  307. }