MoreMenuViewController.swift 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413
  1. // LoopFollow
  2. // MoreMenuViewController.swift
  3. import Combine
  4. import SwiftUI
  5. import UIKit
  6. class MoreMenuViewController: UIViewController {
  7. private var tableView: UITableView!
  8. private var cancellables = Set<AnyCancellable>()
  9. private var fallbackMainViewController: MainViewController?
  10. struct MenuItem {
  11. let title: String
  12. let icon: String
  13. let subtitle: String?
  14. let action: () -> Void
  15. init(title: String, icon: String, subtitle: String? = nil, action: @escaping () -> Void) {
  16. self.title = title
  17. self.icon = icon
  18. self.subtitle = subtitle
  19. self.action = action
  20. }
  21. }
  22. private var menuSections: [[MenuItem]] = []
  23. override func viewDidLoad() {
  24. super.viewDidLoad()
  25. title = "Menu"
  26. view.backgroundColor = .systemBackground
  27. // Apply appearance mode
  28. overrideUserInterfaceStyle = Storage.shared.appearanceMode.value.userInterfaceStyle
  29. // Listen for appearance setting changes
  30. Storage.shared.appearanceMode.$value
  31. .receive(on: DispatchQueue.main)
  32. .sink { [weak self] mode in
  33. self?.overrideUserInterfaceStyle = mode.userInterfaceStyle
  34. }
  35. .store(in: &cancellables)
  36. // Listen for system appearance changes (when in System mode)
  37. NotificationCenter.default.publisher(for: .appearanceDidChange)
  38. .receive(on: DispatchQueue.main)
  39. .sink { [weak self] _ in
  40. self?.overrideUserInterfaceStyle = Storage.shared.appearanceMode.value.userInterfaceStyle
  41. }
  42. .store(in: &cancellables)
  43. setupTableView()
  44. updateMenuItems()
  45. }
  46. override func viewWillAppear(_ animated: Bool) {
  47. super.viewWillAppear(animated)
  48. updateMenuItems()
  49. tableView.reloadData()
  50. Observable.shared.settingsPath.set(NavigationPath())
  51. }
  52. override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
  53. super.traitCollectionDidChange(previousTraitCollection)
  54. if Storage.shared.appearanceMode.value == .system,
  55. previousTraitCollection?.userInterfaceStyle != traitCollection.userInterfaceStyle
  56. {
  57. overrideUserInterfaceStyle = Storage.shared.appearanceMode.value.userInterfaceStyle
  58. }
  59. }
  60. private func setupTableView() {
  61. tableView = UITableView(frame: view.bounds, style: .insetGrouped)
  62. tableView.translatesAutoresizingMaskIntoConstraints = false
  63. tableView.delegate = self
  64. tableView.dataSource = self
  65. tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
  66. view.addSubview(tableView)
  67. NSLayoutConstraint.activate([
  68. tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
  69. tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
  70. tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
  71. tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
  72. ])
  73. }
  74. private func updateMenuItems() {
  75. menuSections = []
  76. // Section 0: Settings (always fixed at top)
  77. let settingsSection = [
  78. MenuItem(
  79. title: "Settings",
  80. icon: "gear",
  81. action: { [weak self] in
  82. self?.openSettings()
  83. }
  84. ),
  85. ]
  86. menuSections.append(settingsSection)
  87. let itemsInMenu = Storage.shared.itemsInMenu()
  88. if !itemsInMenu.isEmpty {
  89. var dynamicSection: [MenuItem] = []
  90. for item in itemsInMenu {
  91. dynamicSection.append(MenuItem(
  92. title: item.displayName,
  93. icon: item.icon,
  94. action: { [weak self] in
  95. self?.openItem(item)
  96. }
  97. ))
  98. }
  99. menuSections.append(dynamicSection)
  100. }
  101. // Section: Community
  102. let communitySection = [
  103. MenuItem(
  104. title: "LoopFollow Facebook Group",
  105. icon: "person.2.fill",
  106. action: { [weak self] in
  107. self?.openFacebookGroup()
  108. }
  109. ),
  110. ]
  111. menuSections.append(communitySection)
  112. }
  113. private func openItem(_ item: TabItem) {
  114. switch item {
  115. case .home:
  116. openHome()
  117. case .alarms:
  118. openAlarmsConfig()
  119. case .remote:
  120. openRemote()
  121. case .nightscout:
  122. openNightscout()
  123. case .snoozer:
  124. openSnoozer()
  125. case .treatments:
  126. openTreatments()
  127. case .stats:
  128. openAggregatedStats()
  129. }
  130. }
  131. private func openSettings() {
  132. let settingsVC = UIHostingController(rootView: SettingsMenuView())
  133. let navController = UINavigationController(rootViewController: settingsVC)
  134. let style = Storage.shared.appearanceMode.value.userInterfaceStyle
  135. settingsVC.overrideUserInterfaceStyle = style
  136. navController.overrideUserInterfaceStyle = style
  137. settingsVC.navigationItem.rightBarButtonItem = UIBarButtonItem(
  138. image: UIImage(systemName: "checkmark"),
  139. style: .plain,
  140. target: self,
  141. action: #selector(dismissSettingsModal)
  142. )
  143. settingsVC.navigationItem.rightBarButtonItem?.tintColor = .systemBlue
  144. navController.modalPresentationStyle = .fullScreen
  145. present(navController, animated: true)
  146. }
  147. private func openAlarmsConfig() {
  148. let alarmsVC = UIHostingController(rootView: AlarmsContainerView())
  149. alarmsVC.title = "Alarms"
  150. let navController = UINavigationController(rootViewController: alarmsVC)
  151. let style = Storage.shared.appearanceMode.value.userInterfaceStyle
  152. alarmsVC.overrideUserInterfaceStyle = style
  153. navController.overrideUserInterfaceStyle = style
  154. alarmsVC.navigationItem.rightBarButtonItem = UIBarButtonItem(
  155. image: UIImage(systemName: "checkmark"),
  156. style: .plain,
  157. target: self,
  158. action: #selector(dismissModal)
  159. )
  160. alarmsVC.navigationItem.rightBarButtonItem?.tintColor = .systemBlue
  161. navController.modalPresentationStyle = .fullScreen
  162. present(navController, animated: true)
  163. }
  164. private func openRemote() {
  165. let storyboard = UIStoryboard(name: "Main", bundle: nil)
  166. let remoteVC = storyboard.instantiateViewController(withIdentifier: "RemoteViewController")
  167. let navController = UINavigationController(rootViewController: remoteVC)
  168. let style = Storage.shared.appearanceMode.value.userInterfaceStyle
  169. remoteVC.overrideUserInterfaceStyle = style
  170. navController.overrideUserInterfaceStyle = style
  171. remoteVC.navigationItem.rightBarButtonItem = UIBarButtonItem(
  172. image: UIImage(systemName: "checkmark"),
  173. style: .plain,
  174. target: self,
  175. action: #selector(dismissModal)
  176. )
  177. remoteVC.navigationItem.rightBarButtonItem?.tintColor = .systemBlue
  178. navController.modalPresentationStyle = .fullScreen
  179. present(navController, animated: true)
  180. }
  181. private func openNightscout() {
  182. let storyboard = UIStoryboard(name: "Main", bundle: nil)
  183. let nightscoutVC = storyboard.instantiateViewController(withIdentifier: "NightscoutViewController")
  184. let navController = UINavigationController(rootViewController: nightscoutVC)
  185. let style = Storage.shared.appearanceMode.value.userInterfaceStyle
  186. nightscoutVC.overrideUserInterfaceStyle = style
  187. navController.overrideUserInterfaceStyle = style
  188. nightscoutVC.navigationItem.rightBarButtonItem = UIBarButtonItem(
  189. image: UIImage(systemName: "checkmark"),
  190. style: .plain,
  191. target: self,
  192. action: #selector(dismissModal)
  193. )
  194. nightscoutVC.navigationItem.rightBarButtonItem?.tintColor = .systemBlue
  195. navController.modalPresentationStyle = .fullScreen
  196. present(navController, animated: true)
  197. }
  198. private func openSnoozer() {
  199. let storyboard = UIStoryboard(name: "Main", bundle: nil)
  200. let snoozerVC = storyboard.instantiateViewController(withIdentifier: "SnoozerViewController")
  201. let navController = UINavigationController(rootViewController: snoozerVC)
  202. let style = Storage.shared.appearanceMode.value.userInterfaceStyle
  203. snoozerVC.overrideUserInterfaceStyle = style
  204. navController.overrideUserInterfaceStyle = style
  205. snoozerVC.navigationItem.rightBarButtonItem = UIBarButtonItem(
  206. image: UIImage(systemName: "checkmark"),
  207. style: .plain,
  208. target: self,
  209. action: #selector(dismissModal)
  210. )
  211. snoozerVC.navigationItem.rightBarButtonItem?.tintColor = .systemBlue
  212. navController.modalPresentationStyle = .fullScreen
  213. present(navController, animated: true)
  214. }
  215. private func openTreatments() {
  216. let treatmentsVC = UIHostingController(rootView: TreatmentsView())
  217. let navController = UINavigationController(rootViewController: treatmentsVC)
  218. // Apply appearance mode
  219. let style = Storage.shared.appearanceMode.value.userInterfaceStyle
  220. treatmentsVC.overrideUserInterfaceStyle = style
  221. navController.overrideUserInterfaceStyle = style
  222. // Add a close button
  223. treatmentsVC.navigationItem.rightBarButtonItem = UIBarButtonItem(
  224. image: UIImage(systemName: "checkmark"),
  225. style: .plain,
  226. target: self,
  227. action: #selector(dismissModal)
  228. )
  229. treatmentsVC.navigationItem.rightBarButtonItem?.tintColor = .systemBlue
  230. navController.modalPresentationStyle = .fullScreen
  231. present(navController, animated: true)
  232. }
  233. private func openAggregatedStats() {
  234. guard let mainVC = getMainViewController() else {
  235. presentSimpleAlert(title: "Error", message: "Unable to access data")
  236. return
  237. }
  238. let statsVC = UIHostingController(
  239. rootView: AggregatedStatsView(viewModel: AggregatedStatsViewModel(mainViewController: mainVC))
  240. )
  241. let navController = UINavigationController(rootViewController: statsVC)
  242. // Apply appearance mode
  243. let style = Storage.shared.appearanceMode.value.userInterfaceStyle
  244. statsVC.overrideUserInterfaceStyle = style
  245. navController.overrideUserInterfaceStyle = style
  246. // Add a close button
  247. statsVC.navigationItem.rightBarButtonItem = UIBarButtonItem(
  248. image: UIImage(systemName: "checkmark"),
  249. style: .plain,
  250. target: self,
  251. action: #selector(dismissModal)
  252. )
  253. statsVC.navigationItem.rightBarButtonItem?.tintColor = .systemBlue
  254. navController.modalPresentationStyle = .fullScreen
  255. present(navController, animated: true)
  256. }
  257. private func openHome() {
  258. // First check if Home is in the tab bar
  259. if let tabVC = tabBarController {
  260. for (index, vc) in (tabVC.viewControllers ?? []).enumerated() {
  261. if vc is MainViewController {
  262. // Home is in the tab bar, switch to it
  263. tabVC.selectedIndex = index
  264. return
  265. }
  266. }
  267. }
  268. // Home is in the menu - present the full Home screen as a modal
  269. let homeModalView = HomeModalView()
  270. let hostingController = UIHostingController(rootView: homeModalView)
  271. hostingController.overrideUserInterfaceStyle = Storage.shared.appearanceMode.value.userInterfaceStyle
  272. hostingController.modalPresentationStyle = .fullScreen
  273. present(hostingController, animated: true)
  274. }
  275. private func openFacebookGroup() {
  276. if let url = URL(string: "https://www.facebook.com/groups/loopfollowlnl") {
  277. UIApplication.shared.open(url)
  278. }
  279. }
  280. @objc private func dismissSettingsModal() {
  281. dismiss(animated: true) {
  282. // Rebuild tabs after settings is dismissed to apply any tab order changes
  283. MainViewController.rebuildTabsIfNeeded()
  284. }
  285. }
  286. private func getMainViewController() -> MainViewController? {
  287. // Try to find MainViewController in the view hierarchy
  288. guard let tabBarController = tabBarController else { return nil }
  289. for vc in tabBarController.viewControllers ?? [] {
  290. if let mainVC = vc as? MainViewController {
  291. return mainVC
  292. }
  293. if let navVC = vc as? UINavigationController,
  294. let mainVC = navVC.viewControllers.first as? MainViewController
  295. {
  296. return mainVC
  297. }
  298. }
  299. if let fallbackMainViewController {
  300. return fallbackMainViewController
  301. }
  302. let storyboard = UIStoryboard(name: "Main", bundle: nil)
  303. guard let mainVC = storyboard.instantiateViewController(withIdentifier: "MainViewController") as? MainViewController else {
  304. return nil
  305. }
  306. mainVC.isPresentedAsModal = true
  307. fallbackMainViewController = mainVC
  308. return mainVC
  309. }
  310. @objc private func dismissModal() {
  311. dismiss(animated: true)
  312. }
  313. }
  314. extension MoreMenuViewController: UITableViewDataSource, UITableViewDelegate {
  315. func numberOfSections(in _: UITableView) -> Int {
  316. return menuSections.count
  317. }
  318. func tableView(_: UITableView, numberOfRowsInSection section: Int) -> Int {
  319. return menuSections[section].count
  320. }
  321. func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  322. let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
  323. let item = menuSections[indexPath.section][indexPath.row]
  324. var config = cell.defaultContentConfiguration()
  325. config.text = item.title
  326. config.image = UIImage(systemName: item.icon)
  327. if let subtitle = item.subtitle {
  328. config.secondaryText = subtitle
  329. config.secondaryTextProperties.color = .orange
  330. config.secondaryTextProperties.font = .preferredFont(forTextStyle: .caption1)
  331. }
  332. cell.contentConfiguration = config
  333. cell.accessoryType = .disclosureIndicator
  334. return cell
  335. }
  336. func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
  337. tableView.deselectRow(at: indexPath, animated: true)
  338. menuSections[indexPath.section][indexPath.row].action()
  339. }
  340. }