SettingsRootView.swift 18 KB

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