MoreMenuView.swift 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  1. // LoopFollow
  2. // MoreMenuView.swift
  3. import SwiftUI
  4. import UIKit
  5. struct MoreMenuView: View {
  6. @State private var pendingRoute: MenuRoute?
  7. @State private var latestVersion: String?
  8. @State private var versionTint: Color = .secondary
  9. @State private var alertTitle = ""
  10. @State private var alertMessage = ""
  11. @State private var showAlert = false
  12. @State private var currentVersion: String = AppVersionManager().version()
  13. var body: some View {
  14. List {
  15. // Settings
  16. Section {
  17. NavigationLink(value: SettingsRoute.settings) {
  18. Label("Settings", systemImage: "gearshape")
  19. }
  20. }
  21. // Features
  22. Section("Features") {
  23. ForEach(TabItem.featureOrder) { item in
  24. FullRowButton(showsChevron: true) {
  25. let tabs = Storage.shared.orderedTabBarItems()
  26. if let tabIndex = tabs.firstIndex(of: item) {
  27. Observable.shared.selectedTabIndex.value = tabIndex
  28. } else {
  29. pendingRoute = MenuRoute(item)
  30. }
  31. } label: {
  32. Label(item.displayName, systemImage: item.icon)
  33. }
  34. }
  35. }
  36. // Logging
  37. Section("Logging") {
  38. FullRowButton(showsChevron: true) { pendingRoute = .log } label: {
  39. Label("View Log", systemImage: "doc.text.magnifyingglass")
  40. }
  41. FullRowButton { shareLogs() } label: {
  42. Label("Share Logs", systemImage: "square.and.arrow.up")
  43. }
  44. }
  45. // Support & Community
  46. Section("Support & Community") {
  47. Link(destination: URL(string: "https://loopfollowdocs.org/")!) {
  48. HStack {
  49. Label("LoopFollow Docs", systemImage: "book")
  50. Spacer()
  51. Image(systemName: "arrow.up.right.square")
  52. .foregroundStyle(.tertiary)
  53. }
  54. }
  55. Link(destination: URL(string: "https://discord.gg/KQgk3gzuYU")!) {
  56. HStack {
  57. Label("Loop and Learn Discord", systemImage: "bubble.left.and.bubble.right")
  58. Spacer()
  59. Image(systemName: "arrow.up.right.square")
  60. .foregroundStyle(.tertiary)
  61. }
  62. }
  63. Link(destination: URL(string: "https://www.facebook.com/groups/loopfollowlnl")!) {
  64. HStack {
  65. Label("LoopFollow Facebook Group", systemImage: "person.2.fill")
  66. Spacer()
  67. Image(systemName: "arrow.up.right.square")
  68. .foregroundStyle(.tertiary)
  69. }
  70. }
  71. }
  72. // Build Information
  73. Section("Build Information") {
  74. buildInfoRow(title: "Version", value: currentVersion, color: versionTint)
  75. buildInfoRow(title: "Latest version", value: latestVersion ?? "Fetching…", color: .secondary)
  76. let build = BuildDetails.default
  77. if !(build.isMacApp() || build.isSimulatorBuild()) {
  78. buildInfoRow(
  79. title: build.expirationHeaderString,
  80. value: dateTimeUtils.formattedDate(from: build.calculateExpirationDate()),
  81. color: .secondary
  82. )
  83. }
  84. buildInfoRow(title: "Built", value: dateTimeUtils.formattedDate(from: build.buildDate()), color: .secondary)
  85. buildInfoRow(title: "Branch", value: build.branchAndSha, color: .secondary)
  86. }
  87. }
  88. .listStyle(.insetGrouped)
  89. .navigationTitle("Menu")
  90. .navigationBarTitleDisplayMode(.large)
  91. .task {
  92. await fetchVersionInfo()
  93. }
  94. .alert(alertTitle, isPresented: $showAlert) {
  95. Button("OK", role: .cancel) {}
  96. } message: {
  97. Text(alertMessage)
  98. }
  99. .navigationDestination(for: SettingsRoute.self) { $0.destination }
  100. .navigationDestination(
  101. isPresented: Binding(
  102. get: { pendingRoute != nil },
  103. set: { if !$0 { pendingRoute = nil } }
  104. )
  105. ) {
  106. if let route = pendingRoute {
  107. route.destination
  108. }
  109. }
  110. }
  111. // MARK: - Helpers
  112. private func buildInfoRow(title: String, value: String, color: Color) -> some View {
  113. HStack {
  114. Text(title)
  115. Spacer()
  116. Text(value)
  117. .foregroundStyle(color)
  118. }
  119. }
  120. private func shareLogs() {
  121. let files = LogManager.shared.logFilesForTodayAndYesterday()
  122. guard !files.isEmpty else {
  123. alertTitle = "No Logs Available"
  124. alertMessage = "There are no logs to share."
  125. showAlert = true
  126. return
  127. }
  128. let avc = UIActivityViewController(activityItems: files, applicationActivities: nil)
  129. UIApplication.shared.topMost?.present(avc, animated: true)
  130. }
  131. private func fetchVersionInfo() async {
  132. let mgr = AppVersionManager()
  133. let (latest, newer, blacklisted) = await mgr.checkForNewVersionAsync()
  134. latestVersion = latest ?? "Unknown"
  135. versionTint = blacklisted ? .red
  136. : newer ? .orange
  137. : latest == currentVersion ? .green
  138. : .secondary
  139. }
  140. }
  141. // MARK: – Full-row tappable button
  142. private struct FullRowButton<Label: View>: View {
  143. var showsChevron: Bool = false
  144. let action: () -> Void
  145. @ViewBuilder let label: () -> Label
  146. var body: some View {
  147. Button(action: action) {
  148. HStack {
  149. label()
  150. Spacer(minLength: 0)
  151. if showsChevron {
  152. Image(systemName: "chevron.right")
  153. .font(.footnote.weight(.semibold))
  154. .foregroundStyle(.tertiary)
  155. }
  156. }
  157. .contentShape(Rectangle())
  158. }
  159. .buttonStyle(.plain)
  160. }
  161. }
  162. // MARK: – Menu routing
  163. enum MenuRoute: Hashable {
  164. case home
  165. case alarms
  166. case remote
  167. case nightscout
  168. case snoozer
  169. case treatments
  170. case stats
  171. case log
  172. init(_ item: TabItem) {
  173. switch item {
  174. case .home: self = .home
  175. case .alarms: self = .alarms
  176. case .remote: self = .remote
  177. case .nightscout: self = .nightscout
  178. case .snoozer: self = .snoozer
  179. case .treatments: self = .treatments
  180. case .stats: self = .stats
  181. }
  182. }
  183. @ViewBuilder
  184. var destination: some View {
  185. switch self {
  186. case .home: HomeContentView(isModal: true)
  187. case .alarms: AlarmsContainerView(embedsInNavigationStack: false)
  188. case .remote: RemoteContentView()
  189. case .nightscout: NightscoutContentView()
  190. case .snoozer: SnoozerView()
  191. case .treatments: TreatmentsView()
  192. case .stats: AggregatedStatsContentView(mainViewController: MainViewController.shared)
  193. case .log: LogView()
  194. }
  195. }
  196. }