Procházet zdrojové kódy

refactor(project): 模块目录重构为 FieldRecords 并更新 Xcode 工程与权限配置

bob.yuxinyang před 1 měsícem
rodič
revize
c6ec2e98db

+ 11 - 3
CelestiaTrace.xcodeproj/project.pbxproj

@@ -54,6 +54,8 @@
 		C7FCC7859292F4917426A553 /* CelestiaTraceLiveActivityBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72CA6784C282FC33FAD436B8 /* CelestiaTraceLiveActivityBundle.swift */; };
 		C8794B9720CD85F518A82473 /* BoundDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EF9EF0F961DB2B546B9C0F1 /* BoundDevice.swift */; };
 		CD45B55AB993D054E50A43BE /* RealAudioRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 977EB01CFA491A1573D7CF22 /* RealAudioRecorder.swift */; };
+		C0DE10000000000000000001 /* TimelineLocationPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DE10000000000000000002 /* TimelineLocationPicker.swift */; };
+		C0DE20000000000000000001 /* DeveloperLogOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DE20000000000000000002 /* DeveloperLogOverlay.swift */; };
 		DDC0D9C4883B4861CBC74C39 /* ChronoFeedRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9275B988BEBBEA80370E1AC4 /* ChronoFeedRow.swift */; };
 		E78FC729614A4C93C97D564D /* ColorTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95AF20E8EF4319BBEBA608C1 /* ColorTheme.swift */; };
 		F385E2FCB06D39917F51846E /* AudioRecorderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B407473A307EEAF2CB5EE94 /* AudioRecorderProtocol.swift */; };
@@ -140,6 +142,8 @@
 		AA13A08133A50094CBFFDDF9 /* EditProfileModalView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = EditProfileModalView.swift; sourceTree = "<group>"; };
 		AEED3C75367F66D6D6CE745D /* SilenceDetector.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SilenceDetector.swift; sourceTree = "<group>"; };
 		B7B9693BD9924C08A1F2ECD1 /* ActiveRecordingView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ActiveRecordingView.swift; sourceTree = "<group>"; };
+		C0DE10000000000000000002 /* TimelineLocationPicker.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TimelineLocationPicker.swift; sourceTree = "<group>"; };
+		C0DE20000000000000000002 /* DeveloperLogOverlay.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DeveloperLogOverlay.swift; sourceTree = "<group>"; };
 		DADF67536C0CF9AE7F405C74 /* ImagePicker.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ImagePicker.swift; sourceTree = "<group>"; };
 		DB09EA3EE9CFFADB34B5B43A /* RecordingEnvironmentDetector.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RecordingEnvironmentDetector.swift; sourceTree = "<group>"; };
 		DB56F0DFCB1FE77D7636B43D /* CelestiaTrace.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CelestiaTrace.app; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -297,18 +301,18 @@
 				ADD73EAAF268C8B37E097585 /* Components */,
 				21AD838392427D841273B592 /* Detail */,
 				44E01A3C9C72DBC8106DEE17 /* Profile */,
-				96406E65C0CB98B68F7082F4 /* History */,
+				96406E65C0CB98B68F7082F4 /* FieldRecords */,
 				71787388F9456A4784534BCC /* ContentView.swift */,
 			);
 			path = Views;
 			sourceTree = "<group>";
 		};
-		96406E65C0CB98B68F7082F4 /* History */ = {
+		96406E65C0CB98B68F7082F4 /* FieldRecords */ = {
 			isa = PBXGroup;
 			children = (
 				0EBB5FC2AC0B9D31C7A04715 /* SessionListView.swift */,
 			);
-			path = History;
+			path = FieldRecords;
 			sourceTree = "<group>";
 		};
 		A9B8C7D6E5F4000000000005 /* Splash */ = {
@@ -342,6 +346,8 @@
 			children = (
 				4F4B4B59E0916CE9ACFF362F /* DiscreetWarningBanner.swift */,
 				DADF67536C0CF9AE7F405C74 /* ImagePicker.swift */,
+				C0DE20000000000000000002 /* DeveloperLogOverlay.swift */,
+				C0DE10000000000000000002 /* TimelineLocationPicker.swift */,
 				3C76F629F3460D6F077C3708 /* SyncAuthPromptModal.swift */,
 				77D23B83EDDD5489004E42DC /* PulsingRecordButton.swift */,
 			);
@@ -607,6 +613,8 @@
 				8B5C51BB4474283BDF4ADACB /* LiveWaveformView.swift in Sources */,
 				75AE08DD135D81A3E19190EE /* DiscreetWarningBanner.swift in Sources */,
 				F5035B942DE1FEC10515D9D6 /* ImagePicker.swift in Sources */,
+				C0DE20000000000000000001 /* DeveloperLogOverlay.swift in Sources */,
+				C0DE10000000000000000001 /* TimelineLocationPicker.swift in Sources */,
 				2FD7EE603C849E281034CDD8 /* SyncAuthPromptModal.swift in Sources */,
 				AF09E026C9AB15874D870E1F /* PulsingRecordButton.swift in Sources */,
 				2C16272F61F8B36A2EF69129 /* SessionDetailView.swift in Sources */,

+ 2 - 0
CelestiaTrace/Resources/Info.plist

@@ -34,6 +34,8 @@
 	<string>需要相机权限以在现场录制中拍摄照片记录</string>
 	<key>NSMicrophoneUsageDescription</key>
 	<string>需要麦克风权限以使用 iPhone 进行现场录音</string>
+	<key>NSLocationWhenInUseUsageDescription</key>
+	<string>需要定位权限以在笔记和照片记录中插入所在位置</string>
 	<key>NSPhotoLibraryUsageDescription</key>
 	<string>需要相册权限以选择照片记录</string>
 	<key>UIBackgroundModes</key>

+ 5 - 0
CelestiaTrace/Services/RecordingLiveActivityManager.swift

@@ -9,6 +9,11 @@ final class RecordingLiveActivityManager {
 
     private init() {}
 
+    var recoverableSessionID: UUID? {
+        activity?.attributes.sessionID
+            ?? Activity<RecordingActivityAttributes>.activities.first?.attributes.sessionID
+    }
+
     func start(
         sessionID: UUID,
         title: String,

+ 13 - 7
CelestiaTrace/ViewModels/RecordingViewModel.swift

@@ -20,6 +20,9 @@ final class RecordingViewModel {
     private(set) var sourceDisplayName: String
     private(set) var statusMessage: String = "正在准备"
     private(set) var errorMessage: String?
+    /// URL of the segment currently being recorded. Exposed as observed state
+    /// so the recovery coordinator can persist it as soon as it is allocated.
+    private(set) var outputFileURL: URL?
 
     // MARK: - Computed Properties
 
@@ -32,11 +35,6 @@ final class RecordingViewModel {
         return String(format: "%02d:%02d:%02d", hours, minutes, seconds)
     }
 
-    /// Local URL where the audio file is stored.
-    var outputFileURL: URL? {
-        recorder.outputFileURL
-    }
-
     // MARK: - Private
 
     /// Concrete recorder instance conforming to AudioRecorderProtocol.
@@ -121,10 +119,17 @@ final class RecordingViewModel {
     /// - Parameter session: The active `CelestiaSession` to attach the event to.
     /// - Returns: The newly created `CelestiaTimelineEvent`.
     @discardableResult
-    func addPhotoEvent(to session: CelestiaSession, localFilePath: String? = nil) -> CelestiaTimelineEvent {
-        let relativeMs = Int64(elapsedTime * 1000)
+    func addPhotoEvent(
+        to session: CelestiaSession,
+        relativeTimeMs: Int64? = nil,
+        localFilePath: String? = nil,
+        note: String? = nil
+    ) -> CelestiaTimelineEvent {
+        let relativeMs = relativeTimeMs ?? Int64(elapsedTime * 1000)
         let event = CelestiaTimelineEvent(relativeTimeMs: relativeMs, eventType: "PHOTO")
         event.localFilePath = localFilePath
+        let trimmedNote = note?.trimmingCharacters(in: .whitespacesAndNewlines)
+        event.textContent = trimmedNote?.isEmpty == false ? trimmedNote : nil
         session.events.append(event)
         session.isSynced = false
         session.syncState = .pending
@@ -192,5 +197,6 @@ final class RecordingViewModel {
         sourceDisplayName = recorder.sourceDisplayName
         statusMessage = recorder.statusMessage
         errorMessage = recorder.errorMessage
+        outputFileURL = recorder.outputFileURL
     }
 }

+ 40 - 15
CelestiaTrace/Views/History/SessionListView.swift → CelestiaTrace/Views/FieldRecords/SessionListView.swift

@@ -8,16 +8,21 @@ import SwiftData
 struct SessionListView: View {
     @Environment(\.modelContext) private var modelContext
     @Query(sort: \CelestiaSession.startTime, order: .reverse) private var sessions: [CelestiaSession]
+    @ObservedObject private var authManager = AuthManager.shared
 
+    @Binding var selectedSession: CelestiaSession?
     var onStartRecording: () -> Void = {}
 
     @State private var searchText = ""
     @State private var deletionError: String?
     @State private var sessionPendingDeletion: CelestiaSession?
-    @State private var selectedSession: CelestiaSession?
-    @State private var showSessionDetail = false
     @State private var deletingSessionID: UUID?
 
+    init(selectedSession: Binding<CelestiaSession?> = .constant(nil), onStartRecording: @escaping () -> Void = {}) {
+        self._selectedSession = selectedSession
+        self.onStartRecording = onStartRecording
+    }
+
     var body: some View {
         NavigationStack {
             ZStack {
@@ -29,13 +34,11 @@ struct SessionListView: View {
                     sessionList
                 }
             }
-            .navigationTitle("历史记录")
+            .navigationTitle("现场记录")
             .navigationBarTitleDisplayMode(.inline)
             .modifier(SessionSearchModifier(isEnabled: !sessions.isEmpty, searchText: $searchText))
-            .navigationDestination(isPresented: $showSessionDetail) {
-                if let selectedSession {
-                    SessionDetailView(session: selectedSession)
-                }
+            .navigationDestination(item: $selectedSession) { session in
+                SessionDetailView(session: session)
             }
             .alert("删除失败", isPresented: Binding(
                 get: { deletionError != nil },
@@ -91,7 +94,6 @@ struct SessionListView: View {
             ForEach(filteredSessions) { session in
                 Button {
                     selectedSession = session
-                    showSessionDetail = true
                 } label: {
                     SessionRowCard(session: session)
                         .contentShape(Rectangle())
@@ -113,6 +115,9 @@ struct SessionListView: View {
         .listStyle(.plain)
         .scrollContentBackground(.hidden)
         .contentMargins(.bottom, 18, for: .scrollContent)
+        .refreshable {
+            await synchronizeFieldRecords()
+        }
     }
 
     @MainActor
@@ -155,14 +160,24 @@ struct SessionListView: View {
 
     @ViewBuilder
     private var emptyState: some View {
-        if sessions.isEmpty {
-            Button(action: onStartRecording) {
-                emptyStateContent
-                    .contentShape(Rectangle())
+        GeometryReader { proxy in
+            ScrollView {
+                Group {
+                    if sessions.isEmpty {
+                        Button(action: onStartRecording) {
+                            emptyStateContent
+                                .contentShape(Rectangle())
+                        }
+                        .buttonStyle(.plain)
+                    } else {
+                        emptyStateContent
+                    }
+                }
+                .frame(minHeight: proxy.size.height)
+            }
+            .refreshable {
+                await synchronizeFieldRecords()
             }
-            .buttonStyle(.plain)
-        } else {
-            emptyStateContent
         }
     }
 
@@ -189,6 +204,16 @@ struct SessionListView: View {
         .frame(maxWidth: .infinity, maxHeight: .infinity)
         .padding(.bottom, 60)
     }
+
+    @MainActor
+    private func synchronizeFieldRecords() async {
+        guard let userID = authManager.currentUser?.id else { return }
+        _ = await SyncManager.shared.sync(
+            sessions: sessions,
+            modelContext: modelContext,
+            userID: userID
+        )
+    }
 }
 
 private struct SessionSearchModifier: ViewModifier {

+ 1 - 0
generate_project.rb

@@ -48,6 +48,7 @@ app_target.build_configurations.each do |config|
   config.build_settings['INFOPLIST_KEY_CFBundleDisplayName'] = '星痕 CelestiaTrace'
   config.build_settings['INFOPLIST_KEY_UISupportedInterfaceOrientations'] = 'UIInterfaceOrientationPortrait'
   config.build_settings['INFOPLIST_KEY_NSMicrophoneUsageDescription'] = '需要麦克风权限以进行会议现场的音轨录音'
+  config.build_settings['INFOPLIST_KEY_NSLocationWhenInUseUsageDescription'] = '需要定位权限以在笔记和照片记录中插入所在位置'
   config.build_settings['INFOPLIST_KEY_NSCameraUsageDescription'] = '需要相机权限以在现场录制中拍摄照片记录'
   config.build_settings['INFOPLIST_KEY_NSPhotoLibraryUsageDescription'] = '需要相册权限以选择照片记录'
   config.build_settings['INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription'] = '需要蓝牙权限以扫描和绑定 BLE 录音设备'