Files
letro-ios/ElementX/Sources/Screens/Timeline/View/VoiceMessageRoomPlaybackView.swift
2025-03-28 10:15:30 +01:00

135 lines
5.2 KiB
Swift

//
// Copyright 2023, 2024 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 DSWaveformImage
import DSWaveformImageViews
import SwiftUI
struct VoiceMessageRoomPlaybackView: View {
@ObservedObject var playerState: AudioPlayerState
@ScaledMetric private var waveformLineWidth = 2.0
@ScaledMetric private var waveformLinePadding = 2.0
@GestureState var isDragging = false
let onPlayPause: () -> Void
let onSeek: (Double) -> Void
let onScrubbing: (Bool) -> Void
var body: some View {
HStack(spacing: 8) {
VoiceMessageButton(state: .init(playerState.playerButtonPlaybackState),
size: .medium,
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)
}
.padding(.leading, 2)
.padding(.trailing, 8)
.onChange(of: isDragging) { _, newValue in
onScrubbing(newValue)
}
.accessibilityLabel(accessibilityLabel)
}
// MARK: - Private
private var durationString: String {
let duration = Date(timeIntervalSinceReferenceDate: playerState.duration)
return if playerState.duration >= 600 {
DateFormatter.longElapsedTimeFormatter.string(from: duration)
} else {
DateFormatter.elapsedTimeFormatter.string(from: duration)
}
}
private 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)
if playerState.duration >= 600 {
return DateFormatter.longElapsedTimeFormatter.string(from: elapsed)
} else {
return DateFormatter.elapsedTimeFormatter.string(from: elapsed)
}
}
private var accessibilityLabel: String {
if playerState.progress > 0.0 {
L10n.a11yPausedVoiceMessage(durationString, timeLabelContent)
} else {
L10n.a11yVoiceMessage(durationString)
}
}
@ViewBuilder
private var waveformView: some View {
if let url = playerState.fileURL {
WaveformView(audioURL: url,
configuration: .init(style: .striped(.init(color: .black, width: waveformLineWidth, spacing: waveformLinePadding)),
verticalScalingFactor: 1.0)) {
estimatedWaveformView
}
.progressMask(progress: playerState.progress)
} else {
estimatedWaveformView
}
}
private var estimatedWaveformView: some View {
EstimatedWaveformView(lineWidth: waveformLineWidth,
linePadding: waveformLinePadding,
waveform: playerState.waveform,
progress: playerState.progress)
}
}
private extension DateFormatter {
static let elapsedTimeFormatter: DateFormatter = {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "m:ss"
return dateFormatter
}()
static let longElapsedTimeFormatter: DateFormatter = {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "mm:ss"
return dateFormatter
}()
}
struct VoiceMessageRoomPlaybackView_Previews: PreviewProvider, TestablePreview {
static let waveform = EstimatedWaveform(data: [3, 127, 400, 266, 126, 122, 373, 251, 45, 112,
334, 205, 99, 138, 397, 354, 125, 361, 199, 51,
294, 131, 19, 2, 3, 3, 1, 2, 0, 0,
0, 0, 0, 0, 0, 3])
static var playerState = AudioPlayerState(id: .timelineItemIdentifier(.randomEvent),
title: L10n.commonVoiceMessage,
duration: 10.0,
waveform: waveform,
progress: 0.3)
static var previews: some View {
VoiceMessageRoomPlaybackView(playerState: playerState,
onPlayPause: { },
onSeek: { value in Task { await playerState.updateState(progress: value) } },
onScrubbing: { _ in })
.fixedSize(horizontal: false, vertical: true)
}
}