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