SettingsRootView.swift 20 KB

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