SettingsView.swift 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402
  1. //
  2. // SettingsOverview.swift
  3. // LibreTransmitterUI
  4. //
  5. // Created by Bjørn Inge Berg on 12/06/2021.
  6. // Copyright © 2021 Mark Wilson. All rights reserved.
  7. //
  8. import SwiftUI
  9. import HealthKit
  10. import UniformTypeIdentifiers
  11. public struct SettingsItem: View {
  12. @State var title: String = "" // we don't want this to change after it is set
  13. @Binding var detail: String
  14. init(title: String, detail: Binding<String>) {
  15. self.title = title
  16. self._detail = detail
  17. }
  18. //basically allows caller to set a static string without having to use .constant
  19. init(title: String, detail: String) {
  20. self.title = title
  21. self._detail = Binding<String>(get: {
  22. detail
  23. }, set: { newVal in
  24. //pass
  25. })
  26. }
  27. public var body: some View {
  28. HStack {
  29. Text(NSLocalizedString(title, comment: "Item title"))
  30. Spacer()
  31. Text(detail).font(.subheadline)
  32. }
  33. }
  34. }
  35. private class FactoryCalibrationInfo : ObservableObject, Equatable, Hashable{
  36. @Published var i1 = ""
  37. @Published var i2 = ""
  38. @Published var i3 = ""
  39. @Published var i4 = ""
  40. @Published var i5 = ""
  41. @Published var i6 = ""
  42. @Published var validForFooter = ""
  43. // For swiftuis stateobject to be able to compare two objects for equality,
  44. // we must exclude the publishers them selves in the comparison
  45. static func ==(lhs: FactoryCalibrationInfo, rhs: FactoryCalibrationInfo) -> Bool {
  46. lhs.i1 == rhs.i1 && lhs.i2 == rhs.i2 &&
  47. lhs.i3 == rhs.i3 && lhs.i4 == rhs.i4 &&
  48. lhs.i5 == rhs.i5 && lhs.i6 == rhs.i6 &&
  49. lhs.validForFooter == rhs.validForFooter
  50. }
  51. //todo: consider using cgmmanagers observable directly
  52. static func loadState() -> FactoryCalibrationInfo{
  53. let newState = FactoryCalibrationInfo()
  54. // User editable calibrationdata: keychain.getLibreNativeCalibrationData()
  55. // Default Calibrationdata stored in sensor: cgmManager?.calibrationData
  56. //do not change this, there is UI support for editing calibrationdata anyway
  57. guard let c = KeychainManagerWrapper.standard.getLibreNativeCalibrationData() else {
  58. return newState
  59. }
  60. newState.i1 = String(c.i1)
  61. newState.i2 = String(c.i2)
  62. newState.i3 = String(c.i3)
  63. newState.i4 = String(c.i4)
  64. newState.i5 = String(c.i5)
  65. newState.i6 = String(c.i6)
  66. newState.validForFooter = String(c.isValidForFooterWithReverseCRCs)
  67. return newState
  68. }
  69. }
  70. class SettingsModel : ObservableObject {
  71. @Published fileprivate var factoryCalibrationInfos = [FactoryCalibrationInfo()]
  72. }
  73. struct SettingsView: View {
  74. //@ObservedObject private var displayGlucoseUnitObservable: DisplayGlucoseUnitObservable
  75. @ObservedObject private var transmitterInfo: LibreTransmitter.TransmitterInfo
  76. @ObservedObject private var sensorInfo: LibreTransmitter.SensorInfo
  77. @ObservedObject private var glucoseMeasurement: LibreTransmitter.GlucoseInfo
  78. @ObservedObject private var notifyComplete: GenericObservableObject
  79. @ObservedObject private var notifyDelete: GenericObservableObject
  80. //most of the settings are now retrieved from the cgmmanager observables instead
  81. @StateObject var model = SettingsModel()
  82. @State private var presentableStatus: StatusMessage?
  83. @State private var showingDestructQuestion = false
  84. @State private var showingExporter = false
  85. //@Environment(\.presentationMode) var presentationMode
  86. static func asHostedViewController(
  87. glucoseUnit: HKUnit,
  88. //displayGlucoseUnitObservable: DisplayGlucoseUnitObservable,
  89. notifyComplete: GenericObservableObject,
  90. notifyDelete: GenericObservableObject,
  91. transmitterInfoObservable:LibreTransmitter.TransmitterInfo,
  92. sensorInfoObervable: LibreTransmitter.SensorInfo,
  93. glucoseInfoObservable: LibreTransmitter.GlucoseInfo) -> UIHostingController<SettingsView> {
  94. UIHostingController(rootView: self.init(
  95. //displayGlucoseUnitObservable: displayGlucoseUnitObservable,
  96. transmitterInfo: transmitterInfoObservable, sensorInfo: sensorInfoObervable, glucoseMeasurement: glucoseInfoObservable, notifyComplete: notifyComplete, notifyDelete: notifyDelete, glucoseUnit: glucoseUnit
  97. ))
  98. }
  99. /*private var glucoseUnit: HKUnit {
  100. displayGlucoseUnitObservable.displayGlucoseUnit
  101. }*/
  102. private var glucoseUnit: HKUnit {
  103. didSet {
  104. UserDefaults.standard.mmGlucoseUnit = glucoseUnit
  105. }
  106. }
  107. static let formatter = NumberFormatter()
  108. // no navigationview necessary when running inside a uihostingcontroller
  109. // uihostingcontroller seems to add a navigationview for us, causing problems if we
  110. // also add one herer
  111. var body: some View {
  112. overview
  113. //.navigationViewStyle(StackNavigationViewStyle())
  114. .navigationBarTitle(Text("Libre Bluetooth"), displayMode: .inline)
  115. .navigationBarItems(trailing: dismissButton)
  116. .onAppear{
  117. print("dabear:: settingsview appeared")
  118. //While loop does this request on our behalf, freeaps does not
  119. NotificationHelper.requestNotificationPermissionsIfNeeded()
  120. //only override savedglucose unit if we haven't saved this locally before
  121. if UserDefaults.standard.mmGlucoseUnit == nil {
  122. UserDefaults.standard.mmGlucoseUnit = glucoseUnit
  123. }
  124. // Yes we load factory calibrationdata every time the view appears
  125. // I know this is bad, but the calibrationdata is stored in
  126. // the keychain and there is no simple way of wrapping the keychain
  127. // as an observable in swiftui without bringing in large third party
  128. // dependencies or hand crafting it, which would be error prone
  129. let newFactoryInfo = FactoryCalibrationInfo.loadState()
  130. if newFactoryInfo != self.model.factoryCalibrationInfos.first{
  131. print("dabear:: factoryinfo was new")
  132. self.model.factoryCalibrationInfos.removeAll()
  133. self.model.factoryCalibrationInfos.append(newFactoryInfo)
  134. }
  135. }
  136. }
  137. var measurementSection : some View {
  138. Section(header: Text("Last measurement")) {
  139. if glucoseUnit == .millimolesPerLiter {
  140. SettingsItem(title: "Glucose", detail: $glucoseMeasurement.glucoseMMOL)
  141. } else if glucoseUnit == .milligramsPerDeciliter {
  142. SettingsItem(title: "Glucose", detail: $glucoseMeasurement.glucoseMGDL)
  143. }
  144. SettingsItem(title: "Date", detail: $glucoseMeasurement.date )
  145. SettingsItem(title: "Sensor Footer checksum", detail: $glucoseMeasurement.checksum )
  146. }
  147. }
  148. var predictionSection : some View {
  149. Section(header: Text("Last Blood Sugar prediction")) {
  150. if glucoseUnit == .millimolesPerLiter {
  151. SettingsItem(title: "CurrentBG", detail: $glucoseMeasurement.predictionMMOL)
  152. } else if glucoseUnit == .milligramsPerDeciliter {
  153. SettingsItem(title: "Glucose", detail: $glucoseMeasurement.predictionMGDL)
  154. }
  155. SettingsItem(title: "Date", detail: $glucoseMeasurement.predictionDate )
  156. }
  157. }
  158. var sensorInfoSection : some View {
  159. Section(header: Text("Sensor Info")) {
  160. SettingsItem(title: "Sensor Age", detail: $sensorInfo.sensorAge )
  161. SettingsItem(title: "Sensor Age Left", detail: $sensorInfo.sensorAgeLeft )
  162. SettingsItem(title: "Sensor Endtime", detail: $sensorInfo.sensorEndTime )
  163. SettingsItem(title: "Sensor State", detail: $sensorInfo.sensorState )
  164. SettingsItem(title: "Sensor Serial", detail: $sensorInfo.sensorSerial )
  165. }
  166. }
  167. var transmitterInfoSection: some View {
  168. Section(header: Text("Transmitter Info")) {
  169. if !transmitterInfo.battery.isEmpty {
  170. SettingsItem(title: "Battery", detail: $transmitterInfo.battery )
  171. }
  172. SettingsItem(title: "Hardware", detail: $transmitterInfo.hardware )
  173. SettingsItem(title: "Firmware", detail: $transmitterInfo.firmware )
  174. SettingsItem(title: "Connection State", detail: $transmitterInfo.connectionState )
  175. SettingsItem(title: "Transmitter Type", detail: $transmitterInfo.transmitterType )
  176. SettingsItem(title: "Mac", detail: $transmitterInfo.transmitterIdentifier )
  177. SettingsItem(title: "Sensor Type", detail: $transmitterInfo.sensorType )
  178. }
  179. }
  180. var factoryCalibrationSection: some View {
  181. Section(header: Text("Factory Calibration Parameters")) {
  182. ForEach(self.model.factoryCalibrationInfos, id: \.self) { factoryCalibrationInfo in
  183. SettingsItem(title: "i1", detail: factoryCalibrationInfo.i1 )
  184. SettingsItem(title: "i2", detail: factoryCalibrationInfo.i2 )
  185. SettingsItem(title: "i3", detail: factoryCalibrationInfo.i3 )
  186. SettingsItem(title: "i4", detail: factoryCalibrationInfo.i4 )
  187. SettingsItem(title: "i5", detail: factoryCalibrationInfo.i5 )
  188. SettingsItem(title: "i6", detail: factoryCalibrationInfo.i6 )
  189. SettingsItem(title: "Valid for footer", detail: factoryCalibrationInfo.validForFooter )
  190. }
  191. ZStack {
  192. NavigationLink(destination: CalibrationEditView()) {
  193. Button("Edit calibrations") {
  194. print("edit calibration clicked")
  195. }
  196. }
  197. }
  198. }
  199. }
  200. private var dismissButton: some View {
  201. Button( action: {
  202. // This should be enough
  203. //self.presentationMode.wrappedValue.dismiss()
  204. //but since Loop uses uihostingcontroller wrapped in cgmviewcontroller we need
  205. // to notify the parent to close the cgmviewcontrollers navigation
  206. notifyComplete.notify()
  207. }) {
  208. Text("Done")
  209. }
  210. }
  211. var destructSection: some View {
  212. Section {
  213. Button("Delete CGM") {
  214. showingDestructQuestion = true
  215. }.foregroundColor(.red)
  216. .alert(isPresented: $showingDestructQuestion) {
  217. Alert(
  218. title: Text("Are you sure you want to remove this cgm from loop?"),
  219. message: Text("There is no undo"),
  220. primaryButton: .destructive(Text("Delete")) {
  221. notifyDelete.notify()
  222. },
  223. secondaryButton: .cancel()
  224. )
  225. }
  226. }
  227. }
  228. //todo: replace sub with navigationlinks
  229. var advancedSection: some View {
  230. Section(header: Text("Advanced")) {
  231. ZStack {
  232. NavigationLink(destination: GlucoseSettingsView(glucoseUnit: self.glucoseUnit)) {
  233. SettingsItem(title: "Glucose Settings", detail: .constant(""))
  234. }
  235. }
  236. ZStack {
  237. NavigationLink(destination: NotificationSettingsView(glucoseUnit: self.glucoseUnit)) {
  238. SettingsItem(title: "Notifications", detail: .constant(""))
  239. }
  240. }
  241. }
  242. }
  243. var logExportSection : some View {
  244. Section {
  245. Button("Export logs") {
  246. if Features.supportsLogExport {
  247. showingExporter = true
  248. } else {
  249. presentableStatus = StatusMessage(title: "Export not available", message: "Log export requires ios 15")
  250. }
  251. }.foregroundColor(.blue)
  252. }
  253. }
  254. var overview: some View {
  255. List {
  256. measurementSection
  257. // if !glucoseMeasurement.predictionDate.isEmpty{
  258. // predictionSection
  259. // }
  260. advancedSection
  261. sensorInfoSection
  262. transmitterInfoSection
  263. // factoryCalibrationSection
  264. //disable for now due to null byte document issues
  265. if true {
  266. logExportSection
  267. }
  268. destructSection
  269. }
  270. .fileExporter(isPresented: $showingExporter, document: LogsAsTextFile(), contentType: .plainText) { result in
  271. switch result {
  272. case .success(let url):
  273. print("Saved to \(url)")
  274. case .failure(let error):
  275. print(error.localizedDescription)
  276. }
  277. }
  278. .listStyle(InsetGroupedListStyle())
  279. .alert(item: $presentableStatus) { status in
  280. Alert(title: Text(status.title), message: Text(status.message) , dismissButton: .default(Text("Got it!")))
  281. }
  282. }
  283. }
  284. struct LogsAsTextFile: FileDocument {
  285. // tell the system we support only plain text
  286. static var readableContentTypes = [UTType.plainText]
  287. // a simple initializer that creates new, empty documents
  288. init() {
  289. }
  290. // this initializer loads data that has been saved previously
  291. init(configuration: ReadConfiguration) throws {
  292. }
  293. // this will be called when the system wants to write our data to disk
  294. func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
  295. var data = Data()
  296. do {
  297. data = try getLogs()
  298. } catch {
  299. data.append("No logs available".data(using: .utf8, allowLossyConversion: false)!)
  300. }
  301. let wrapper = FileWrapper(regularFileWithContents: data)
  302. let today = Date().getFormattedDate(format: "yyyy-MM-dd")
  303. wrapper.preferredFilename = "libretransmitterlogs-\(today).txt"
  304. return wrapper
  305. }
  306. }
  307. struct SettingsOverview_Previews: PreviewProvider {
  308. static var previews: some View {
  309. NotificationSettingsView(glucoseUnit: HKUnit.millimolesPerLiter)
  310. }
  311. }