Răsfoiți Sursa

feat(recording): 优化录音笔记打点时间锚定与界面交互

bob.yuxinyang 1 lună în urmă
părinte
comite
e3cb739280

+ 9 - 4
CelestiaTrace/Views/Home/HomeView.swift

@@ -109,10 +109,15 @@ struct HomeView: View {
                 beginNewRecording()
             }
 
-            Text("开始")
-                .font(.system(size: 16, weight: .medium))
-                .foregroundStyle(Color.primary.opacity(0.88))
-                .tracking(0.4)
+            Button {
+                beginNewRecording()
+            } label: {
+                Text("开始")
+                    .font(.system(size: 16, weight: .medium))
+                    .foregroundStyle(Color.primary.opacity(0.88))
+                    .tracking(0.4)
+            }
+            .buttonStyle(.plain)
         }
     }
 

+ 122 - 81
CelestiaTrace/Views/Recording/ActiveRecordingView.swift

@@ -18,6 +18,7 @@ struct ActiveRecordingView: View {
     @State private var showNoteSheet = false
     @State private var showCamera = false
     @State private var noteText = ""
+    @State private var pendingNoteTimeMs: Double = 0
     @State private var latestEvent: CelestiaTimelineEvent?
     @State private var latestEventVisible = false
     @State private var isEndingRecording = false
@@ -25,7 +26,6 @@ struct ActiveRecordingView: View {
     @State private var recordName: String
     @State private var showRecordNameEditor = false
     @State private var hasStartedLiveActivity = false
-    @FocusState private var isRecordNameFocused: Bool
 
     init(
         session: CelestiaSession,
@@ -190,61 +190,11 @@ struct ActiveRecordingView: View {
     }
 
     private var recordNameEditorSheet: some View {
-        NavigationStack {
-            ZStack {
-                Color.spaceBlack.ignoresSafeArea()
-
-                VStack(alignment: .leading, spacing: 10) {
-                    Text("记录名称")
-                        .font(.system(size: 10, weight: .semibold, design: .monospaced))
-                        .foregroundStyle(Color.secondary)
-                        .tracking(1.4)
-
-                    TextField("输入记录名称", text: $recordName)
-                        .focused($isRecordNameFocused)
-                        .font(.system(size: 18, weight: .semibold))
-                        .foregroundStyle(Color.primary)
-                        .textInputAutocapitalization(.never)
-                        .autocorrectionDisabled()
-                        .submitLabel(.done)
-                        .padding(.horizontal, 14)
-                        .padding(.vertical, 12)
-                        .background(Color.cardBackground.opacity(0.5))
-                        .businessBorder(cornerRadius: 8)
-                        .onSubmit {
-                            saveRecordNameAndCloseEditor()
-                        }
-
-                    Spacer()
-                }
-                .padding(20)
-            }
-            .navigationTitle("修改记录名称")
-            .navigationBarTitleDisplayMode(.inline)
-            .toolbar {
-                ToolbarItem(placement: .cancellationAction) {
-                    Button("取消") {
-                        recordName = session.title
-                        showRecordNameEditor = false
-                    }
-                    .foregroundStyle(Color.secondary)
-                }
-
-                ToolbarItem(placement: .confirmationAction) {
-                    Button("保存") {
-                        saveRecordNameAndCloseEditor()
-                    }
-                    .disabled(recordName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
-                }
-            }
-        }
-        .presentationDetents([.height(190)])
-        .presentationDragIndicator(.visible)
-        .presentationBackground(Color.spaceBlack)
-        .onAppear {
-            DispatchQueue.main.async {
-                isRecordNameFocused = true
-            }
+        RecordNameEditorSheet(recordName: $recordName) {
+            saveRecordNameAndCloseEditor()
+        } onCancel: {
+            recordName = session.title
+            showRecordNameEditor = false
         }
     }
 
@@ -385,7 +335,7 @@ struct ActiveRecordingView: View {
                 icon: "doc.text",
                 label: "文字笔记"
             ) {
-                showNoteSheet = true
+                presentNoteSheet()
             }
         }
     }
@@ -483,32 +433,37 @@ struct ActiveRecordingView: View {
             ZStack {
                 Color.spaceBlack.ignoresSafeArea()
 
-                VStack(spacing: 20) {
-                    TextField("输入笔记内容...", text: $noteText, axis: .vertical)
-                        .textFieldStyle(.plain)
-                        .font(.system(size: 15))
-                        .foregroundStyle(Color.primary)
-                        .padding(14)
-                        .frame(minHeight: 120, alignment: .top)
-                        .background(Color.cardBackground.opacity(0.5))
-                        .businessBorder(cornerRadius: 8)
+                VStack(alignment: .leading, spacing: 16) {
+                    Label(
+                        "添加到 \(formattedNoteTime(pendingNoteTimeMs))",
+                        systemImage: "clock"
+                    )
+                    .font(.system(size: 12, weight: .medium))
+                    .foregroundStyle(Color.secondary)
 
-                    Button {
-                        addNote()
-                    } label: {
-                        Text("保存笔记")
-                            .font(.system(size: 14, weight: .semibold))
-                            .foregroundStyle(Color.spaceBlack)
-                            .frame(maxWidth: .infinity)
-                            .padding(.vertical, 12)
-                            .background(Color.primary)
-                            .cornerRadius(8)
+                    ZStack(alignment: .topLeading) {
+                        if noteText.isEmpty {
+                            Text("输入笔记内容…")
+                                .font(.system(size: 15))
+                                .foregroundStyle(Color.secondary.opacity(0.6))
+                                .padding(.horizontal, 18)
+                                .padding(.vertical, 16)
+                                .allowsHitTesting(false)
+                        }
+
+                        TextEditor(text: $noteText)
+                            .font(.system(size: 15))
+                            .foregroundStyle(Color.primary)
+                            .scrollContentBackground(.hidden)
+                            .padding(10)
+                            .frame(minHeight: 160)
+                            .background(Color.cardBackground.opacity(0.5))
+                            .businessBorder(cornerRadius: 8)
                     }
-                    .disabled(noteText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
 
                     Spacer()
                 }
-                .padding(24)
+                .padding(20)
             }
             .navigationTitle("添加笔记")
             .navigationBarTitleDisplayMode(.inline)
@@ -518,7 +473,13 @@ struct ActiveRecordingView: View {
                         showNoteSheet = false
                         noteText = ""
                     }
-                    .foregroundStyle(Color.secondary)
+                }
+
+                ToolbarItem(placement: .confirmationAction) {
+                    Button("保存") {
+                        addNote()
+                    }
+                    .disabled(noteText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
                 }
             }
         }
@@ -553,6 +514,7 @@ struct ActiveRecordingView: View {
         guard !text.isEmpty else { return }
 
         let event = recordingVM.addNoteEvent(to: session, text: text)
+        event.relativeTimeMs = Int64(pendingNoteTimeMs.rounded())
 
         HapticManager.trigger(.noteAdded)
         showLatestEvent(event)
@@ -561,6 +523,24 @@ struct ActiveRecordingView: View {
         showNoteSheet = false
     }
 
+    private func presentNoteSheet() {
+        pendingNoteTimeMs = max(0, recordingVM.elapsedTime * 1_000)
+        noteText = ""
+        showNoteSheet = true
+    }
+
+    private func formattedNoteTime(_ timeMs: Double) -> String {
+        let totalSeconds = max(0, Int(timeMs / 1_000))
+        let hours = totalSeconds / 3_600
+        let minutes = (totalSeconds % 3_600) / 60
+        let seconds = totalSeconds % 60
+
+        if hours > 0 {
+            return String(format: "%d:%02d:%02d", hours, minutes, seconds)
+        }
+        return String(format: "%02d:%02d", minutes, seconds)
+    }
+
     private func toggleRecordingPause() {
         guard !isEndingRecording else { return }
         HapticManager.trigger(.tapFeedback)
@@ -671,7 +651,6 @@ struct ActiveRecordingView: View {
     private func saveRecordNameAndCloseEditor() {
         commitRecordName()
         guard !recordName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return }
-        isRecordNameFocused = false
         showRecordNameEditor = false
     }
 
@@ -687,13 +666,75 @@ struct ActiveRecordingView: View {
         case "photo":
             capturePhoto()
         case "note":
-            showNoteSheet = true
+            presentNoteSheet()
         default:
             break
         }
     }
 }
 
+struct RecordNameEditorSheet: View {
+    @Binding var recordName: String
+    let onSave: () -> Void
+    let onCancel: () -> Void
+
+    @FocusState private var isRecordNameFocused: Bool
+
+    var body: some View {
+        NavigationStack {
+            ZStack {
+                Color.spaceBlack.ignoresSafeArea()
+
+                VStack(alignment: .leading, spacing: 10) {
+                    Text("记录名称")
+                        .font(.system(size: 10, weight: .semibold, design: .monospaced))
+                        .foregroundStyle(Color.secondary)
+                        .tracking(1.4)
+
+                    TextField("输入记录名称", text: $recordName)
+                        .focused($isRecordNameFocused)
+                        .font(.system(size: 18, weight: .semibold))
+                        .foregroundStyle(Color.primary)
+                        .textInputAutocapitalization(.never)
+                        .autocorrectionDisabled()
+                        .submitLabel(.done)
+                        .padding(.horizontal, 14)
+                        .padding(.vertical, 12)
+                        .background(Color.cardBackground.opacity(0.5))
+                        .businessBorder(cornerRadius: 8)
+                        .onSubmit {
+                            onSave()
+                        }
+
+                    Spacer()
+                }
+                .padding(20)
+            }
+            .navigationTitle("修改记录名称")
+            .navigationBarTitleDisplayMode(.inline)
+            .toolbar {
+                ToolbarItem(placement: .cancellationAction) {
+                    Button("取消", action: onCancel)
+                        .foregroundStyle(Color.secondary)
+                }
+
+                ToolbarItem(placement: .confirmationAction) {
+                    Button("保存", action: onSave)
+                        .disabled(recordName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
+                }
+            }
+        }
+        .presentationDetents([.height(190)])
+        .presentationDragIndicator(.visible)
+        .presentationBackground(Color.spaceBlack)
+        .onAppear {
+            DispatchQueue.main.async {
+                isRecordNameFocused = true
+            }
+        }
+    }
+}
+
 // MARK: - Amplitude Level Meter View
 
 /// A minimalist graphical VU meter bar visualizing real-time audio amplitude.