SettingsViewController.swift 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513
  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. import SwiftUI
  13. class SettingsViewController: FormViewController {
  14. var tokenRow: TextRow?
  15. var appStateController: AppStateController?
  16. var statusLabelRow: LabelRow!
  17. func showHideNSDetails() {
  18. var isHidden = false
  19. var isEnabled = true
  20. if !IsNightscoutEnabled() {
  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 IsNightscoutEnabled() {
  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 = ObservableUserDefaults.shared.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. ObservableUserDefaults.shared.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. ObservableUserDefaults.shared.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.count > 8 && cleanURL.last == "/" {
  107. cleanURL = String(cleanURL.dropLast())
  108. }
  109. ObservableUserDefaults.shared.url.value = cleanURL
  110. row.value = cleanURL
  111. row.updateCell()
  112. }
  113. self.showHideNSDetails()
  114. // Verify Nightscout URL and token
  115. self.checkNightscoutStatus()
  116. }
  117. <<< TextRow() { row in
  118. row.title = "NS Token"
  119. row.placeholder = "Leave blank if not using tokens"
  120. row.value = UserDefaultsRepository.token.value
  121. row.hidden = "$showNS == false"
  122. self.tokenRow = row
  123. }.cellSetup { (cell, row) in
  124. cell.textField.autocorrectionType = .no
  125. cell.textField.autocapitalizationType = .none
  126. cell.textField.textContentType = .password
  127. }.onChange { row in
  128. if row.value == nil {
  129. UserDefaultsRepository.token.value = ""
  130. }
  131. guard let value = row.value else { return }
  132. UserDefaultsRepository.token.value = value
  133. // Verify Nightscout URL and token
  134. self.checkNightscoutStatus()
  135. }
  136. <<< LabelRow() { row in
  137. row.title = "NS Status"
  138. row.value = "Checking..."
  139. statusLabelRow = row
  140. row.hidden = "$showNS == false"
  141. }
  142. <<< SwitchRow("showDex"){ row in
  143. row.title = "Show Dexcom Settings"
  144. row.value = UserDefaultsRepository.showDex.value
  145. }.onChange { row in
  146. guard let value = row.value else { return }
  147. UserDefaultsRepository.showDex.value = value
  148. }
  149. <<< TextRow(){ row in
  150. row.title = "User Name"
  151. row.value = UserDefaultsRepository.shareUserName.value
  152. row.hidden = "$showDex == false"
  153. }.cellSetup { (cell, row) in
  154. cell.textField.autocorrectionType = .no
  155. cell.textField.autocapitalizationType = .none
  156. }.onChange { row in
  157. if row.value == nil {
  158. UserDefaultsRepository.shareUserName.value = ""
  159. }
  160. guard let value = row.value else { return }
  161. UserDefaultsRepository.shareUserName.value = value
  162. }
  163. <<< TextRow(){ row in
  164. row.title = "Password"
  165. row.value = UserDefaultsRepository.sharePassword.value
  166. row.hidden = "$showDex == false"
  167. }.cellSetup { (cell, row) in
  168. cell.textField.autocorrectionType = .no
  169. cell.textField.isSecureTextEntry = true
  170. cell.textField.autocapitalizationType = .none
  171. }.onChange { row in
  172. if row.value == nil {
  173. UserDefaultsRepository.sharePassword.value = ""
  174. }
  175. guard let value = row.value else { return }
  176. UserDefaultsRepository.sharePassword.value = value
  177. }
  178. <<< SegmentedRow<String>("shareServer") { row in
  179. row.title = "Server"
  180. row.options = ["US", "NON-US"]
  181. row.value = UserDefaultsRepository.shareServer.value
  182. row.hidden = "$showDex == false"
  183. }.onChange { row in
  184. guard let value = row.value else { return }
  185. UserDefaultsRepository.shareServer.value = value
  186. }
  187. +++ Section("App Settings")
  188. <<< ButtonRow("backgroundRefreshSettings") {
  189. $0.title = "Background Refresh Settings"
  190. $0.presentationMode = .show(
  191. controllerProvider: .callback(builder: {
  192. self.presentBackgroundRefreshSettings()
  193. return UIViewController()
  194. }),
  195. onDismiss: nil
  196. )
  197. }
  198. <<< ButtonRow() {
  199. $0.title = "General Settings"
  200. $0.presentationMode = .show(
  201. controllerProvider: .callback(builder: {
  202. let controller = GeneralSettingsViewController()
  203. controller.appStateController = self.appStateController
  204. return controller
  205. }
  206. ), onDismiss: nil)
  207. }
  208. <<< ButtonRow("graphSettings") {
  209. $0.title = "Graph Settings"
  210. $0.presentationMode = .show(
  211. controllerProvider: .callback(builder: {
  212. let controller = GraphSettingsViewController()
  213. controller.appStateController = self.appStateController
  214. return controller
  215. }
  216. ), onDismiss: nil)
  217. }
  218. <<< ButtonRow("informationDisplaySettings") {
  219. $0.title = "Information Display Settings"
  220. $0.presentationMode = .show(
  221. controllerProvider: .callback(builder: {
  222. self.presentInfoDisplaySettings()
  223. return UIViewController()
  224. }
  225. ), onDismiss: nil)
  226. }
  227. <<< ButtonRow("alarmsSettings") {
  228. $0.title = "Alarms"
  229. $0.presentationMode = .show(
  230. controllerProvider: .callback(builder: {
  231. guard let alarmVC = ViewControllerManager.shared.alarmViewController else {
  232. fatalError("AlarmViewController should be pre-instantiated and available")
  233. }
  234. return alarmVC
  235. }), onDismiss: nil)
  236. }
  237. <<< ButtonRow("remoteSettings") {
  238. $0.title = "Remote Settings"
  239. $0.presentationMode = .show(
  240. controllerProvider: .callback(builder: {
  241. self.presentRemoteSettings()
  242. return UIViewController()
  243. }),
  244. onDismiss: nil
  245. )
  246. }
  247. +++ Section("Integrations")
  248. <<< ButtonRow() {
  249. $0.title = "Calendar"
  250. $0.presentationMode = .show(
  251. controllerProvider: .callback(builder: {
  252. let controller = WatchSettingsViewController()
  253. controller.appStateController = self.appStateController
  254. return controller
  255. }
  256. ), onDismiss: nil)
  257. }
  258. <<< ButtonRow("contact") {
  259. $0.title = "Contact"
  260. $0.presentationMode = .show(
  261. controllerProvider: .callback(builder: {
  262. self.presentContactSettings()
  263. return UIViewController()
  264. }
  265. ), onDismiss: nil)
  266. }
  267. +++ Section("Advanced Settings")
  268. <<< ButtonRow() {
  269. $0.title = "Advanced Settings"
  270. $0.presentationMode = .show(
  271. controllerProvider: .callback(builder: {
  272. let controller = AdvancedSettingsViewController()
  273. controller.appStateController = self.appStateController
  274. return controller
  275. }
  276. ), onDismiss: nil)
  277. }
  278. +++ Section("Logging")
  279. <<< ButtonRow("viewlog") {
  280. $0.title = "View Log"
  281. $0.presentationMode = .show(
  282. controllerProvider: .callback(builder: {
  283. self.presentLogView()
  284. return UIViewController()
  285. }
  286. ), onDismiss: nil)
  287. }
  288. <<< ButtonRow("shareLogs") {
  289. $0.title = "Share Logs"
  290. $0.cellSetup { cell, row in
  291. cell.accessibilityIdentifier = "ShareLogsButton"
  292. }
  293. $0.onCellSelection { [weak self] _, _ in
  294. self?.shareLogs()
  295. }
  296. }
  297. +++ Section("Build Information")
  298. <<< LabelRow() {
  299. $0.title = "Version"
  300. $0.value = version
  301. $0.tag = "currentVersionRow"
  302. }
  303. <<< LabelRow() {
  304. $0.title = "Latest version"
  305. $0.value = "Fetching..."
  306. $0.tag = "latestVersionRow"
  307. }
  308. <<< LabelRow() {
  309. $0.title = expirationHeaderString
  310. $0.value = expiration
  311. $0.hidden = Condition(booleanLiteral: isMacApp())
  312. }
  313. <<< LabelRow() {
  314. $0.title = "Built"
  315. $0.value = formattedBuildDate
  316. }
  317. <<< LabelRow() {
  318. $0.title = "Branch"
  319. $0.value = branchAndSha
  320. }
  321. showHideNSDetails()
  322. checkNightscoutStatus()
  323. }
  324. override func viewWillAppear(_ animated: Bool) {
  325. super.viewWillAppear(animated)
  326. refreshVersionInfo()
  327. checkNightscoutStatus()
  328. }
  329. func refreshVersionInfo() {
  330. let versionManager = AppVersionManager()
  331. versionManager.checkForNewVersion { latestVersion, isNewer, isBlacklisted in
  332. DispatchQueue.main.async {
  333. if let currentVersionRow = self.form.rowBy(tag: "currentVersionRow") as? LabelRow {
  334. currentVersionRow.cell.detailTextLabel?.textColor = self.getColor(isBlacklisted: isBlacklisted, isNewer: isNewer, isCurrent: latestVersion == versionManager.version())
  335. currentVersionRow.updateCell()
  336. }
  337. if let latestVersionRow = self.form.rowBy(tag: "latestVersionRow") as? LabelRow {
  338. latestVersionRow.value = latestVersion ?? "Unknown"
  339. latestVersionRow.updateCell()
  340. }
  341. }
  342. }
  343. }
  344. private func getColor(isBlacklisted: Bool, isNewer: Bool, isCurrent: Bool) -> UIColor {
  345. if isBlacklisted {
  346. return .red
  347. } else if isNewer {
  348. return .orange
  349. } else if isCurrent {
  350. return .green
  351. } else {
  352. return .secondaryLabel
  353. }
  354. }
  355. func isMacApp() -> Bool {
  356. #if targetEnvironment(macCatalyst)
  357. return true
  358. #else
  359. return false
  360. #endif
  361. }
  362. func updateStatusLabel(error: NightscoutUtils.NightscoutError?) {
  363. if let error = error {
  364. switch error {
  365. case .invalidURL:
  366. statusLabelRow.value = "Invalid URL"
  367. case .networkError:
  368. statusLabelRow.value = "Network Error"
  369. case .invalidToken:
  370. statusLabelRow.value = "Invalid Token"
  371. case .tokenRequired:
  372. statusLabelRow.value = "Token Required"
  373. case .siteNotFound:
  374. statusLabelRow.value = "Site Not Found"
  375. case .unknown:
  376. statusLabelRow.value = "Unknown Error"
  377. case .emptyAddress:
  378. statusLabelRow.value = "Address Empty"
  379. }
  380. } else {
  381. statusLabelRow.value = "OK (Read\(ObservableUserDefaults.shared.nsWriteAuth.value ? " & Write" : ""))"
  382. NotificationCenter.default.post(name: NSNotification.Name("refresh"), object: nil)
  383. }
  384. statusLabelRow.updateCell()
  385. }
  386. func checkNightscoutStatus() {
  387. NightscoutUtils.verifyURLAndToken { error, jwtToken, nsWriteAuth in
  388. DispatchQueue.main.async {
  389. ObservableUserDefaults.shared.nsWriteAuth.value = nsWriteAuth
  390. self.updateStatusLabel(error: error)
  391. }
  392. }
  393. }
  394. func presentInfoDisplaySettings() {
  395. let viewModel = InfoDisplaySettingsViewModel()
  396. let settingsView = InfoDisplaySettingsView(viewModel: viewModel)
  397. let hostingController = UIHostingController(rootView: settingsView)
  398. hostingController.modalPresentationStyle = .formSheet
  399. if UserDefaultsRepository.forceDarkMode.value {
  400. hostingController.overrideUserInterfaceStyle = .dark
  401. }
  402. present(hostingController, animated: true, completion: nil)
  403. }
  404. func presentRemoteSettings() {
  405. let viewModel = RemoteSettingsViewModel()
  406. let settingsView = RemoteSettingsView(viewModel: viewModel)
  407. let hostingController = UIHostingController(rootView: settingsView)
  408. hostingController.modalPresentationStyle = .formSheet
  409. if UserDefaultsRepository.forceDarkMode.value {
  410. hostingController.overrideUserInterfaceStyle = .dark
  411. }
  412. present(hostingController, animated: true, completion: nil)
  413. }
  414. func presentContactSettings() {
  415. let viewModel = ContactSettingsViewModel()
  416. let contactSettingsView = ContactSettingsView(viewModel: viewModel)
  417. let hostingController = UIHostingController(rootView: contactSettingsView)
  418. hostingController.modalPresentationStyle = .formSheet
  419. if UserDefaultsRepository.forceDarkMode.value {
  420. hostingController.overrideUserInterfaceStyle = .dark
  421. }
  422. present(hostingController, animated: true, completion: nil)
  423. }
  424. func presentBackgroundRefreshSettings() {
  425. let viewModel = BackgroundRefreshSettingsViewModel()
  426. let settingsView = BackgroundRefreshSettingsView(viewModel: viewModel)
  427. let hostingController = UIHostingController(rootView: settingsView)
  428. hostingController.modalPresentationStyle = .formSheet
  429. if UserDefaultsRepository.forceDarkMode.value {
  430. hostingController.overrideUserInterfaceStyle = .dark
  431. }
  432. present(hostingController, animated: true, completion: nil)
  433. }
  434. func presentLogView() {
  435. let viewModel = LogViewModel()
  436. let logView = LogView(viewModel: viewModel)
  437. let hostingController = UIHostingController(rootView: logView)
  438. hostingController.modalPresentationStyle = .formSheet
  439. if UserDefaultsRepository.forceDarkMode.value {
  440. hostingController.overrideUserInterfaceStyle = .dark
  441. }
  442. present(hostingController, animated: true, completion: nil)
  443. }
  444. private func shareLogs() {
  445. let logManager = LogManager.shared
  446. let logFileURL = logManager.currentLogFileURL
  447. if FileManager.default.fileExists(atPath: logFileURL.path) {
  448. let activityViewController = UIActivityViewController(activityItems: [logFileURL], applicationActivities: nil)
  449. activityViewController.popoverPresentationController?.sourceView = self.view
  450. present(activityViewController, animated: true, completion: nil)
  451. } else {
  452. let alert = UIAlertController(title: "No Logs Available", message: "There are no logs to share.", preferredStyle: .alert)
  453. alert.addAction(UIAlertAction(title: "OK", style: .default))
  454. present(alert, animated: true, completion: nil)
  455. }
  456. }
  457. }