SettingsRootView.swift 20 KB

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