// // Copyright 2025 Element Creations Ltd. // Copyright 2023-2025 New Vector Ltd. // // SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. // Please see LICENSE files in the repository root for full details. // import Compound import DSWaveformImage import DSWaveformImageViews import Foundation import SwiftUI struct VoiceMessagePreviewComposer: View { @ObservedObject var playerState: AudioPlayerState let waveform: WaveformSource @ScaledMetric private var waveformLineWidth = 2.0 @ScaledMetric private var waveformLinePadding = 2.0 @GestureState var isDragging = false let onPlay: () -> Void let onPause: () -> Void let onSeek: (Double) -> Void let onScrubbing: (Bool) -> Void var timeLabelContent: String { // Display the duration if progress is 0.0 let percent = playerState.progress > 0.0 ? playerState.progress : 1.0 // If the duration is greater or equal 10 minutes, use the long format let elapsed = Date(timeIntervalSinceReferenceDate: playerState.duration * percent) return DateFormatter.elapsedTimeFormatter.string(from: elapsed) } var body: some View { HStack(spacing: 8) { VoiceMessageButton(state: .init(playerState.playerButtonPlaybackState), size: .small, action: onPlayPause) Text(timeLabelContent) .lineLimit(1) .font(.compound.bodySMSemibold) .foregroundColor(.compound.textSecondary) .monospacedDigit() .fixedSize(horizontal: true, vertical: true) waveformView .waveformInteraction(isDragging: $isDragging, progress: playerState.progress, showCursor: playerState.showProgressIndicator, onSeek: onSeek) } .onChange(of: isDragging) { _, newValue in onScrubbing(newValue) } .padding(.vertical, Compound.supportsGlass ? 7 : 4) .padding(.horizontal, Compound.supportsGlass ? 8 : 6) .padding(.trailing, Compound.supportsGlass ? 8 : 0) .background { RoundedRectangle(cornerRadius: Compound.supportsGlass ? 21 : 12) .fill(.compound.bgSubtleSecondary) } } @ViewBuilder private var waveformView: some View { let configuration: Waveform.Configuration = .init(style: .striped(.init(color: .black, width: waveformLineWidth, spacing: waveformLinePadding)), verticalScalingFactor: 1.0) switch waveform { case .url(let url): WaveformView(audioURL: url, configuration: configuration) .progressMask(progress: playerState.progress) case .data(let array): WaveformLiveCanvas(samples: array, configuration: configuration) .progressMask(progress: playerState.progress) } } private func onPlayPause() { if playerState.playbackState == .playing { onPause() } else { onPlay() } } } private extension DateFormatter { static let elapsedTimeFormatter: DateFormatter = { let dateFormatter = DateFormatter() dateFormatter.dateFormat = "mm:ss" return dateFormatter }() } // MARK: - Previews struct VoiceMessagePreviewComposer_Previews: PreviewProvider, TestablePreview { static let playerState = AudioPlayerState(id: .recorderPreview, title: L10n.commonVoiceMessage, duration: 10.0, waveform: EstimatedWaveform.mockWaveform, progress: 0.4) static let waveformData: [Float] = Array(repeating: 1.0, count: 1000) static var previews: some View { VoiceMessagePreviewComposer(playerState: playerState, waveform: .data(waveformData), onPlay: { }, onPause: { }, onSeek: { _ in }, onScrubbing: { _ in }) .fixedSize(horizontal: false, vertical: true) } }