| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118 |
- import SwiftUI
- /// Interactive waveform display with playback progress and seek.
- struct WaveformView: View {
- let samples: [WaveformGenerator.WaveformSample]
- let progress: Double
- let isLoading: Bool
- let onSeek: (Double) -> Void
- @State private var hoveredProgress: Double?
- @State private var isDragging = false
- var body: some View {
- GeometryReader { geometry in
- ZStack {
- if isLoading {
- ProgressView("Generating waveform...")
- .font(.caption)
- } else if samples.isEmpty {
- RoundedRectangle(cornerRadius: 4)
- .fill(.quaternary)
- } else {
- // Waveform canvas
- Canvas { context, size in
- drawWaveform(context: context, size: size)
- }
- .gesture(waveformGesture(width: geometry.size.width))
- .onHover { hovering in
- if !hovering { hoveredProgress = nil }
- }
- .onContinuousHover { phase in
- switch phase {
- case .active(let location):
- hoveredProgress = location.x / geometry.size.width
- case .ended:
- hoveredProgress = nil
- @unknown default:
- break
- }
- }
- // Hover indicator
- if let hovered = hoveredProgress, !isDragging {
- Rectangle()
- .fill(.white.opacity(0.3))
- .frame(width: 1)
- .position(x: hovered * geometry.size.width, y: geometry.size.height / 2)
- .allowsHitTesting(false)
- }
- }
- }
- .clipShape(RoundedRectangle(cornerRadius: 6))
- .background(
- RoundedRectangle(cornerRadius: 6)
- .fill(.black.opacity(0.3))
- )
- }
- }
- // MARK: - Drawing
- private func drawWaveform(context: GraphicsContext, size: CGSize) {
- let width = size.width
- let height = size.height
- let midY = height / 2
- let count = samples.count
- guard count > 0 else { return }
- let playedIndex = Int(progress * Double(count))
- // Draw each column
- for (index, sample) in samples.enumerated() {
- let x = CGFloat(index) / CGFloat(count) * width
- let barWidth = max(1, width / CGFloat(count))
- let maxHeight = CGFloat(sample.max) * midY
- let minHeight = CGFloat(sample.min) * midY
- let rect = CGRect(
- x: x,
- y: midY - maxHeight,
- width: barWidth,
- height: maxHeight - minHeight
- )
- let color: Color
- if index < playedIndex {
- color = .accentColor
- } else {
- color = .gray.opacity(0.5)
- }
- context.fill(Path(rect), with: .color(color))
- }
- // Playhead line
- let playheadX = progress * width
- var playheadPath = Path()
- playheadPath.move(to: CGPoint(x: playheadX, y: 0))
- playheadPath.addLine(to: CGPoint(x: playheadX, y: height))
- context.stroke(playheadPath, with: .color(.white), lineWidth: 1.5)
- }
- // MARK: - Gesture
- private func waveformGesture(width: CGFloat) -> some Gesture {
- DragGesture(minimumDistance: 0)
- .onChanged { value in
- isDragging = true
- let prog = max(0, min(1, value.location.x / width))
- onSeek(prog)
- }
- .onEnded { _ in
- isDragging = false
- }
- }
- }
|