ContentView.swift 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
  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. var body: some View {
  17. ZStack(alignment: .bottomTrailing) {
  18. TabView(selection: $selectedTab) {
  19. HomeView { session in
  20. fieldRecordsSelectedSession = session
  21. selectedTab = .fieldRecords
  22. }
  23. .tabItem {
  24. Label(Tab.home.title, systemImage: Tab.home.icon)
  25. }
  26. .tag(Tab.home)
  27. SessionListView(
  28. selectedSession: $fieldRecordsSelectedSession,
  29. onStartRecording: {
  30. selectedTab = .home
  31. }
  32. )
  33. .tabItem {
  34. Label(Tab.fieldRecords.title, systemImage: Tab.fieldRecords.icon)
  35. }
  36. .tag(Tab.fieldRecords)
  37. SettingsView()
  38. .tabItem {
  39. Label(Tab.settings.title, systemImage: Tab.settings.icon)
  40. }
  41. .tag(Tab.settings)
  42. }
  43. .tint(.celestiaCyan)
  44. if isDeveloperModeEnabled {
  45. DeveloperLogOverlay()
  46. .transition(.scale(scale: 0.92, anchor: .bottomTrailing).combined(with: .opacity))
  47. .zIndex(20)
  48. }
  49. }
  50. .animation(.easeInOut(duration: 0.2), value: isDeveloperModeEnabled)
  51. .onAppear {
  52. if isDeveloperModeEnabled {
  53. DeveloperLogStore.log("App", "开发者模式已启用,开始记录本次运行日志", level: .success)
  54. }
  55. if RecordingRecoveryStore.activeSessionID != nil
  56. || RecordingLiveActivityManager.shared.recoverableSessionID != nil {
  57. selectedTab = .home
  58. }
  59. }
  60. .task(id: authManager.currentUser?.id) {
  61. await synchronizeFieldRecords()
  62. }
  63. .onChange(of: scenePhase) { _, newPhase in
  64. guard newPhase == .active else { return }
  65. Task {
  66. await synchronizeFieldRecords()
  67. }
  68. }
  69. .onOpenURL { url in
  70. guard RecordingLiveActivityRoute(url: url) != nil else { return }
  71. selectedTab = .home
  72. }
  73. .onChange(of: isDeveloperModeEnabled) { _, enabled in
  74. if enabled {
  75. DeveloperLogStore.log("App", "开发者模式已开启", level: .success)
  76. }
  77. }
  78. }
  79. @MainActor
  80. private func synchronizeFieldRecords() async {
  81. guard let userID = authManager.currentUser?.id else { return }
  82. _ = await SyncManager.shared.sync(
  83. sessions: sessions,
  84. modelContext: modelContext,
  85. userID: userID
  86. )
  87. }
  88. }
  89. // MARK: - Tab Definition
  90. extension ContentView {
  91. /// Defines the available tabs in the app's root navigation.
  92. enum Tab: Hashable {
  93. case home
  94. case fieldRecords
  95. case settings
  96. var title: String {
  97. switch self {
  98. case .home: "首页"
  99. case .fieldRecords: "现场记录"
  100. case .settings: "设置"
  101. }
  102. }
  103. var icon: String {
  104. switch self {
  105. case .home: "house.fill"
  106. case .fieldRecords: "waveform.path.ecg"
  107. case .settings: "gearshape.fill"
  108. }
  109. }
  110. }
  111. }
  112. // MARK: - Preview
  113. #Preview {
  114. ContentView()
  115. .modelContainer(for: CelestiaSession.self, inMemory: true)
  116. }