| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513 |
- //
- // SettingsViewController.swift
- // LoopFollow
- //
- // Created by Jon Fawcett on 6/3/20.
- // Copyright © 2020 Jon Fawcett. All rights reserved.
- //
- import UIKit
- import Eureka
- import EventKit
- import EventKitUI
- import SwiftUI
- class SettingsViewController: FormViewController {
- var tokenRow: TextRow?
- var appStateController: AppStateController?
- var statusLabelRow: LabelRow!
- func showHideNSDetails() {
- var isHidden = false
- var isEnabled = true
- if !IsNightscoutEnabled() {
- isHidden = true
- isEnabled = false
- }
- if let row1 = form.rowBy(tag: "informationDisplaySettings") as? ButtonRow {
- row1.hidden = .function(["hide"], {form in
- return isHidden
- })
- row1.evaluateHidden()
- }
- if IsNightscoutEnabled() {
- isEnabled = true
- }
- guard let nightscoutTab = self.tabBarController?.tabBar.items![3] else { return }
- nightscoutTab.isEnabled = isEnabled
- }
- override func viewDidLoad() {
- super.viewDidLoad()
- if UserDefaultsRepository.forceDarkMode.value {
- overrideUserInterfaceStyle = .dark
- }
- UserDefaultsRepository.showNS.value = false
- UserDefaultsRepository.showDex.value = false
- let buildDetails = BuildDetails.default
- let formattedBuildDate = dateTimeUtils.formattedDate(from: buildDetails.buildDate())
- let branchAndSha = buildDetails.branchAndSha
- let expiration = dateTimeUtils.formattedDate(from: buildDetails.calculateExpirationDate())
- let expirationHeaderString = buildDetails.expirationHeaderString
- let versionManager = AppVersionManager()
- let version = versionManager.version()
- form
- +++ Section(header: "Data Settings", footer: "")
- <<< SegmentedRow<String>("units") { row in
- row.title = "Units"
- row.options = ["mg/dL", "mmol/L"]
- row.value = UserDefaultsRepository.units.value
- }.onChange { row in
- guard let value = row.value else { return }
- UserDefaultsRepository.units.value = value
- }
- <<< SwitchRow("showNS"){ row in
- row.title = "Show Nightscout Settings"
- row.value = UserDefaultsRepository.showNS.value
- }.onChange { [weak self] row in
- guard let value = row.value else { return }
- UserDefaultsRepository.showNS.value = value
- }
- <<< TextRow() { row in
- row.title = "URL"
- row.placeholder = "https://mycgm.herokuapp.com"
- row.value = ObservableUserDefaults.shared.url.value
- row.hidden = "$showNS == false"
- }.cellSetup { (cell, row) in
- cell.textField.autocorrectionType = .no
- cell.textField.autocapitalizationType = .none
- }.onChange { row in
- guard let value = row.value else {
- ObservableUserDefaults.shared.url.value = ""
- self.showHideNSDetails()
- return
- }
- var useTokenUrl = false
- // Attempt to handle special case: pasted URL including token
- if let urlComponents = URLComponents(string: value), let queryItems = urlComponents.queryItems {
- if let tokenItem = queryItems.first(where: { $0.name.lowercased() == "token" }) {
- let tokenPattern = "^[^-\\s]+-[0-9a-fA-F]{16}$"
- if let token = tokenItem.value, let _ = token.range(of: tokenPattern, options: .regularExpression) {
- var baseComponents = urlComponents
- baseComponents.queryItems = nil
- if let baseURL = baseComponents.string {
- UserDefaultsRepository.token.value = token
- self.tokenRow?.value = token
- self.tokenRow?.updateCell()
- ObservableUserDefaults.shared.url.value = baseURL
- row.value = baseURL
- row.updateCell()
- useTokenUrl = true
- }
- }
- }
- }
- if !useTokenUrl {
- // Normalize input: remove unwanted characters and lowercase
- let filtered = value.replacingOccurrences(of: "[^A-Za-z0-9:/._-]", with: "", options: .regularExpression).lowercased()
- // Further clean-up: Remove trailing slashes
- var cleanURL = filtered
- while cleanURL.count > 8 && cleanURL.last == "/" {
- cleanURL = String(cleanURL.dropLast())
- }
- ObservableUserDefaults.shared.url.value = cleanURL
- row.value = cleanURL
- row.updateCell()
- }
- self.showHideNSDetails()
- // Verify Nightscout URL and token
- self.checkNightscoutStatus()
- }
- <<< TextRow() { row in
- row.title = "NS Token"
- row.placeholder = "Leave blank if not using tokens"
- row.value = UserDefaultsRepository.token.value
- row.hidden = "$showNS == false"
- self.tokenRow = row
- }.cellSetup { (cell, row) in
- cell.textField.autocorrectionType = .no
- cell.textField.autocapitalizationType = .none
- cell.textField.textContentType = .password
- }.onChange { row in
- if row.value == nil {
- UserDefaultsRepository.token.value = ""
- }
- guard let value = row.value else { return }
- UserDefaultsRepository.token.value = value
- // Verify Nightscout URL and token
- self.checkNightscoutStatus()
- }
- <<< LabelRow() { row in
- row.title = "NS Status"
- row.value = "Checking..."
- statusLabelRow = row
- row.hidden = "$showNS == false"
- }
- <<< SwitchRow("showDex"){ row in
- row.title = "Show Dexcom Settings"
- row.value = UserDefaultsRepository.showDex.value
- }.onChange { row in
- guard let value = row.value else { return }
- UserDefaultsRepository.showDex.value = value
- }
- <<< TextRow(){ row in
- row.title = "User Name"
- row.value = UserDefaultsRepository.shareUserName.value
- row.hidden = "$showDex == false"
- }.cellSetup { (cell, row) in
- cell.textField.autocorrectionType = .no
- cell.textField.autocapitalizationType = .none
- }.onChange { row in
- if row.value == nil {
- UserDefaultsRepository.shareUserName.value = ""
- }
- guard let value = row.value else { return }
- UserDefaultsRepository.shareUserName.value = value
- }
- <<< TextRow(){ row in
- row.title = "Password"
- row.value = UserDefaultsRepository.sharePassword.value
- row.hidden = "$showDex == false"
- }.cellSetup { (cell, row) in
- cell.textField.autocorrectionType = .no
- cell.textField.isSecureTextEntry = true
- cell.textField.autocapitalizationType = .none
- }.onChange { row in
- if row.value == nil {
- UserDefaultsRepository.sharePassword.value = ""
- }
- guard let value = row.value else { return }
- UserDefaultsRepository.sharePassword.value = value
- }
- <<< SegmentedRow<String>("shareServer") { row in
- row.title = "Server"
- row.options = ["US", "NON-US"]
- row.value = UserDefaultsRepository.shareServer.value
- row.hidden = "$showDex == false"
- }.onChange { row in
- guard let value = row.value else { return }
- UserDefaultsRepository.shareServer.value = value
- }
- +++ Section("App Settings")
- <<< ButtonRow("backgroundRefreshSettings") {
- $0.title = "Background Refresh Settings"
- $0.presentationMode = .show(
- controllerProvider: .callback(builder: {
- self.presentBackgroundRefreshSettings()
- return UIViewController()
- }),
- onDismiss: nil
- )
- }
- <<< ButtonRow() {
- $0.title = "General Settings"
- $0.presentationMode = .show(
- controllerProvider: .callback(builder: {
- let controller = GeneralSettingsViewController()
- controller.appStateController = self.appStateController
- return controller
- }
- ), onDismiss: nil)
- }
- <<< ButtonRow("graphSettings") {
- $0.title = "Graph Settings"
- $0.presentationMode = .show(
- controllerProvider: .callback(builder: {
- let controller = GraphSettingsViewController()
- controller.appStateController = self.appStateController
- return controller
- }
- ), onDismiss: nil)
- }
- <<< ButtonRow("informationDisplaySettings") {
- $0.title = "Information Display Settings"
- $0.presentationMode = .show(
- controllerProvider: .callback(builder: {
- self.presentInfoDisplaySettings()
- return UIViewController()
- }
- ), onDismiss: nil)
- }
- <<< ButtonRow("alarmsSettings") {
- $0.title = "Alarms"
- $0.presentationMode = .show(
- controllerProvider: .callback(builder: {
- guard let alarmVC = ViewControllerManager.shared.alarmViewController else {
- fatalError("AlarmViewController should be pre-instantiated and available")
- }
- return alarmVC
- }), onDismiss: nil)
- }
- <<< ButtonRow("remoteSettings") {
- $0.title = "Remote Settings"
- $0.presentationMode = .show(
- controllerProvider: .callback(builder: {
- self.presentRemoteSettings()
- return UIViewController()
- }),
- onDismiss: nil
- )
- }
- +++ Section("Integrations")
- <<< ButtonRow() {
- $0.title = "Calendar"
- $0.presentationMode = .show(
- controllerProvider: .callback(builder: {
- let controller = WatchSettingsViewController()
- controller.appStateController = self.appStateController
- return controller
- }
- ), onDismiss: nil)
- }
- <<< ButtonRow("contact") {
- $0.title = "Contact"
- $0.presentationMode = .show(
- controllerProvider: .callback(builder: {
- self.presentContactSettings()
- return UIViewController()
- }
- ), onDismiss: nil)
- }
- +++ Section("Advanced Settings")
- <<< ButtonRow() {
- $0.title = "Advanced Settings"
- $0.presentationMode = .show(
- controllerProvider: .callback(builder: {
- let controller = AdvancedSettingsViewController()
- controller.appStateController = self.appStateController
- return controller
- }
- ), onDismiss: nil)
- }
- +++ Section("Logging")
- <<< ButtonRow("viewlog") {
- $0.title = "View Log"
- $0.presentationMode = .show(
- controllerProvider: .callback(builder: {
- self.presentLogView()
- return UIViewController()
- }
- ), onDismiss: nil)
- }
- <<< ButtonRow("shareLogs") {
- $0.title = "Share Logs"
- $0.cellSetup { cell, row in
- cell.accessibilityIdentifier = "ShareLogsButton"
- }
- $0.onCellSelection { [weak self] _, _ in
- self?.shareLogs()
- }
- }
- +++ Section("Build Information")
- <<< LabelRow() {
- $0.title = "Version"
- $0.value = version
- $0.tag = "currentVersionRow"
- }
- <<< LabelRow() {
- $0.title = "Latest version"
- $0.value = "Fetching..."
- $0.tag = "latestVersionRow"
- }
- <<< LabelRow() {
- $0.title = expirationHeaderString
- $0.value = expiration
- $0.hidden = Condition(booleanLiteral: isMacApp())
- }
- <<< LabelRow() {
- $0.title = "Built"
- $0.value = formattedBuildDate
- }
- <<< LabelRow() {
- $0.title = "Branch"
- $0.value = branchAndSha
- }
- showHideNSDetails()
- checkNightscoutStatus()
- }
- override func viewWillAppear(_ animated: Bool) {
- super.viewWillAppear(animated)
- refreshVersionInfo()
- checkNightscoutStatus()
- }
- func refreshVersionInfo() {
- let versionManager = AppVersionManager()
- versionManager.checkForNewVersion { latestVersion, isNewer, isBlacklisted in
- DispatchQueue.main.async {
- if let currentVersionRow = self.form.rowBy(tag: "currentVersionRow") as? LabelRow {
- currentVersionRow.cell.detailTextLabel?.textColor = self.getColor(isBlacklisted: isBlacklisted, isNewer: isNewer, isCurrent: latestVersion == versionManager.version())
- currentVersionRow.updateCell()
- }
- if let latestVersionRow = self.form.rowBy(tag: "latestVersionRow") as? LabelRow {
- latestVersionRow.value = latestVersion ?? "Unknown"
- latestVersionRow.updateCell()
- }
- }
- }
- }
- private func getColor(isBlacklisted: Bool, isNewer: Bool, isCurrent: Bool) -> UIColor {
- if isBlacklisted {
- return .red
- } else if isNewer {
- return .orange
- } else if isCurrent {
- return .green
- } else {
- return .secondaryLabel
- }
- }
- func isMacApp() -> Bool {
- #if targetEnvironment(macCatalyst)
- return true
- #else
- return false
- #endif
- }
- func updateStatusLabel(error: NightscoutUtils.NightscoutError?) {
- if let error = error {
- switch error {
- case .invalidURL:
- statusLabelRow.value = "Invalid URL"
- case .networkError:
- statusLabelRow.value = "Network Error"
- case .invalidToken:
- statusLabelRow.value = "Invalid Token"
- case .tokenRequired:
- statusLabelRow.value = "Token Required"
- case .siteNotFound:
- statusLabelRow.value = "Site Not Found"
- case .unknown:
- statusLabelRow.value = "Unknown Error"
- case .emptyAddress:
- statusLabelRow.value = "Address Empty"
- }
- } else {
- statusLabelRow.value = "OK (Read\(ObservableUserDefaults.shared.nsWriteAuth.value ? " & Write" : ""))"
- NotificationCenter.default.post(name: NSNotification.Name("refresh"), object: nil)
- }
- statusLabelRow.updateCell()
- }
- func checkNightscoutStatus() {
- NightscoutUtils.verifyURLAndToken { error, jwtToken, nsWriteAuth in
- DispatchQueue.main.async {
- ObservableUserDefaults.shared.nsWriteAuth.value = nsWriteAuth
- self.updateStatusLabel(error: error)
- }
- }
- }
- func presentInfoDisplaySettings() {
- let viewModel = InfoDisplaySettingsViewModel()
- let settingsView = InfoDisplaySettingsView(viewModel: viewModel)
- let hostingController = UIHostingController(rootView: settingsView)
- hostingController.modalPresentationStyle = .formSheet
- if UserDefaultsRepository.forceDarkMode.value {
- hostingController.overrideUserInterfaceStyle = .dark
- }
- present(hostingController, animated: true, completion: nil)
- }
- func presentRemoteSettings() {
- let viewModel = RemoteSettingsViewModel()
- let settingsView = RemoteSettingsView(viewModel: viewModel)
- let hostingController = UIHostingController(rootView: settingsView)
- hostingController.modalPresentationStyle = .formSheet
- if UserDefaultsRepository.forceDarkMode.value {
- hostingController.overrideUserInterfaceStyle = .dark
- }
- present(hostingController, animated: true, completion: nil)
- }
- func presentContactSettings() {
- let viewModel = ContactSettingsViewModel()
- let contactSettingsView = ContactSettingsView(viewModel: viewModel)
- let hostingController = UIHostingController(rootView: contactSettingsView)
- hostingController.modalPresentationStyle = .formSheet
- if UserDefaultsRepository.forceDarkMode.value {
- hostingController.overrideUserInterfaceStyle = .dark
- }
- present(hostingController, animated: true, completion: nil)
- }
- func presentBackgroundRefreshSettings() {
- let viewModel = BackgroundRefreshSettingsViewModel()
- let settingsView = BackgroundRefreshSettingsView(viewModel: viewModel)
- let hostingController = UIHostingController(rootView: settingsView)
- hostingController.modalPresentationStyle = .formSheet
- if UserDefaultsRepository.forceDarkMode.value {
- hostingController.overrideUserInterfaceStyle = .dark
- }
- present(hostingController, animated: true, completion: nil)
- }
- func presentLogView() {
- let viewModel = LogViewModel()
- let logView = LogView(viewModel: viewModel)
- let hostingController = UIHostingController(rootView: logView)
- hostingController.modalPresentationStyle = .formSheet
- if UserDefaultsRepository.forceDarkMode.value {
- hostingController.overrideUserInterfaceStyle = .dark
- }
- present(hostingController, animated: true, completion: nil)
- }
- private func shareLogs() {
- let logManager = LogManager.shared
- let logFileURL = logManager.currentLogFileURL
- if FileManager.default.fileExists(atPath: logFileURL.path) {
- let activityViewController = UIActivityViewController(activityItems: [logFileURL], applicationActivities: nil)
- activityViewController.popoverPresentationController?.sourceView = self.view
- present(activityViewController, animated: true, completion: nil)
- } else {
- let alert = UIAlertController(title: "No Logs Available", message: "There are no logs to share.", preferredStyle: .alert)
- alert.addAction(UIAlertAction(title: "OK", style: .default))
- present(alert, animated: true, completion: nil)
- }
- }
- }
|