|
@@ -18,6 +18,7 @@ struct ActiveRecordingView: View {
|
|
|
@State private var showNoteSheet = false
|
|
@State private var showNoteSheet = false
|
|
|
@State private var showCamera = false
|
|
@State private var showCamera = false
|
|
|
@State private var noteText = ""
|
|
@State private var noteText = ""
|
|
|
|
|
+ @State private var pendingNoteTimeMs: Double = 0
|
|
|
@State private var latestEvent: CelestiaTimelineEvent?
|
|
@State private var latestEvent: CelestiaTimelineEvent?
|
|
|
@State private var latestEventVisible = false
|
|
@State private var latestEventVisible = false
|
|
|
@State private var isEndingRecording = false
|
|
@State private var isEndingRecording = false
|
|
@@ -25,7 +26,6 @@ struct ActiveRecordingView: View {
|
|
|
@State private var recordName: String
|
|
@State private var recordName: String
|
|
|
@State private var showRecordNameEditor = false
|
|
@State private var showRecordNameEditor = false
|
|
|
@State private var hasStartedLiveActivity = false
|
|
@State private var hasStartedLiveActivity = false
|
|
|
- @FocusState private var isRecordNameFocused: Bool
|
|
|
|
|
|
|
|
|
|
init(
|
|
init(
|
|
|
session: CelestiaSession,
|
|
session: CelestiaSession,
|
|
@@ -190,61 +190,11 @@ struct ActiveRecordingView: View {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private var recordNameEditorSheet: some 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",
|
|
icon: "doc.text",
|
|
|
label: "文字笔记"
|
|
label: "文字笔记"
|
|
|
) {
|
|
) {
|
|
|
- showNoteSheet = true
|
|
|
|
|
|
|
+ presentNoteSheet()
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -483,32 +433,37 @@ struct ActiveRecordingView: View {
|
|
|
ZStack {
|
|
ZStack {
|
|
|
Color.spaceBlack.ignoresSafeArea()
|
|
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()
|
|
Spacer()
|
|
|
}
|
|
}
|
|
|
- .padding(24)
|
|
|
|
|
|
|
+ .padding(20)
|
|
|
}
|
|
}
|
|
|
.navigationTitle("添加笔记")
|
|
.navigationTitle("添加笔记")
|
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
@@ -518,7 +473,13 @@ struct ActiveRecordingView: View {
|
|
|
showNoteSheet = false
|
|
showNoteSheet = false
|
|
|
noteText = ""
|
|
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 }
|
|
guard !text.isEmpty else { return }
|
|
|
|
|
|
|
|
let event = recordingVM.addNoteEvent(to: session, text: text)
|
|
let event = recordingVM.addNoteEvent(to: session, text: text)
|
|
|
|
|
+ event.relativeTimeMs = Int64(pendingNoteTimeMs.rounded())
|
|
|
|
|
|
|
|
HapticManager.trigger(.noteAdded)
|
|
HapticManager.trigger(.noteAdded)
|
|
|
showLatestEvent(event)
|
|
showLatestEvent(event)
|
|
@@ -561,6 +523,24 @@ struct ActiveRecordingView: View {
|
|
|
showNoteSheet = false
|
|
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() {
|
|
private func toggleRecordingPause() {
|
|
|
guard !isEndingRecording else { return }
|
|
guard !isEndingRecording else { return }
|
|
|
HapticManager.trigger(.tapFeedback)
|
|
HapticManager.trigger(.tapFeedback)
|
|
@@ -671,7 +651,6 @@ struct ActiveRecordingView: View {
|
|
|
private func saveRecordNameAndCloseEditor() {
|
|
private func saveRecordNameAndCloseEditor() {
|
|
|
commitRecordName()
|
|
commitRecordName()
|
|
|
guard !recordName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return }
|
|
guard !recordName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return }
|
|
|
- isRecordNameFocused = false
|
|
|
|
|
showRecordNameEditor = false
|
|
showRecordNameEditor = false
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -687,13 +666,75 @@ struct ActiveRecordingView: View {
|
|
|
case "photo":
|
|
case "photo":
|
|
|
capturePhoto()
|
|
capturePhoto()
|
|
|
case "note":
|
|
case "note":
|
|
|
- showNoteSheet = true
|
|
|
|
|
|
|
+ presentNoteSheet()
|
|
|
default:
|
|
default:
|
|
|
break
|
|
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
|
|
// MARK: - Amplitude Level Meter View
|
|
|
|
|
|
|
|
/// A minimalist graphical VU meter bar visualizing real-time audio amplitude.
|
|
/// A minimalist graphical VU meter bar visualizing real-time audio amplitude.
|