diff --git a/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_slider_max_track.imageset/Contents.json b/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_slider_max_track.imageset/Contents.json
new file mode 100644
index 000000000..03c7aa158
--- /dev/null
+++ b/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_slider_max_track.imageset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "images" : [
+ {
+ "filename" : "voice_broadcast_slider_max_track.svg",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_slider_max_track.imageset/voice_broadcast_slider_max_track.svg b/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_slider_max_track.imageset/voice_broadcast_slider_max_track.svg
new file mode 100644
index 000000000..1730c4783
--- /dev/null
+++ b/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_slider_max_track.imageset/voice_broadcast_slider_max_track.svg
@@ -0,0 +1,3 @@
+
diff --git a/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_slider_min_track.imageset/Contents.json b/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_slider_min_track.imageset/Contents.json
new file mode 100644
index 000000000..42ea4f6e3
--- /dev/null
+++ b/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_slider_min_track.imageset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "images" : [
+ {
+ "filename" : "voice_broadcast_slider_min_track.svg",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_slider_min_track.imageset/voice_broadcast_slider_min_track.svg b/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_slider_min_track.imageset/voice_broadcast_slider_min_track.svg
new file mode 100644
index 000000000..5cb3d3427
--- /dev/null
+++ b/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_slider_min_track.imageset/voice_broadcast_slider_min_track.svg
@@ -0,0 +1,3 @@
+
diff --git a/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_slider_thumb.imageset/Contents.json b/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_slider_thumb.imageset/Contents.json
new file mode 100644
index 000000000..9904db687
--- /dev/null
+++ b/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_slider_thumb.imageset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "images" : [
+ {
+ "filename" : "voice_broadcast_slider_thumb.svg",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_slider_thumb.imageset/voice_broadcast_slider_thumb.svg b/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_slider_thumb.imageset/voice_broadcast_slider_thumb.svg
new file mode 100644
index 000000000..507831e8b
--- /dev/null
+++ b/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_slider_thumb.imageset/voice_broadcast_slider_thumb.svg
@@ -0,0 +1,3 @@
+
diff --git a/Riot/Generated/Images.swift b/Riot/Generated/Images.swift
index 1dc910c14..ed763a171 100644
--- a/Riot/Generated/Images.swift
+++ b/Riot/Generated/Images.swift
@@ -346,6 +346,9 @@ internal class Asset: NSObject {
internal static let voiceBroadcastPlay = ImageAsset(name: "voice_broadcast_play")
internal static let voiceBroadcastRecord = ImageAsset(name: "voice_broadcast_record")
internal static let voiceBroadcastRecordPause = ImageAsset(name: "voice_broadcast_record_pause")
+ internal static let voiceBroadcastSliderMaxTrack = ImageAsset(name: "voice_broadcast_slider_max_track")
+ internal static let voiceBroadcastSliderMinTrack = ImageAsset(name: "voice_broadcast_slider_min_track")
+ internal static let voiceBroadcastSliderThumb = ImageAsset(name: "voice_broadcast_slider_thumb")
internal static let voiceBroadcastSpinner = ImageAsset(name: "voice_broadcast_spinner")
internal static let voiceBroadcastStop = ImageAsset(name: "voice_broadcast_stop")
internal static let voiceBroadcastTileLive = ImageAsset(name: "voice_broadcast_tile_live")
diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/MatrixSDK/VoiceBroadcastPlaybackViewModel.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/MatrixSDK/VoiceBroadcastPlaybackViewModel.swift
index 61b450d77..000e3f450 100644
--- a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/MatrixSDK/VoiceBroadcastPlaybackViewModel.swift
+++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/MatrixSDK/VoiceBroadcastPlaybackViewModel.swift
@@ -58,6 +58,21 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic
private static let defaultBackwardForwardValue: Float = 30000.0 // 30sec in ms
+ private var fullDateFormatter: DateComponentsFormatter {
+ let formatter = DateComponentsFormatter()
+ formatter.unitsStyle = .positional
+ formatter.allowedUnits = [.hour, .minute, .second]
+ return formatter
+ }
+
+ private var shortDateFormatter: DateComponentsFormatter {
+ let formatter = DateComponentsFormatter()
+ formatter.unitsStyle = .positional
+ formatter.zeroFormattingBehavior = .pad
+ formatter.allowedUnits = [.minute, .second]
+ return formatter
+ }
+
// MARK: Public
// MARK: - Setup
@@ -330,12 +345,16 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic
private func updateDuration() {
let duration = voiceBroadcastAggregator.voiceBroadcast.duration
- let time = TimeInterval(duration / 1000)
- let formatter = DateComponentsFormatter()
- formatter.unitsStyle = .abbreviated
-
state.playingState.duration = Float(duration)
- state.playingState.durationLabel = formatter.string(from: time)
+ updateUI()
+ }
+
+ private func dateFormatter(for time: TimeInterval) -> DateComponentsFormatter {
+ if time >= 3600 {
+ return self.fullDateFormatter
+ } else {
+ return self.shortDateFormatter
+ }
}
private func didSliderChanged(_ didChange: Bool) {
@@ -368,6 +387,21 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic
}
private func updateUI() {
+ let time = TimeInterval(state.playingState.duration / 1000)
+ let formatter = dateFormatter(for: time)
+
+ let currentProgress = TimeInterval(state.bindings.progress / 1000)
+ state.playingState.elapsedTimeLabel = formatter.string(from: currentProgress)
+ if let remainingTimeString = formatter.string(from: time-currentProgress) {
+ if time-currentProgress < 1.0 {
+ state.playingState.remainingTimeLabel = remainingTimeString
+ } else {
+ state.playingState.remainingTimeLabel = "-" + remainingTimeString
+ }
+ } else {
+ state.playingState.remainingTimeLabel = ""
+ }
+
state.playingState.canMoveBackward = state.bindings.progress > 0
state.playingState.canMoveForward = state.bindings.progress < state.playingState.duration
}
diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/View/VoiceBroadcastPlaybackView.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/View/VoiceBroadcastPlaybackView.swift
index ff36b0e36..c25e1c995 100644
--- a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/View/VoiceBroadcastPlaybackView.swift
+++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/View/VoiceBroadcastPlaybackView.swift
@@ -152,15 +152,23 @@ struct VoiceBroadcastPlaybackView: View {
}
}
- Slider(value: $viewModel.progress, in: 0...viewModel.viewState.playingState.duration) {
- Text("Slider")
- } minimumValueLabel: {
- Text("")
- } maximumValueLabel: {
- Text(viewModel.viewState.playingState.durationLabel ?? "").font(.body)
- } onEditingChanged: { didChange in
+ VoiceBroadcastSlider(value: $viewModel.progress,
+ minValue: 0.0,
+ maxValue: viewModel.viewState.playingState.duration) { didChange in
viewModel.send(viewAction: .sliderChange(didChange: didChange))
}
+
+ HStack {
+ Text(viewModel.viewState.playingState.elapsedTimeLabel ?? "")
+ .foregroundColor(theme.colors.secondaryContent)
+ .font(theme.fonts.caption1)
+ .padding(EdgeInsets(top: -4.0, leading: 4.0, bottom: 0.0, trailing: 0.0))
+ Spacer()
+ Text(viewModel.viewState.playingState.remainingTimeLabel ?? "")
+ .foregroundColor(theme.colors.secondaryContent)
+ .font(theme.fonts.caption1)
+ .padding(EdgeInsets(top: -4.0, leading: 0.0, bottom: 0.0, trailing: 4.0))
+ }
}
.padding([.horizontal, .top], 2.0)
.padding([.bottom])
diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/View/VoiceBroadcastSlider.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/View/VoiceBroadcastSlider.swift
new file mode 100644
index 000000000..50845d5c8
--- /dev/null
+++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/View/VoiceBroadcastSlider.swift
@@ -0,0 +1,69 @@
+//
+// Copyright 2022 New Vector Ltd
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+import SwiftUI
+
+/// Customized UISlider for SwiftUI.
+
+struct VoiceBroadcastSlider: UIViewRepresentable {
+ @Binding var value: Float
+
+ var minValue: Float = 0.0
+ var maxValue: Float = 1.0
+ var onEditingChanged : ((Bool) -> Void)?
+
+ func makeUIView(context: Context) -> UISlider {
+ let slider = UISlider(frame: .zero)
+ slider.setThumbImage(Asset.Images.voiceBroadcastSliderThumb.image, for: .normal)
+ slider.setMinimumTrackImage(Asset.Images.voiceBroadcastSliderMinTrack.image, for: .normal)
+ slider.setMaximumTrackImage(Asset.Images.voiceBroadcastSliderMaxTrack.image, for: .normal)
+ slider.minimumValue = Float(minValue)
+ slider.maximumValue = Float(maxValue)
+ slider.value = Float(value)
+ slider.addTarget(context.coordinator, action: #selector(Coordinator.valueChanged(_:)), for: .valueChanged)
+ slider.addTarget(context.coordinator, action: #selector(Coordinator.sliderEditingChanged(_:)), for: .touchUpInside)
+ slider.addTarget(context.coordinator, action: #selector(Coordinator.sliderEditingChanged(_:)), for: .touchUpOutside)
+ slider.addTarget(context.coordinator, action: #selector(Coordinator.sliderEditingChanged(_:)), for: .touchDown)
+
+ return slider
+ }
+
+ func updateUIView(_ uiView: UISlider, context: Context) {
+ uiView.value = Float(value)
+ }
+
+ func makeCoordinator() -> VoiceBroadcastSlider.Coordinator {
+ Coordinator(parent: self, value: $value)
+ }
+
+ class Coordinator: NSObject {
+ var parent: VoiceBroadcastSlider
+ var value: Binding
+
+ init(parent: VoiceBroadcastSlider, value: Binding) {
+ self.value = value
+ self.parent = parent
+ }
+
+ @objc func valueChanged(_ sender: UISlider) {
+ self.value.wrappedValue = sender.value
+ }
+
+ @objc func sliderEditingChanged(_ sender: UISlider) {
+ parent.onEditingChanged?(sender.isTracking)
+ }
+ }
+}
diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/VoiceBroadcastPlaybackModels.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/VoiceBroadcastPlaybackModels.swift
index 5e5c6696c..c2ca1ad5f 100644
--- a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/VoiceBroadcastPlaybackModels.swift
+++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/VoiceBroadcastPlaybackModels.swift
@@ -40,7 +40,8 @@ struct VoiceBroadcastPlaybackDetails {
struct VoiceBroadcastPlayingState {
var duration: Float
- var durationLabel: String?
+ var elapsedTimeLabel: String?
+ var remainingTimeLabel: String?
var isLive: Bool
var canMoveForward: Bool
var canMoveBackward: Bool