ContentView.swift 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. import SwiftUI
  2. import SwiftData
  3. // MARK: - ContentView
  4. /// Root navigation container for the CelestiaTrace app.
  5. /// Uses a TabView with three tabs: Home, Field Records, and Settings.
  6. struct ContentView: View {
  7. @Environment(\.modelContext) private var modelContext
  8. @Environment(\.scenePhase) private var scenePhase
  9. @Query(sort: \CelestiaSession.startTime, order: .reverse) private var sessions: [CelestiaSession]
  10. @ObservedObject private var authManager = AuthManager.shared
  11. @AppStorage(DeveloperLogStore.enabledKey) private var isDeveloperModeEnabled = false
  12. /// Currently selected tab.
  13. @State private var selectedTab: Tab = .home
  14. /// Session selected for detail playback navigation in Field Records tab.
  15. @State private var fieldRecordsSelectedSession: CelestiaSession?
  16. @State private var showFieldRecordsAuthentication = false
  17. @State private var shouldOpenFieldRecordsAfterLogin = false
  18. var body: some View {
  19. ZStack(alignment: .bottomTrailing) {
  20. TabView(selection: protectedTabSelection) {
  21. HomeView { session in
  22. fieldRecordsSelectedSession = session
  23. selectTab(.fieldRecords)
  24. }
  25. .tabItem {
  26. Label(Tab.home.title, systemImage: Tab.home.icon)
  27. }
  28. .tag(Tab.home)
  29. SessionListView(
  30. selectedSession: $fieldRecordsSelectedSession,
  31. onStartRecording: {
  32. selectedTab = .home
  33. }
  34. )
  35. .tabItem {
  36. Label(Tab.fieldRecords.title, systemImage: Tab.fieldRecords.icon)
  37. }
  38. .tag(Tab.fieldRecords)
  39. SettingsView()
  40. .tabItem {
  41. Label(Tab.settings.title, systemImage: Tab.settings.icon)
  42. }
  43. .tag(Tab.settings)
  44. }
  45. .tint(.celestiaCyan)
  46. if isDeveloperModeEnabled {
  47. DeveloperLogOverlay()
  48. .transition(.scale(scale: 0.92, anchor: .bottomTrailing).combined(with: .opacity))
  49. .zIndex(20)
  50. }
  51. }
  52. .animation(.easeInOut(duration: 0.2), value: isDeveloperModeEnabled)
  53. .sheet(isPresented: $showFieldRecordsAuthentication, onDismiss: {
  54. if !authManager.isLoggedIn {
  55. shouldOpenFieldRecordsAfterLogin = false
  56. }
  57. }) {
  58. AuthModalView(
  59. guidanceMessage: "登录或注册后,才能查看现场记录"
  60. )
  61. }
  62. .onAppear {
  63. if isDeveloperModeEnabled {
  64. DeveloperLogStore.log("App", "开发者模式已启用,开始记录本次运行日志", level: .success)
  65. }
  66. if RecordingRecoveryStore.activeSessionID != nil
  67. || RecordingLiveActivityManager.shared.recoverableSessionID != nil {
  68. selectedTab = .home
  69. }
  70. }
  71. .task(id: authManager.currentUser?.id) {
  72. await synchronizeFieldRecords()
  73. }
  74. .onChange(of: authManager.currentUser?.id) { _, userID in
  75. if userID != nil, shouldOpenFieldRecordsAfterLogin {
  76. shouldOpenFieldRecordsAfterLogin = false
  77. selectedTab = .fieldRecords
  78. } else if userID == nil, selectedTab == .fieldRecords {
  79. selectedTab = .home
  80. requestFieldRecordsAuthentication()
  81. }
  82. }
  83. .onChange(of: scenePhase) { _, newPhase in
  84. guard newPhase == .active else { return }
  85. Task {
  86. await synchronizeFieldRecords()
  87. }
  88. }
  89. .onOpenURL { url in
  90. guard RecordingLiveActivityRoute(url: url) != nil else { return }
  91. selectedTab = .home
  92. }
  93. .onChange(of: isDeveloperModeEnabled) { _, enabled in
  94. if enabled {
  95. DeveloperLogStore.log("App", "开发者模式已开启", level: .success)
  96. }
  97. }
  98. }
  99. private var protectedTabSelection: Binding<Tab> {
  100. Binding(
  101. get: { selectedTab },
  102. set: { selectTab($0) }
  103. )
  104. }
  105. private func selectTab(_ tab: Tab) {
  106. guard tab != .fieldRecords || authManager.isLoggedIn else {
  107. requestFieldRecordsAuthentication()
  108. return
  109. }
  110. selectedTab = tab
  111. }
  112. private func requestFieldRecordsAuthentication() {
  113. shouldOpenFieldRecordsAfterLogin = true
  114. showFieldRecordsAuthentication = true
  115. }
  116. @MainActor
  117. private func synchronizeFieldRecords() async {
  118. guard let userID = authManager.currentUser?.id else { return }
  119. _ = await SyncManager.shared.sync(
  120. sessions: sessions,
  121. modelContext: modelContext,
  122. userID: userID
  123. )
  124. }
  125. }
  126. // MARK: - Tab Definition
  127. extension ContentView {
  128. /// Defines the available tabs in the app's root navigation.
  129. enum Tab: Hashable {
  130. case home
  131. case fieldRecords
  132. case settings
  133. var title: String {
  134. switch self {
  135. case .home: "首页"
  136. case .fieldRecords: "现场记录"
  137. case .settings: "设置"
  138. }
  139. }
  140. var icon: String {
  141. switch self {
  142. case .home: "house.fill"
  143. case .fieldRecords: "waveform.path.ecg"
  144. case .settings: "gearshape.fill"
  145. }
  146. }
  147. }
  148. }
  149. // MARK: - Preview
  150. #Preview {
  151. ContentView()
  152. .modelContainer(for: CelestiaSession.self, inMemory: true)
  153. }