diff --git a/submodules/TelegramBaseController/Sources/MediaNavigationAccessoryHeaderNode.swift b/submodules/TelegramBaseController/Sources/MediaNavigationAccessoryHeaderNode.swift index f40bf6be3d..e02b80d015 100644 --- a/submodules/TelegramBaseController/Sources/MediaNavigationAccessoryHeaderNode.swift +++ b/submodules/TelegramBaseController/Sources/MediaNavigationAccessoryHeaderNode.swift @@ -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) } diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index b32546be6d..fc92fcbbac 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -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, diff --git a/submodules/TelegramUI/Sources/OverlayAudioPlayerController.swift b/submodules/TelegramUI/Sources/OverlayAudioPlayerController.swift index ee8be06678..79d79fdfe8 100644 --- a/submodules/TelegramUI/Sources/OverlayAudioPlayerController.swift +++ b/submodules/TelegramUI/Sources/OverlayAudioPlayerController.swift @@ -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()) diff --git a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift index 125ab450ff..cd49d9a72c 100644 --- a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift +++ b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift @@ -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 diff --git a/submodules/TelegramUI/Sources/OverlayPlayerControlsNode.swift b/submodules/TelegramUI/Sources/OverlayPlayerControlsNode.swift index b1dad1f55d..ac64fd5774 100644 --- a/submodules/TelegramUI/Sources/OverlayPlayerControlsNode.swift +++ b/submodules/TelegramUI/Sources/OverlayPlayerControlsNode.swift @@ -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 - 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, 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) 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 { + 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 + + init(controller: ViewController, sourceNode: ContextReferenceContentNode, shouldBeDismissed: Signal) { + self.controller = controller + self.sourceNode = sourceNode + self.shouldBeDismissed = shouldBeDismissed + } + + func transitionInfo() -> ContextControllerReferenceViewInfo? { + return ContextControllerReferenceViewInfo(referenceView: self.sourceNode.view, contentAreaInScreenSpace: UIScreen.main.bounds) + } +}