SettingsViewController.swift 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387
  1. //
  2. // SettingsViewController.swift
  3. // LoopFollow
  4. //
  5. // Created by Jon Fawcett on 6/3/20.
  6. // Copyright © 2020 Jon Fawcett. All rights reserved.
  7. //
  8. import UIKit
  9. import Eureka
  10. import EventKit
  11. import EventKitUI
  12. class SettingsViewController: FormViewController {
  13. var tokenRow: TextRow?
  14. var appStateController: AppStateController?
  15. var statusLabelRow: LabelRow!
  16. func showHideNSDetails() {
  17. var isHidden = false
  18. var isEnabled = true
  19. var isLoopHidden = false;
  20. if UserDefaultsRepository.url.value == "" || !UserDefaultsRepository.loopUser.value {
  21. isHidden = true
  22. isEnabled = false
  23. }
  24. if let row1 = form.rowBy(tag: "informationDisplaySettings") as? ButtonRow {
  25. row1.hidden = .function(["hide"], {form in
  26. return isHidden
  27. })
  28. row1.evaluateHidden()
  29. }
  30. if UserDefaultsRepository.url.value != "" {
  31. isEnabled = true
  32. }
  33. guard let nightscoutTab = self.tabBarController?.tabBar.items![3] else { return }
  34. nightscoutTab.isEnabled = isEnabled
  35. }
  36. override func viewDidLoad() {
  37. super.viewDidLoad()
  38. if UserDefaultsRepository.forceDarkMode.value {
  39. overrideUserInterfaceStyle = .dark
  40. }
  41. UserDefaultsRepository.showNS.value = false
  42. UserDefaultsRepository.showDex.value = false
  43. let buildDetails = BuildDetails.default
  44. let formattedBuildDate = dateTimeUtils.formattedDate(from: buildDetails.buildDate())
  45. let branchAndSha = buildDetails.branchAndSha
  46. let expiration = dateTimeUtils.formattedDate(from: buildDetails.calculateExpirationDate())
  47. let expirationHeaderString = buildDetails.expirationHeaderString
  48. let versionManager = AppVersionManager()
  49. let version = versionManager.version()
  50. form
  51. +++ Section(header: "Data Settings", footer: "")
  52. <<< SegmentedRow<String>("units") { row in
  53. row.title = "Units"
  54. row.options = ["mg/dL", "mmol/L"]
  55. row.value = UserDefaultsRepository.units.value
  56. }.onChange { row in
  57. guard let value = row.value else { return }
  58. UserDefaultsRepository.units.value = value
  59. }
  60. <<< SwitchRow("showNS"){ row in
  61. row.title = "Show Nightscout Settings"
  62. row.value = UserDefaultsRepository.showNS.value
  63. }.onChange { [weak self] row in
  64. guard let value = row.value else { return }
  65. UserDefaultsRepository.showNS.value = value
  66. }
  67. <<< TextRow() { row in
  68. row.title = "URL"
  69. row.placeholder = "https://mycgm.herokuapp.com"
  70. row.value = UserDefaultsRepository.url.value
  71. row.hidden = "$showNS == false"
  72. }.cellSetup { (cell, row) in
  73. cell.textField.autocorrectionType = .no
  74. cell.textField.autocapitalizationType = .none
  75. }.onChange { row in
  76. guard let value = row.value else {
  77. UserDefaultsRepository.url.value = ""
  78. self.showHideNSDetails()
  79. return
  80. }
  81. var useTokenUrl = false
  82. // Attempt to handle special case: pasted URL including token
  83. if let urlComponents = URLComponents(string: value), let queryItems = urlComponents.queryItems {
  84. if let tokenItem = queryItems.first(where: { $0.name.lowercased() == "token" }) {
  85. let tokenPattern = "^[^-\\s]+-[0-9a-fA-F]{16}$"
  86. if let token = tokenItem.value, let _ = token.range(of: tokenPattern, options: .regularExpression) {
  87. var baseComponents = urlComponents
  88. baseComponents.queryItems = nil
  89. if let baseURL = baseComponents.string {
  90. UserDefaultsRepository.token.value = token
  91. self.tokenRow?.value = token
  92. self.tokenRow?.updateCell()
  93. UserDefaultsRepository.url.value = baseURL
  94. row.value = baseURL
  95. row.updateCell()
  96. useTokenUrl = true
  97. }
  98. }
  99. }
  100. }
  101. if !useTokenUrl {
  102. // Normalize input: remove unwanted characters and lowercase
  103. let filtered = value.replacingOccurrences(of: "[^A-Za-z0-9:/._-]", with: "", options: .regularExpression).lowercased()
  104. // Further clean-up: Remove trailing slashes
  105. var cleanURL = filtered
  106. while cleanURL.last == "/" {
  107. cleanURL = String(cleanURL.dropLast())
  108. }
  109. UserDefaultsRepository.url.value = cleanURL
  110. row.value = cleanURL
  111. row.updateCell()
  112. }
  113. self.showHideNSDetails()
  114. globalVariables.nsVerifiedAlert = 0
  115. // Verify Nightscout URL and token
  116. self.checkNightscoutStatus()
  117. }
  118. <<< TextRow() { row in
  119. row.title = "NS Token"
  120. row.placeholder = "Leave blank if not using tokens"
  121. row.value = UserDefaultsRepository.token.value
  122. row.hidden = "$showNS == false"
  123. self.tokenRow = row
  124. }.cellSetup { (cell, row) in
  125. cell.textField.autocorrectionType = .no
  126. cell.textField.autocapitalizationType = .none
  127. cell.textField.textContentType = .password
  128. }.onChange { row in
  129. if row.value == nil {
  130. UserDefaultsRepository.token.value = ""
  131. }
  132. guard let value = row.value else { return }
  133. UserDefaultsRepository.token.value = value
  134. globalVariables.nsVerifiedAlert = 0
  135. // Verify Nightscout URL and token
  136. self.checkNightscoutStatus()
  137. }
  138. <<< LabelRow() { row in
  139. row.title = "NS Status"
  140. row.value = "Checking..."
  141. statusLabelRow = row
  142. row.hidden = "$showNS == false"
  143. }
  144. <<< SwitchRow("loopUser"){ row in
  145. row.title = "Download Loop/iAPS Data"
  146. row.tag = "loopUser"
  147. row.value = UserDefaultsRepository.loopUser.value
  148. row.hidden = "$showNS == false"
  149. }.onChange { row in
  150. guard let value = row.value else { return }
  151. UserDefaultsRepository.loopUser.value = value
  152. }
  153. <<< SwitchRow("showDex"){ row in
  154. row.title = "Show Dexcom Settings"
  155. row.value = UserDefaultsRepository.showDex.value
  156. }.onChange { row in
  157. guard let value = row.value else { return }
  158. UserDefaultsRepository.showDex.value = value
  159. }
  160. <<< TextRow(){ row in
  161. row.title = "User Name"
  162. row.value = UserDefaultsRepository.shareUserName.value
  163. row.hidden = "$showDex == false"
  164. }.cellSetup { (cell, row) in
  165. cell.textField.autocorrectionType = .no
  166. cell.textField.autocapitalizationType = .none
  167. }.onChange { row in
  168. if row.value == nil {
  169. UserDefaultsRepository.shareUserName.value = ""
  170. }
  171. guard let value = row.value else { return }
  172. UserDefaultsRepository.shareUserName.value = value
  173. globalVariables.dexVerifiedAlert = 0
  174. }
  175. <<< TextRow(){ row in
  176. row.title = "Password"
  177. row.value = UserDefaultsRepository.sharePassword.value
  178. row.hidden = "$showDex == false"
  179. }.cellSetup { (cell, row) in
  180. cell.textField.autocorrectionType = .no
  181. cell.textField.isSecureTextEntry = true
  182. cell.textField.autocapitalizationType = .none
  183. }.onChange { row in
  184. if row.value == nil {
  185. UserDefaultsRepository.sharePassword.value = ""
  186. }
  187. guard let value = row.value else { return }
  188. UserDefaultsRepository.sharePassword.value = value
  189. globalVariables.dexVerifiedAlert = 0
  190. }
  191. <<< SegmentedRow<String>("shareServer") { row in
  192. row.title = "Server"
  193. row.options = ["US", "NON-US"]
  194. row.value = UserDefaultsRepository.shareServer.value
  195. row.hidden = "$showDex == false"
  196. }.onChange { row in
  197. guard let value = row.value else { return }
  198. UserDefaultsRepository.shareServer.value = value
  199. }
  200. +++ Section("App Settings")
  201. <<< ButtonRow() {
  202. $0.title = "General Settings"
  203. $0.presentationMode = .show(
  204. controllerProvider: .callback(builder: {
  205. let controller = GeneralSettingsViewController()
  206. controller.appStateController = self.appStateController
  207. return controller
  208. }
  209. ), onDismiss: nil)
  210. }
  211. <<< ButtonRow("graphSettings") {
  212. $0.title = "Graph Settings"
  213. $0.presentationMode = .show(
  214. controllerProvider: .callback(builder: {
  215. let controller = GraphSettingsViewController()
  216. controller.appStateController = self.appStateController
  217. return controller
  218. }
  219. ), onDismiss: nil)
  220. }
  221. <<< ButtonRow("informationDisplaySettings") {
  222. $0.title = "Information Display Settings"
  223. $0.presentationMode = .show(
  224. controllerProvider: .callback(builder: {
  225. let controller = InfoDisplaySettingsViewController()
  226. controller.appStateController = self.appStateController
  227. return controller
  228. }
  229. ), onDismiss: nil)
  230. }
  231. +++ Section("Integrations")
  232. <<< ButtonRow() {
  233. $0.title = "Apple Watch and Carplay"
  234. $0.presentationMode = .show(
  235. controllerProvider: .callback(builder: {
  236. let controller = WatchSettingsViewController()
  237. controller.appStateController = self.appStateController
  238. return controller
  239. }
  240. ), onDismiss: nil)
  241. }
  242. +++ Section("Advanced Settings")
  243. <<< ButtonRow() {
  244. $0.title = "Advanced Settings"
  245. $0.presentationMode = .show(
  246. controllerProvider: .callback(builder: {
  247. let controller = AdvancedSettingsViewController()
  248. controller.appStateController = self.appStateController
  249. return controller
  250. }
  251. ), onDismiss: nil)
  252. }
  253. +++ Section("Build Information")
  254. <<< LabelRow() {
  255. $0.title = "Version"
  256. $0.value = version
  257. $0.tag = "currentVersionRow"
  258. }
  259. <<< LabelRow() {
  260. $0.title = "Latest version"
  261. $0.value = "Fetching..."
  262. $0.tag = "latestVersionRow"
  263. }
  264. <<< LabelRow() {
  265. $0.title = expirationHeaderString
  266. $0.value = expiration
  267. $0.hidden = Condition(booleanLiteral: isMacApp())
  268. }
  269. <<< LabelRow() {
  270. $0.title = "Built"
  271. $0.value = formattedBuildDate
  272. }
  273. <<< LabelRow() {
  274. $0.title = "Branch"
  275. $0.value = branchAndSha
  276. }
  277. showHideNSDetails()
  278. checkNightscoutStatus()
  279. }
  280. override func viewWillAppear(_ animated: Bool) {
  281. super.viewWillAppear(animated)
  282. refreshVersionInfo()
  283. checkNightscoutStatus()
  284. }
  285. func refreshVersionInfo() {
  286. let versionManager = AppVersionManager()
  287. versionManager.checkForNewVersion { latestVersion, isNewer, isBlacklisted in
  288. DispatchQueue.main.async {
  289. if let currentVersionRow = self.form.rowBy(tag: "currentVersionRow") as? LabelRow {
  290. currentVersionRow.cell.detailTextLabel?.textColor = self.getColor(isBlacklisted: isBlacklisted, isNewer: isNewer, isCurrent: latestVersion == versionManager.version())
  291. currentVersionRow.updateCell()
  292. }
  293. if let latestVersionRow = self.form.rowBy(tag: "latestVersionRow") as? LabelRow {
  294. latestVersionRow.value = latestVersion ?? "Unknown"
  295. latestVersionRow.updateCell()
  296. }
  297. }
  298. }
  299. }
  300. private func getColor(isBlacklisted: Bool, isNewer: Bool, isCurrent: Bool) -> UIColor {
  301. if isBlacklisted {
  302. return .red
  303. } else if isNewer {
  304. return .orange
  305. } else if isCurrent {
  306. return .green
  307. } else {
  308. return .secondaryLabel
  309. }
  310. }
  311. func isMacApp() -> Bool {
  312. #if targetEnvironment(macCatalyst)
  313. return true
  314. #else
  315. return false
  316. #endif
  317. }
  318. func updateStatusLabel(error: NightscoutUtils.NightscoutError?) {
  319. if let error = error {
  320. switch error {
  321. case .invalidURL:
  322. statusLabelRow.value = "Invalid URL"
  323. case .networkError:
  324. statusLabelRow.value = "Network Error"
  325. case .invalidToken:
  326. statusLabelRow.value = "Invalid Token"
  327. case .tokenRequired:
  328. statusLabelRow.value = "Token Required"
  329. case .siteNotFound:
  330. statusLabelRow.value = "Site Not Found"
  331. case .unknown:
  332. statusLabelRow.value = "Unknown Error"
  333. case .emptyAddress:
  334. statusLabelRow.value = "Address Empty"
  335. }
  336. } else {
  337. statusLabelRow.value = "OK"
  338. NotificationCenter.default.post(name: NSNotification.Name("refresh"), object: nil)
  339. }
  340. statusLabelRow.updateCell()
  341. }
  342. func checkNightscoutStatus() {
  343. NightscoutUtils.verifyURLAndToken(urlUser: UserDefaultsRepository.url.value, token: UserDefaultsRepository.token.value) { error in
  344. DispatchQueue.main.async {
  345. self.updateStatusLabel(error: error)
  346. }
  347. }
  348. }
  349. }