|
@@ -27,6 +27,9 @@ struct MultiTrackTimeline: View {
|
|
|
private let pointsPerSecond: CGFloat = 2.5
|
|
private let pointsPerSecond: CGFloat = 2.5
|
|
|
/// Minimum timeline width
|
|
/// Minimum timeline width
|
|
|
private let minimumWidth: CGFloat = 400
|
|
private let minimumWidth: CGFloat = 400
|
|
|
|
|
+ /// Matches the live-recording waveform height.
|
|
|
|
|
+ private let audioTrackHeight: CGFloat = 90
|
|
|
|
|
+ private let eventTrackHeight: CGFloat = 34
|
|
|
|
|
|
|
|
private var timelineWidth: CGFloat {
|
|
private var timelineWidth: CGFloat {
|
|
|
max(minimumWidth, CGFloat(totalDurationMs / 1000.0) * pointsPerSecond + 60)
|
|
max(minimumWidth, CGFloat(totalDurationMs / 1000.0) * pointsPerSecond + 60)
|
|
@@ -44,23 +47,23 @@ struct MultiTrackTimeline: View {
|
|
|
VStack(spacing: 0) {
|
|
VStack(spacing: 0) {
|
|
|
// Time Ruler
|
|
// Time Ruler
|
|
|
timeRuler
|
|
timeRuler
|
|
|
- .frame(height: 26)
|
|
|
|
|
|
|
+ .frame(height: 28)
|
|
|
|
|
|
|
|
// Track 1: Audio Waveform
|
|
// Track 1: Audio Waveform
|
|
|
audioTrack
|
|
audioTrack
|
|
|
- .frame(height: 38)
|
|
|
|
|
|
|
+ .frame(height: audioTrackHeight)
|
|
|
|
|
|
|
|
trackDivider
|
|
trackDivider
|
|
|
|
|
|
|
|
// Track 2: Photo Markers
|
|
// Track 2: Photo Markers
|
|
|
photoTrack
|
|
photoTrack
|
|
|
- .frame(height: 28)
|
|
|
|
|
|
|
+ .frame(height: eventTrackHeight)
|
|
|
|
|
|
|
|
trackDivider
|
|
trackDivider
|
|
|
|
|
|
|
|
// Track 3: Note Markers
|
|
// Track 3: Note Markers
|
|
|
noteTrack
|
|
noteTrack
|
|
|
- .frame(height: 28)
|
|
|
|
|
|
|
+ .frame(height: eventTrackHeight)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Playhead Cursor
|
|
// Playhead Cursor
|
|
@@ -189,6 +192,29 @@ struct MultiTrackTimeline: View {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
trackLeadingIcon(systemName: "waveform", accessibilityLabel: "音频轨道")
|
|
trackLeadingIcon(systemName: "waveform", accessibilityLabel: "音频轨道")
|
|
|
|
|
+
|
|
|
|
|
+ ForEach(continuationEvents) { event in
|
|
|
|
|
+ Button {
|
|
|
|
|
+ onEventTap?(event)
|
|
|
|
|
+ } label: {
|
|
|
|
|
+ ZStack {
|
|
|
|
|
+ Rectangle()
|
|
|
|
|
+ .fill(Color.primary.opacity(0.35))
|
|
|
|
|
+ .frame(width: 1, height: audioTrackHeight)
|
|
|
|
|
+
|
|
|
|
|
+ Image(systemName: "record.circle")
|
|
|
|
|
+ .font(.system(size: 11, weight: .medium))
|
|
|
|
|
+ .foregroundStyle(Color.primary)
|
|
|
|
|
+ .frame(width: 20, height: 20)
|
|
|
|
|
+ .background(Color.cardBackground.opacity(0.9))
|
|
|
|
|
+ .businessBorder(cornerRadius: 10)
|
|
|
|
|
+ }
|
|
|
|
|
+ .frame(width: 20, height: audioTrackHeight)
|
|
|
|
|
+ }
|
|
|
|
|
+ .buttonStyle(.plain)
|
|
|
|
|
+ .offset(x: xPosition(for: Double(event.relativeTimeMs)) - 10)
|
|
|
|
|
+ .accessibilityLabel("查看 \(event.relativeTimeFormatted) 的续录信息")
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -310,6 +336,9 @@ struct MultiTrackTimeline: View {
|
|
|
trackLabel(icon: "waveform", text: "音频")
|
|
trackLabel(icon: "waveform", text: "音频")
|
|
|
trackLabel(icon: "camera", text: "图片")
|
|
trackLabel(icon: "camera", text: "图片")
|
|
|
trackLabel(icon: "doc.text", text: "笔记")
|
|
trackLabel(icon: "doc.text", text: "笔记")
|
|
|
|
|
+ if !continuationEvents.isEmpty {
|
|
|
|
|
+ trackLabel(icon: "record.circle", text: "续录")
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -375,7 +404,14 @@ struct MultiTrackTimeline: View {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private var noteEvents: [CelestiaTimelineEvent] {
|
|
private var noteEvents: [CelestiaTimelineEvent] {
|
|
|
- events.filter { $0.eventType == "NOTE" || $0.eventType == "MARKER" }
|
|
|
|
|
|
|
+ events.filter {
|
|
|
|
|
+ $0.eventType == "NOTE"
|
|
|
|
|
+ || ($0.eventType == "MARKER" && !$0.isContinuationMarker)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private var continuationEvents: [CelestiaTimelineEvent] {
|
|
|
|
|
+ events.filter(\.isContinuationMarker)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|