Add audio rate control in overlay player

This commit is contained in:
Ilya Laktyushin 2023-03-01 17:37:07 +04:00
parent 685c3f95a2
commit 7d8266ff89
5 changed files with 170 additions and 66 deletions

View File

@ -155,7 +155,7 @@ public final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, UIScrollVi
private let closeButton: HighlightableButtonNode
private let actionButton: HighlightTrackingButtonNode
private let playPauseIconNode: PlayPauseIconNode
private let rateButton: RateButton
private let rateButton: AudioRateButton
private let accessibilityAreaNode: AccessibilityAreaNode
private let scrubbingNode: MediaPlayerScrubbingNode
@ -241,8 +241,7 @@ public final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, UIScrollVi
self.closeButton.contentEdgeInsets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 2.0)
self.closeButton.displaysAsynchronously = false
self.rateButton = RateButton()
self.rateButton = AudioRateButton()
self.rateButton.hitTestSlop = UIEdgeInsets(top: -8.0, left: -4.0, bottom: -8.0, right: -4.0)
self.rateButton.displaysAsynchronously = false
@ -669,20 +668,20 @@ private func optionsRateImage(rate: String, color: UIColor = .white) -> UIImage?
})
}
private final class RateButton: HighlightableButtonNode {
enum Content {
public final class AudioRateButton: HighlightableButtonNode {
public enum Content {
case image(UIImage?)
}
let referenceNode: ContextReferenceContentNode
public let referenceNode: ContextReferenceContentNode
let containerNode: ContextControllerSourceNode
private let iconNode: ASImageNode
var contextAction: ((ASDisplayNode, ContextGesture?) -> Void)?
public var contextAction: ((ASDisplayNode, ContextGesture?) -> Void)?
private let wide: Bool
init(wide: Bool = false) {
public init(wide: Bool = false) {
self.wide = wide
self.referenceNode = ContextReferenceContentNode()
@ -723,7 +722,7 @@ private final class RateButton: HighlightableButtonNode {
}
private var content: Content?
func setContent(_ content: Content, animated: Bool = false) {
public func setContent(_ content: Content, animated: Bool = false) {
if animated {
if let snapshotView = self.referenceNode.view.snapshotContentTree() {
snapshotView.frame = self.referenceNode.frame
@ -761,12 +760,12 @@ private final class RateButton: HighlightableButtonNode {
}
}
override func didLoad() {
public override func didLoad() {
super.didLoad()
self.view.isOpaque = false
}
override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
public override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
return CGSize(width: wide ? 32.0 : 22.0, height: 44.0)
}

View File

@ -358,6 +358,7 @@ swift_library(
"//submodules/DrawingUI:DrawingUI",
"//submodules/FeaturedStickersScreen:FeaturedStickersScreen",
"//submodules/TelegramUI/Components/SendInviteLinkScreen",
"//submodules/TelegramUI/Components/SliderContextItem:SliderContextItem",
] + select({
"@build_bazel_rules_apple//apple:ios_armv7": [],
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,

View File

@ -120,6 +120,9 @@ final class OverlayAudioPlayerControllerImpl: ViewController, OverlayAudioPlayer
strongSelf.dismiss()
}
})
self.controllerNode.getParentController = { [weak self] in
return self
}
self.ready.set(self.controllerNode.ready.get())

View File

@ -45,6 +45,12 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu
private var presentationDataDisposable: Disposable?
private let replacementHistoryNodeReadyDisposable = MetaDisposable()
var getParentController: () -> ViewController? = { return nil } {
didSet {
self.controlsNode.getParentController = self.getParentController
}
}
init(context: AccountContext, chatLocation: ChatLocation, type: MediaManagerPlayerType, initialMessageId: MessageId, initialOrder: MusicPlaybackSettingsOrder, playlistLocation: SharedMediaPlaylistLocation?, requestDismiss: @escaping () -> Void, requestShare: @escaping (MessageId) -> Void, requestSearchByArtist: @escaping (String) -> Void) {
self.context = context
self.chatLocation = chatLocation

View File

@ -13,6 +13,10 @@ import PhotoResources
import AppBundle
import ManagedAnimationNode
import RangeSet
import TelegramBaseController
import ContextUI
import SliderContextItem
import UndoUI
private func generateBackground(theme: PresentationTheme) -> UIImage? {
return generateImage(CGSize(width: 20.0, height: 10.0 + 8.0), rotatedContext: { size, context in
@ -73,51 +77,6 @@ private func optionsRateImage(rate: String, color: UIColor = .white) -> UIImage?
})
}
//private func optionsRateImage(rate: String, color: UIColor = .white) -> UIImage? {
// return generateImage(CGSize(width: 36.0, height: 16.0), rotatedContext: { size, context in
// UIGraphicsPushContext(context)
//
// context.clear(CGRect(origin: CGPoint(), size: size))
//
// let lineWidth = 1.0 + UIScreenPixel
// context.setLineWidth(lineWidth)
// context.setStrokeColor(color.cgColor)
//
//
// let string = NSMutableAttributedString(string: rate, font: Font.with(size: 11.0, design: .round, weight: .bold), textColor: color)
//
// var offset = CGPoint(x: 1.0, y: 0.0)
// var width: CGFloat
// if rate.count >= 5 {
// string.addAttribute(.kern, value: -0.8 as NSNumber, range: NSRange(string.string.startIndex ..< string.string.endIndex, in: string.string))
// offset.x += -0.5
// width = 33.0
// } else if rate.count >= 3 {
// if rate == "0.5X" {
// string.addAttribute(.kern, value: -0.8 as NSNumber, range: NSRange(string.string.startIndex ..< string.string.endIndex, in: string.string))
// offset.x += -0.5
// } else {
// string.addAttribute(.kern, value: -0.5 as NSNumber, range: NSRange(string.string.startIndex ..< string.string.endIndex, in: string.string))
// offset.x += -0.3
// }
// width = 29.0
// } else {
// string.addAttribute(.kern, value: -0.5 as NSNumber, range: NSRange(string.string.startIndex ..< string.string.endIndex, in: string.string))
// width = 19.0
// offset.x += -0.3
// }
//
// let path = UIBezierPath(roundedRect: CGRect(x: floorToScreenPixels((size.width - width) / 2.0), y: 0.0, width: width, height: 16.0).insetBy(dx: lineWidth / 2.0, dy: lineWidth / 2.0), byRoundingCorners: .allCorners, cornerRadii: CGSize(width: 2.0, height: 2.0))
// context.addPath(path.cgPath)
// context.strokePath()
//
// let boundingRect = string.boundingRect(with: size, options: [], context: nil)
// string.draw(at: CGPoint(x: offset.x + floor((size.width - boundingRect.width) / 2.0), y: offset.y + UIScreenPixel + floor((size.height - boundingRect.height) / 2.0)))
//
// UIGraphicsPopContext()
// })
//}
private let digitsSet = CharacterSet(charactersIn: "0123456789")
private func timestampLabelWidthForDuration(_ timestamp: Double) -> CGFloat {
let text: String
@ -173,7 +132,7 @@ private func stringsForDisplayData(_ data: SharedMediaPlaybackDisplayData?, pres
final class OverlayPlayerControlsNode: ASDisplayNode {
private let accountManager: AccountManager<TelegramAccountManagerTypes>
private let postbox: Postbox
private let account: Account
private let engine: TelegramEngine
private var presentationData: PresentationData
@ -211,7 +170,7 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
private let loopingButton: IconButtonNode
private var currentRate: AudioPlaybackRate?
private let rateButton: HighlightableButtonNode
private let rateButton: AudioRateButton
let separatorNode: ASDisplayNode
@ -225,6 +184,8 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
var updateOrder: ((MusicPlaybackSettingsOrder) -> Void)?
var control: ((SharedMediaPlayerControlAction) -> Void)?
var getParentController: () -> ViewController? = { return nil }
private(set) var currentItemId: SharedMediaPlaylistItemId?
private var displayData: SharedMediaPlaybackDisplayData?
private var currentAlbumArtInitialized = false
@ -251,7 +212,7 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
init(account: Account, engine: TelegramEngine, accountManager: AccountManager<TelegramAccountManagerTypes>, presentationData: PresentationData, status: Signal<(Account, SharedMediaPlayerItemPlaybackStateOrLoading, MediaManagerPlayerType)?, NoError>) {
self.accountManager = accountManager
self.postbox = account.postbox
self.account = account
self.engine = engine
self.presentationData = presentationData
@ -295,7 +256,7 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
self.infoNode.isUserInteractionEnabled = false
self.infoNode.displaysAsynchronously = false
self.rateButton = HighlightableButtonNode()
self.rateButton = AudioRateButton()
self.rateButton.hitTestSlop = UIEdgeInsets(top: -8.0, left: -4.0, bottom: -8.0, right: -4.0)
self.rateButton.displaysAsynchronously = false
@ -500,7 +461,7 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
canShare = !isCopyProtected
strongSelf.currentFileReference = fileReference
if let size = fileReference.media.size {
strongSelf.scrubberNode.bufferingStatus = strongSelf.postbox.mediaBox.resourceRangesStatus(fileReference.media.resource)
strongSelf.scrubberNode.bufferingStatus = strongSelf.account.postbox.mediaBox.resourceRangesStatus(fileReference.media.resource)
|> map { ranges -> (RangeSet<Int64>, Int64) in
return (ranges, size)
}
@ -595,6 +556,10 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
}
}
self.rateButton.contextAction = { [weak self] sourceNode, gesture in
self?.openRateMenu(sourceNode: sourceNode, gesture: gesture)
}
self.playPauseButton.circleColor = presentationData.theme.list.controlSecondaryColor.withAlphaComponent(0.35)
self.backwardButton.circleColor = presentationData.theme.list.controlSecondaryColor.withAlphaComponent(0.35)
self.forwardButton.circleColor = presentationData.theme.list.controlSecondaryColor.withAlphaComponent(0.35)
@ -782,9 +747,9 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
if self.currentAlbumArt != albumArt || !self.currentAlbumArtInitialized {
self.currentAlbumArtInitialized = true
self.currentAlbumArt = albumArt
self.albumArtNode.setSignal(playerAlbumArt(postbox: self.postbox, engine: self.engine, fileReference: self.currentFileReference, albumArt: albumArt, thumbnail: true))
self.albumArtNode.setSignal(playerAlbumArt(postbox: self.account.postbox, engine: self.engine, fileReference: self.currentFileReference, albumArt: albumArt, thumbnail: true))
if let largeAlbumArtNode = self.largeAlbumArtNode {
largeAlbumArtNode.setSignal(playerAlbumArt(postbox: self.postbox, engine: self.engine, fileReference: self.currentFileReference, albumArt: albumArt, thumbnail: false))
largeAlbumArtNode.setSignal(playerAlbumArt(postbox: self.account.postbox, engine: self.engine, fileReference: self.currentFileReference, albumArt: albumArt, thumbnail: false))
}
}
}
@ -823,7 +788,8 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
private func updateRateButton(_ playbackBaseRate: AudioPlaybackRate) {
let rate = self.previousRate ?? playbackBaseRate
self.rateButton.setImage(optionsRateImage(rate: rate.stringValue.uppercased(), color: self.presentationData.theme.list.itemSecondaryTextColor), for: .normal)
self.rateButton.setContent(.image(optionsRateImage(rate: rate.stringValue.uppercased(), color: self.presentationData.theme.list.itemSecondaryTextColor)))
}
static let basePanelHeight: CGFloat = 220.0
@ -877,7 +843,7 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
self.largeAlbumArtNode = largeAlbumArtNode
self.addSubnode(largeAlbumArtNode)
if self.currentAlbumArtInitialized {
largeAlbumArtNode.setSignal(playerAlbumArt(postbox: self.postbox, engine: self.engine, fileReference: self.currentFileReference, albumArt: self.currentAlbumArt, thumbnail: false))
largeAlbumArtNode.setSignal(playerAlbumArt(postbox: self.account.postbox, engine: self.engine, fileReference: self.currentFileReference, albumArt: self.currentAlbumArt, thumbnail: false))
}
}
@ -947,7 +913,7 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
let rateRightOffset = timestampLabelWidthForDuration(self.currentDuration)
transition.updateFrame(node: self.rateButton, frame: CGRect(origin: CGPoint(x: width - sideInset - rightInset - rateRightOffset - 28.0, y: scrubberVerticalOrigin + 10.0 + rightLabelVerticalOffset), size: CGSize(width: 24.0, height: 24.0)))
transition.updateFrame(node: self.rateButton, frame: CGRect(origin: CGPoint(x: width - sideInset - rightInset - rateRightOffset - 28.0, y: scrubberVerticalOrigin + 10.0 + rightLabelVerticalOffset - 10.0), size: CGSize(width: 24.0, height: 44.0)))
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -8.0), size: CGSize(width: width, height: panelHeight + 8.0)))
@ -1050,6 +1016,118 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
self.control?(.setBaseRate(nextRate))
}
private func speedList(strings: PresentationStrings) -> [(String, String, AudioPlaybackRate)] {
let speedList: [(String, String, AudioPlaybackRate)] = [
("0.5x", "0.5x", .x0_5),
(strings.PlaybackSpeed_Normal, "1x", .x1),
("1.5x", "1.5x", .x1_5),
("2x", "2x", .x2)
]
return speedList
}
private func contextMenuSpeedItems(scheduleTooltip: @escaping (MediaNavigationAccessoryPanel.ChangeType) -> Void) -> Signal<ContextController.Items, NoError> {
var presetItems: [ContextMenuItem] = []
let previousValue = self.currentRate?.doubleValue ?? 1.0
let sliderItem: ContextMenuItem = .custom(SliderContextItem(minValue: 0.5, maxValue: 2.5, value: previousValue, valueChanged: { [weak self] newValue, finished in
self?.control?(.setBaseRate(AudioPlaybackRate(newValue)))
if finished {
scheduleTooltip(.sliderCommit(previousValue, newValue))
}
}), true)
for (text, _, rate) in self.speedList(strings: self.presentationData.strings) {
let isSelected = self.currentRate == rate
presetItems.append(.action(ContextMenuActionItem(text: text, icon: { theme in
if isSelected {
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor)
} else {
return nil
}
}, action: { [weak self] _, f in
f(.default)
self?.control?(.setBaseRate(rate))
self?.presentAudioRateTooltip(baseRate: rate, changeType: .preset)
})))
}
return .single(ContextController.Items(content: .twoLists(presetItems, [sliderItem])))
}
private func openRateMenu(sourceNode: ASDisplayNode, gesture: ContextGesture?) {
guard let controller = self.getParentController() else {
return
}
var scheduledTooltip: MediaNavigationAccessoryPanel.ChangeType?
let items = self.contextMenuSpeedItems(scheduleTooltip: { change in
scheduledTooltip = change
})
let contextController = ContextController(account: self.account, presentationData: self.presentationData, source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceNode: self.rateButton.referenceNode, shouldBeDismissed: .single(false))), items: items, gesture: gesture)
contextController.dismissed = { [weak self] in
if let scheduledTooltip, let self, let rate = self.currentRate {
self.presentAudioRateTooltip(baseRate: rate, changeType: scheduledTooltip)
}
}
controller.presentInGlobalOverlay(contextController)
}
private func presentAudioRateTooltip(baseRate: AudioPlaybackRate, changeType: MediaNavigationAccessoryPanel.ChangeType) {
guard let controller = self.getParentController() else {
return
}
let presentationData = self.presentationData
let text: String?
let rate: CGFloat?
if baseRate == .x1 {
text = presentationData.strings.Conversation_AudioRateTooltipNormal
rate = 1.0
} else if baseRate == .x1_5 {
text = presentationData.strings.Conversation_AudioRateTooltip15X
rate = 1.5
} else if baseRate == .x2 {
text = presentationData.strings.Conversation_AudioRateTooltipSpeedUp
rate = 2.0
} else {
let value = String(format: "%0.1f", baseRate.doubleValue)
text = presentationData.strings.Conversation_AudioRateTooltipCustom(value).string
if case let .sliderCommit(previousValue, newValue) = changeType {
if newValue > previousValue {
rate = .infinity
} else if newValue < previousValue {
rate = -.infinity
} else {
rate = nil
}
} else {
rate = nil
}
}
var showTooltip = true
if case .sliderChange = changeType {
showTooltip = false
}
if let rate, let text, showTooltip {
controller.presentInGlobalOverlay(
UndoOverlayController(
presentationData: presentationData,
content: .audioRate(
rate: rate,
text: text
),
elevatedLayout: false,
animateInAsReplacement: false,
action: { action in
return true
}
)
)
}
}
@objc func albumArtTap(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
if let supernode = self.supernode {
@ -1128,3 +1206,20 @@ private final class PlayPauseIconNode: ManagedAnimationNode {
}
}
}
private final class HeaderContextReferenceContentSource: ContextReferenceContentSource {
private let controller: ViewController
private let sourceNode: ContextReferenceContentNode
var shouldBeDismissed: Signal<Bool, NoError>
init(controller: ViewController, sourceNode: ContextReferenceContentNode, shouldBeDismissed: Signal<Bool, NoError>) {
self.controller = controller
self.sourceNode = sourceNode
self.shouldBeDismissed = shouldBeDismissed
}
func transitionInfo() -> ContextControllerReferenceViewInfo? {
return ContextControllerReferenceViewInfo(referenceView: self.sourceNode.view, contentAreaInScreenSpace: UIScreen.main.bounds)
}
}