Modernize video playback UI

This commit is contained in:
Ali 2021-07-20 21:17:14 +02:00
parent fed038104e
commit 7161c34527
27 changed files with 1349 additions and 67 deletions

View File

@ -18,10 +18,10 @@ public final class GalleryControllerActionInteraction {
public let openHashtag: (String?, String) -> Void
public let openBotCommand: (String) -> Void
public let addContact: (String) -> Void
public let storeMediaPlaybackState: (MessageId, Double?) -> Void
public let storeMediaPlaybackState: (MessageId, Double?, Double) -> Void
public let editMedia: (MessageId, [UIView], @escaping () -> Void) -> Void
public init(openUrl: @escaping (String, Bool) -> Void, openUrlIn: @escaping (String) -> Void, openPeerMention: @escaping (String) -> Void, openPeer: @escaping (PeerId) -> Void, openHashtag: @escaping (String?, String) -> Void, openBotCommand: @escaping (String) -> Void, addContact: @escaping (String) -> Void, storeMediaPlaybackState: @escaping (MessageId, Double?) -> Void, editMedia: @escaping (MessageId, [UIView], @escaping () -> Void) -> Void) {
public init(openUrl: @escaping (String, Bool) -> Void, openUrlIn: @escaping (String) -> Void, openPeerMention: @escaping (String) -> Void, openPeer: @escaping (PeerId) -> Void, openHashtag: @escaping (String?, String) -> Void, openBotCommand: @escaping (String) -> Void, addContact: @escaping (String) -> Void, storeMediaPlaybackState: @escaping (MessageId, Double?, Double) -> Void, editMedia: @escaping (MessageId, [UIView], @escaping () -> Void) -> Void) {
self.openUrl = openUrl
self.openUrlIn = openUrlIn
self.openPeerMention = openPeerMention

View File

@ -239,7 +239,7 @@ open class NavigationBar: ASDisplayNode {
var presentationData: NavigationBarPresentationData
private var validLayout: (size: CGSize, defaultHeight: CGFloat, additionalTopHeight: CGFloat, additionalContentHeight: CGFloat, additionalBackgroundHeight: CGFloat, leftInset: CGFloat, rightInset: CGFloat, appearsHidden: Bool)?
private var validLayout: (size: CGSize, defaultHeight: CGFloat, additionalTopHeight: CGFloat, additionalContentHeight: CGFloat, additionalBackgroundHeight: CGFloat, leftInset: CGFloat, rightInset: CGFloat, appearsHidden: Bool, isLandscape: Bool)?
private var requestedLayout: Bool = false
var requestContainerLayout: (ContainedViewLayoutTransition) -> Void = { _ in }
@ -940,16 +940,16 @@ open class NavigationBar: ASDisplayNode {
if let validLayout = self.validLayout, self.requestedLayout {
self.requestedLayout = false
self.updateLayout(size: validLayout.size, defaultHeight: validLayout.defaultHeight, additionalTopHeight: validLayout.additionalTopHeight, additionalContentHeight: validLayout.additionalContentHeight, additionalBackgroundHeight: validLayout.additionalBackgroundHeight, leftInset: validLayout.leftInset, rightInset: validLayout.rightInset, appearsHidden: validLayout.appearsHidden, transition: .immediate)
self.updateLayout(size: validLayout.size, defaultHeight: validLayout.defaultHeight, additionalTopHeight: validLayout.additionalTopHeight, additionalContentHeight: validLayout.additionalContentHeight, additionalBackgroundHeight: validLayout.additionalBackgroundHeight, leftInset: validLayout.leftInset, rightInset: validLayout.rightInset, appearsHidden: validLayout.appearsHidden, isLandscape: validLayout.isLandscape, transition: .immediate)
}
}
func updateLayout(size: CGSize, defaultHeight: CGFloat, additionalTopHeight: CGFloat, additionalContentHeight: CGFloat, additionalBackgroundHeight: CGFloat, leftInset: CGFloat, rightInset: CGFloat, appearsHidden: Bool, transition: ContainedViewLayoutTransition) {
func updateLayout(size: CGSize, defaultHeight: CGFloat, additionalTopHeight: CGFloat, additionalContentHeight: CGFloat, additionalBackgroundHeight: CGFloat, leftInset: CGFloat, rightInset: CGFloat, appearsHidden: Bool, isLandscape: Bool, transition: ContainedViewLayoutTransition) {
if self.layoutSuspended {
return
}
self.validLayout = (size, defaultHeight, additionalTopHeight, additionalContentHeight, additionalBackgroundHeight, leftInset, rightInset, appearsHidden)
self.validLayout = (size, defaultHeight, additionalTopHeight, additionalContentHeight, additionalBackgroundHeight, leftInset, rightInset, appearsHidden, isLandscape)
let backgroundFrame = CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height + additionalBackgroundHeight))
if self.backgroundNode.frame != backgroundFrame {
@ -995,7 +995,7 @@ open class NavigationBar: ASDisplayNode {
var leftTitleInset: CGFloat = leftInset + 1.0
var rightTitleInset: CGFloat = rightInset + 1.0
if self.backButtonNode.supernode != nil {
let backButtonSize = self.backButtonNode.updateLayout(constrainedSize: CGSize(width: size.width, height: nominalHeight))
let backButtonSize = self.backButtonNode.updateLayout(constrainedSize: CGSize(width: size.width, height: nominalHeight), isLandscape: isLandscape)
leftTitleInset += backButtonSize.width + backButtonInset + 1.0
let topHitTestSlop = (nominalHeight - backButtonSize.height) * 0.5
@ -1047,7 +1047,7 @@ open class NavigationBar: ASDisplayNode {
self.badgeNode.alpha = 1.0
}
} else if self.leftButtonNode.supernode != nil {
let leftButtonSize = self.leftButtonNode.updateLayout(constrainedSize: CGSize(width: size.width, height: nominalHeight))
let leftButtonSize = self.leftButtonNode.updateLayout(constrainedSize: CGSize(width: size.width, height: nominalHeight), isLandscape: isLandscape)
leftTitleInset += leftButtonSize.width + leftButtonInset + 1.0
self.leftButtonNode.alpha = 1.0
@ -1059,7 +1059,7 @@ open class NavigationBar: ASDisplayNode {
transition.updateFrame(node: self.badgeNode, frame: CGRect(origin: backButtonArrowFrame.origin.offsetBy(dx: 7.0, dy: -9.0), size: badgeSize))
if self.rightButtonNode.supernode != nil {
let rightButtonSize = self.rightButtonNode.updateLayout(constrainedSize: (CGSize(width: size.width, height: nominalHeight)))
let rightButtonSize = self.rightButtonNode.updateLayout(constrainedSize: (CGSize(width: size.width, height: nominalHeight)), isLandscape: isLandscape)
rightTitleInset += rightButtonSize.width + leftButtonInset + 1.0
self.rightButtonNode.alpha = 1.0
transition.updateFrame(node: self.rightButtonNode, frame: CGRect(origin: CGPoint(x: size.width - leftButtonInset - rightButtonSize.width, y: contentVerticalOrigin + floor((nominalHeight - rightButtonSize.height) / 2.0)), size: rightButtonSize))
@ -1073,7 +1073,7 @@ open class NavigationBar: ASDisplayNode {
break
case .bottom:
if let transitionBackButtonNode = self.transitionBackButtonNode {
let transitionBackButtonSize = transitionBackButtonNode.updateLayout(constrainedSize: CGSize(width: size.width, height: nominalHeight))
let transitionBackButtonSize = transitionBackButtonNode.updateLayout(constrainedSize: CGSize(width: size.width, height: nominalHeight), isLandscape: isLandscape)
let initialX: CGFloat = backButtonInset + size.width * 0.3
let finalX: CGFloat = floor((size.width - transitionBackButtonSize.width) / 2.0)
@ -1204,7 +1204,7 @@ open class NavigationBar: ASDisplayNode {
node.updateManualText(self.backButtonNode.manualText)
node.color = accentColor
if let validLayout = self.validLayout {
let _ = node.updateLayout(constrainedSize: CGSize(width: validLayout.size.width, height: validLayout.defaultHeight))
let _ = node.updateLayout(constrainedSize: CGSize(width: validLayout.size.width, height: validLayout.defaultHeight), isLandscape: validLayout.isLandscape)
node.frame = self.backButtonNode.frame
}
return node
@ -1227,7 +1227,7 @@ open class NavigationBar: ASDisplayNode {
node.updateItems(items)
node.color = accentColor
if let validLayout = self.validLayout {
let _ = node.updateLayout(constrainedSize: CGSize(width: validLayout.size.width, height: validLayout.defaultHeight))
let _ = node.updateLayout(constrainedSize: CGSize(width: validLayout.size.width, height: validLayout.defaultHeight), isLandscape: validLayout.isLandscape)
node.frame = self.backButtonNode.frame
}
return node

View File

@ -255,8 +255,6 @@ private final class NavigationButtonItemNode: ImmediateTextNode {
public override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesMoved(touches, with: event)
//self.updateHighlightedState(self.touchInsideApparentBounds(touches.first!), animated: true)
}
public override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
@ -274,6 +272,15 @@ private final class NavigationButtonItemNode: ImmediateTextNode {
self.pressed()
}
}
public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if let node = self.node as? HighlightableButtonNode {
let result = node.view.hitTest(self.view.convert(point, to: node.view), with: event)
return result
} else {
return super.hitTest(point, with: event)
}
}
public override func touchesCancelled(_ touches: Set<UITouch>?, with event: UIEvent?) {
super.touchesCancelled(touches, with: event)
@ -453,7 +460,7 @@ public final class NavigationButtonNode: ASDisplayNode {
}
}
public func updateLayout(constrainedSize: CGSize) -> CGSize {
public func updateLayout(constrainedSize: CGSize, isLandscape: Bool) -> CGSize {
var nodeOrigin = CGPoint()
var totalSize = CGSize()
for node in self.nodes {
@ -468,6 +475,9 @@ public final class NavigationButtonNode: ASDisplayNode {
totalSize.height = max(totalSize.height, nodeSize.height)
node.frame = CGRect(origin: CGPoint(x: nodeOrigin.x, y: floor((totalSize.height - nodeSize.height) / 2.0)), size: nodeSize)
nodeOrigin.x += node.bounds.width
if isLandscape {
nodeOrigin.x += 16.0
}
}
return totalSize
}

View File

@ -381,6 +381,8 @@ public enum TabBarItemContextActionType {
}
self.navigationBarOrigin = navigationBarFrame.origin.y
let isLandscape = layout.size.width > layout.size.height
if let navigationBar = self.navigationBar {
if let contentNode = navigationBar.contentNode, case .expansion = contentNode.mode, !self.displayNavigationBar {
@ -392,7 +394,7 @@ public enum TabBarItemContextActionType {
navigationBarFrame.size.height += NavigationBar.defaultSecondaryContentHeight
//navigationBarFrame.origin.y += NavigationBar.defaultSecondaryContentHeight
}
navigationBar.updateLayout(size: navigationBarFrame.size, defaultHeight: navigationLayout.defaultContentHeight, additionalTopHeight: statusBarHeight, additionalContentHeight: self.additionalNavigationBarHeight, additionalBackgroundHeight: additionalBackgroundHeight, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, appearsHidden: !self.displayNavigationBar, transition: transition)
navigationBar.updateLayout(size: navigationBarFrame.size, defaultHeight: navigationLayout.defaultContentHeight, additionalTopHeight: statusBarHeight, additionalContentHeight: self.additionalNavigationBarHeight, additionalBackgroundHeight: additionalBackgroundHeight, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, appearsHidden: !self.displayNavigationBar, isLandscape: isLandscape, transition: transition)
if !transition.isAnimated {
navigationBar.layer.cancelAnimationsRecursive(key: "bounds")
navigationBar.layer.cancelAnimationsRecursive(key: "position")

View File

@ -230,20 +230,20 @@ public func chatMessageGalleryControllerData(context: AccountContext, chatLocati
let gallery = SecretMediaPreviewController(context: context, messageId: message.id)
return .secretGallery(gallery)
} else {
let startTimecode: Signal<Double?, NoError>
let startState: Signal<(timecode: Double?, rate: Double), NoError>
if let timecode = timecode {
startTimecode = .single(timecode)
startState = .single((timecode: timecode, rate: 1.0))
} else {
startTimecode = mediaPlaybackStoredState(postbox: context.account.postbox, messageId: message.id)
startState = mediaPlaybackStoredState(postbox: context.account.postbox, messageId: message.id)
|> map { state in
return state?.timestamp
return (state?.timestamp, state?.playbackRate.doubleValue ?? 1.0)
}
}
return .gallery(startTimecode
return .gallery(startState
|> deliverOnMainQueue
|> map { timecode in
let gallery = GalleryController(context: context, source: source ?? (standalone ? .standaloneMessage(message) : .peerMessagesAtId(messageId: message.id, chatLocation: chatLocation ?? .peer(message.id.peerId), chatLocationContextHolder: chatLocationContextHolder ?? Atomic<ChatLocationContextHolder?>(value: nil))), invertItemOrder: reverseMessageGalleryOrder, streamSingleVideo: stream, fromPlayingVideo: autoplayingVideo, landscape: landscape, timecode: timecode, synchronousLoad: synchronousLoad, replaceRootController: { [weak navigationController] controller, ready in
|> map { startState in
let gallery = GalleryController(context: context, source: source ?? (standalone ? .standaloneMessage(message) : .peerMessagesAtId(messageId: message.id, chatLocation: chatLocation ?? .peer(message.id.peerId), chatLocationContextHolder: chatLocationContextHolder ?? Atomic<ChatLocationContextHolder?>(value: nil))), invertItemOrder: reverseMessageGalleryOrder, streamSingleVideo: stream, fromPlayingVideo: autoplayingVideo, landscape: landscape, timecode: startState.timecode, playbackRate: startState.rate, synchronousLoad: synchronousLoad, replaceRootController: { [weak navigationController] controller, ready in
navigationController?.replaceTopController(controller, animated: false, ready: ready)
}, baseNavigationController: navigationController, actionInteraction: actionInteraction)
gallery.temporaryDoNotWaitForReady = autoplayingVideo

View File

@ -28,7 +28,10 @@ swift_library(
"//submodules/OverlayStatusController:OverlayStatusController",
"//submodules/PresentationDataUtils:PresentationDataUtils",
"//submodules/UrlEscaping:UrlEscaping",
"//submodules/ManagedAnimationNode:ManagedAnimationNode"
"//submodules/ManagedAnimationNode:ManagedAnimationNode",
"//submodules/ContextUI:ContextUI",
"//submodules/SaveToCameraRoll:SaveToCameraRoll",
"//submodules/TelegramUIPreferences:TelegramUIPreferences",
],
visibility = [
"//visibility:public",

View File

@ -31,6 +31,9 @@ private let forwardImage = generateTintedImage(image: UIImage(bundleImageName: "
private let cloudFetchIcon = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/FileCloudFetch"), color: UIColor.white)
private let fullscreenOnImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Expand"), color: .white)
private let fullscreenOffImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Collapse"), color: .white)
private let captionMaskImage = generateImage(CGSize(width: 1.0, height: 17.0), opaque: false, rotatedContext: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
context.clear(bounds)
@ -119,6 +122,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
private let contentNode: ASDisplayNode
private let deleteButton: UIButton
private let fullscreenButton: UIButton
private let actionButton: UIButton
private let editButton: UIButton
private let maskNode: ASDisplayNode
@ -152,6 +156,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
var seekBackward: ((Double) -> Void)?
var seekForward: ((Double) -> Void)?
var setPlayRate: ((Double) -> Void)?
var toggleFullscreen: (() -> Void)?
var fetchControl: (() -> Void)?
var interacting: ((Bool) -> Void)?
@ -286,6 +291,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
self.contentNode = ASDisplayNode()
self.deleteButton = UIButton()
self.fullscreenButton = UIButton()
self.actionButton = UIButton()
self.editButton = UIButton()
@ -363,6 +369,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
}
self.contentNode.view.addSubview(self.deleteButton)
self.contentNode.view.addSubview(self.fullscreenButton)
self.contentNode.view.addSubview(self.actionButton)
self.contentNode.view.addSubview(self.editButton)
self.contentNode.addSubnode(self.scrollWrapperNode)
@ -381,6 +388,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
self.contentNode.addSubnode(self.statusButtonNode)
self.deleteButton.addTarget(self, action: #selector(self.deleteButtonPressed), for: [.touchUpInside])
self.fullscreenButton.addTarget(self, action: #selector(self.fullscreenButtonPressed), for: [.touchUpInside])
self.actionButton.addTarget(self, action: #selector(self.actionButtonPressed), for: [.touchUpInside])
self.editButton.addTarget(self, action: #selector(self.editButtonPressed), for: [.touchUpInside])
@ -559,6 +567,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
if origin == nil {
self.editButton.isHidden = true
self.deleteButton.isHidden = true
self.fullscreenButton.isHidden = true
self.editButton.isHidden = true
}
}
@ -568,12 +577,22 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
let canDelete: Bool
var canShare = !message.containsSecretMedia
var canFullscreen = false
var canEdit = false
for media in message.media {
if media is TelegramMediaImage {
canEdit = true
break
} else if let media = media as? TelegramMediaFile, !media.isAnimated {
for attribute in media.attributes {
switch attribute {
case .Video:
canFullscreen = true
default:
break
}
}
}
}
@ -637,7 +656,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
messageText = galleryCaptionStringWithAppliedEntities(message.text, entities: entities)
}
if self.currentMessageText != messageText || canDelete != !self.deleteButton.isHidden || canShare != !self.actionButton.isHidden || canEdit != !self.editButton.isHidden || self.currentAuthorNameText != authorNameText || self.currentDateText != dateText {
if self.currentMessageText != messageText || canDelete != !self.deleteButton.isHidden || canFullscreen != !self.fullscreenButton.isHidden || canShare != !self.actionButton.isHidden || canEdit != !self.editButton.isHidden || self.currentAuthorNameText != authorNameText || self.currentDateText != dateText {
self.currentMessageText = messageText
if messageText.length == 0 {
@ -654,8 +673,15 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
self.authorNameNode.attributedText = nil
}
self.dateNode.attributedText = NSAttributedString(string: dateText, font: dateFont, textColor: .white)
self.deleteButton.isHidden = !canDelete
if canFullscreen {
self.fullscreenButton.isHidden = false
self.deleteButton.isHidden = true
} else {
self.deleteButton.isHidden = !canDelete
self.fullscreenButton.isHidden = true
}
self.actionButton.isHidden = !canShare
self.editButton.isHidden = !canEdit
@ -683,6 +709,9 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
panelHeight += contentInset
let isLandscape = size.width > size.height
self.fullscreenButton.setImage(isLandscape ? fullscreenOffImage : fullscreenOnImage, for: [.normal])
let displayCaption: Bool
if case .compact = metrics.widthClass {
displayCaption = !self.textNode.isHidden && !isLandscape
@ -776,10 +805,11 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
let deleteFrame = CGRect(origin: CGPoint(x: width - 44.0 - rightInset, y: panelHeight - bottomInset - 44.0), size: CGSize(width: 44.0, height: 44.0))
var editFrame = CGRect(origin: CGPoint(x: width - 44.0 - 50.0 - rightInset, y: panelHeight - bottomInset - 44.0), size: CGSize(width: 44.0, height: 44.0))
if self.deleteButton.isHidden {
if self.deleteButton.isHidden && self.fullscreenButton.isHidden {
editFrame = deleteFrame
}
self.deleteButton.frame = deleteFrame
self.fullscreenButton.frame = deleteFrame
self.editButton.frame = editFrame
if let image = self.backwardButton.backgroundIconNode.image {
@ -789,7 +819,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
self.forwardButton.frame = CGRect(origin: CGPoint(x: floor((width - image.size.width) / 2.0) + 66.0, y: panelHeight - bottomInset - 44.0 + 7.0), size: image.size)
}
self.playbackControlButton.frame = CGRect(origin: CGPoint(x: floor((width - 44.0) / 2.0), y: panelHeight - bottomInset - 44.0), size: CGSize(width: 44.0, height: 44.0))
self.playbackControlButton.frame = CGRect(origin: CGPoint(x: floor((width - 44.0) / 2.0), y: panelHeight - bottomInset - 44.0 - 2.0), size: CGSize(width: 44.0, height: 44.0))
self.playPauseIconNode.frame = self.playbackControlButton.bounds.offsetBy(dx: 2.0, dy: 2.0)
let statusSize = CGSize(width: 28.0, height: 28.0)
@ -855,6 +885,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
self.dateNode.alpha = 1.0
self.authorNameNode.alpha = 1.0
self.deleteButton.alpha = 1.0
self.fullscreenButton.alpha = 1.0
self.actionButton.alpha = 1.0
self.editButton.alpha = 1.0
self.backwardButton.alpha = 1.0
@ -878,6 +909,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
self.dateNode.alpha = 0.0
self.authorNameNode.alpha = 0.0
self.deleteButton.alpha = 0.0
self.fullscreenButton.alpha = 0.0
self.actionButton.alpha = 0.0
self.editButton.alpha = 0.0
self.backwardButton.alpha = 0.0
@ -888,6 +920,10 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
completion()
})
}
@objc func fullscreenButtonPressed() {
self.toggleFullscreen?()
}
@objc func deleteButtonPressed() {
if let currentMessage = self.currentMessage {

View File

@ -145,7 +145,7 @@ private func galleryMessageCaptionText(_ message: Message) -> String {
return message.text
}
public func galleryItemForEntry(context: AccountContext, presentationData: PresentationData, entry: MessageHistoryEntry, isCentral: Bool = false, streamVideos: Bool, loopVideos: Bool = false, hideControls: Bool = false, fromPlayingVideo: Bool = false, isSecret: Bool = false, landscape: Bool = false, timecode: Double? = nil, displayInfoOnTop: Bool = false, configuration: GalleryConfiguration? = nil, tempFilePath: String? = nil, playbackCompleted: @escaping () -> Void = {}, performAction: @escaping (GalleryControllerInteractionTapAction) -> Void = { _ in }, openActionOptions: @escaping (GalleryControllerInteractionTapAction) -> Void = { _ in }, storeMediaPlaybackState: @escaping (MessageId, Double?) -> Void = { _, _ in }, present: @escaping (ViewController, Any?) -> Void) -> GalleryItem? {
public func galleryItemForEntry(context: AccountContext, presentationData: PresentationData, entry: MessageHistoryEntry, isCentral: Bool = false, streamVideos: Bool, loopVideos: Bool = false, hideControls: Bool = false, fromPlayingVideo: Bool = false, isSecret: Bool = false, landscape: Bool = false, timecode: Double? = nil, playbackRate: Double = 1.0, displayInfoOnTop: Bool = false, configuration: GalleryConfiguration? = nil, tempFilePath: String? = nil, playbackCompleted: @escaping () -> Void = {}, performAction: @escaping (GalleryControllerInteractionTapAction) -> Void = { _ in }, openActionOptions: @escaping (GalleryControllerInteractionTapAction) -> Void = { _ in }, storeMediaPlaybackState: @escaping (MessageId, Double?, Double) -> Void = { _, _, _ in }, present: @escaping (ViewController, Any?) -> Void) -> GalleryItem? {
let message = entry.message
let location = entry.location
if let (media, mediaImage) = mediaForMessage(message: message) {
@ -178,7 +178,7 @@ public func galleryItemForEntry(context: AccountContext, presentationData: Prese
}
let caption = galleryCaptionStringWithAppliedEntities(text, entities: entities)
return UniversalVideoGalleryItem(context: context, presentationData: presentationData, content: content, originData: GalleryItemOriginData(title: message.effectiveAuthor?.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), timestamp: message.timestamp), indexData: location.flatMap { GalleryItemIndexData(position: Int32($0.index), totalCount: Int32($0.count)) }, contentInfo: .message(message), caption: caption, displayInfoOnTop: displayInfoOnTop, hideControls: hideControls, fromPlayingVideo: fromPlayingVideo, isSecret: isSecret, landscape: landscape, timecode: timecode, configuration: configuration, playbackCompleted: playbackCompleted, performAction: performAction, openActionOptions: openActionOptions, storeMediaPlaybackState: storeMediaPlaybackState, present: present)
return UniversalVideoGalleryItem(context: context, presentationData: presentationData, content: content, originData: GalleryItemOriginData(title: message.effectiveAuthor?.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), timestamp: message.timestamp), indexData: location.flatMap { GalleryItemIndexData(position: Int32($0.index), totalCount: Int32($0.count)) }, contentInfo: .message(message), caption: caption, displayInfoOnTop: displayInfoOnTop, hideControls: hideControls, fromPlayingVideo: fromPlayingVideo, isSecret: isSecret, landscape: landscape, timecode: timecode, playbackRate: playbackRate, configuration: configuration, playbackCompleted: playbackCompleted, performAction: performAction, openActionOptions: openActionOptions, storeMediaPlaybackState: storeMediaPlaybackState, present: present)
} else {
if let fileName = file.fileName, (fileName as NSString).pathExtension.lowercased() == "json" {
return ChatAnimationGalleryItem(context: context, presentationData: presentationData, message: message, location: location)
@ -188,7 +188,7 @@ public func galleryItemForEntry(context: AccountContext, presentationData: Prese
if let dimensions = file.dimensions {
pixelsCount = Int(dimensions.width) * Int(dimensions.height)
}
if (file.size == nil || file.size! < 4 * 1024 * 1024) && pixelsCount < 4096 * 4096 {
if pixelsCount < 10000 * 10000 {
return ChatImageGalleryItem(context: context, presentationData: presentationData, message: message, location: location, displayInfoOnTop: displayInfoOnTop, performAction: performAction, openActionOptions: openActionOptions, present: present)
} else {
return ChatDocumentGalleryItem(context: context, presentationData: presentationData, message: message, location: location)
@ -219,7 +219,7 @@ public func galleryItemForEntry(context: AccountContext, presentationData: Prese
}
}
if let content = content {
return UniversalVideoGalleryItem(context: context, presentationData: presentationData, content: content, originData: GalleryItemOriginData(title: message.effectiveAuthor?.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), timestamp: message.timestamp), indexData: location.flatMap { GalleryItemIndexData(position: Int32($0.index), totalCount: Int32($0.count)) }, contentInfo: .message(message), caption: NSAttributedString(string: ""), displayInfoOnTop: displayInfoOnTop, fromPlayingVideo: fromPlayingVideo, isSecret: isSecret, landscape: landscape, timecode: timecode, configuration: configuration, performAction: performAction, openActionOptions: openActionOptions, storeMediaPlaybackState: storeMediaPlaybackState, present: present)
return UniversalVideoGalleryItem(context: context, presentationData: presentationData, content: content, originData: GalleryItemOriginData(title: message.effectiveAuthor?.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), timestamp: message.timestamp), indexData: location.flatMap { GalleryItemIndexData(position: Int32($0.index), totalCount: Int32($0.count)) }, contentInfo: .message(message), caption: NSAttributedString(string: ""), displayInfoOnTop: displayInfoOnTop, fromPlayingVideo: fromPlayingVideo, isSecret: isSecret, landscape: landscape, timecode: timecode, playbackRate: playbackRate, configuration: configuration, performAction: performAction, openActionOptions: openActionOptions, storeMediaPlaybackState: storeMediaPlaybackState, present: present)
} else {
return nil
}
@ -349,6 +349,7 @@ public class GalleryController: ViewController, StandalonePresentableController
private let fromPlayingVideo: Bool
private let landscape: Bool
private let timecode: Double?
private let playbackRate: Double
private let accountInUseDisposable = MetaDisposable()
private let disposable = MetaDisposable()
@ -388,7 +389,7 @@ public class GalleryController: ViewController, StandalonePresentableController
private var initialOrientation: UIInterfaceOrientation?
public init(context: AccountContext, source: GalleryControllerItemSource, invertItemOrder: Bool = false, streamSingleVideo: Bool = false, fromPlayingVideo: Bool = false, landscape: Bool = false, timecode: Double? = nil, synchronousLoad: Bool = false, replaceRootController: @escaping (ViewController, Promise<Bool>?) -> Void, baseNavigationController: NavigationController?, actionInteraction: GalleryControllerActionInteraction? = nil) {
public init(context: AccountContext, source: GalleryControllerItemSource, invertItemOrder: Bool = false, streamSingleVideo: Bool = false, fromPlayingVideo: Bool = false, landscape: Bool = false, timecode: Double? = nil, playbackRate: Double = 1.0, synchronousLoad: Bool = false, replaceRootController: @escaping (ViewController, Promise<Bool>?) -> Void, baseNavigationController: NavigationController?, actionInteraction: GalleryControllerActionInteraction? = nil) {
self.context = context
self.source = source
self.invertItemOrder = invertItemOrder
@ -399,6 +400,7 @@ public class GalleryController: ViewController, StandalonePresentableController
self.fromPlayingVideo = fromPlayingVideo
self.landscape = landscape
self.timecode = timecode
self.playbackRate = playbackRate
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
@ -537,7 +539,7 @@ public class GalleryController: ViewController, StandalonePresentableController
if entry.message.stableId == strongSelf.centralEntryStableId {
isCentral = true
}
if let item = galleryItemForEntry(context: context, presentationData: strongSelf.presentationData, entry: entry, isCentral: isCentral, streamVideos: streamSingleVideo, fromPlayingVideo: isCentral && fromPlayingVideo, landscape: isCentral && landscape, timecode: isCentral ? timecode : nil, displayInfoOnTop: displayInfoOnTop, configuration: configuration, performAction: strongSelf.performAction, openActionOptions: strongSelf.openActionOptions, storeMediaPlaybackState: strongSelf.actionInteraction?.storeMediaPlaybackState ?? { _, _ in }, present: { [weak self] c, a in
if let item = galleryItemForEntry(context: context, presentationData: strongSelf.presentationData, entry: entry, isCentral: isCentral, streamVideos: streamSingleVideo, fromPlayingVideo: isCentral && fromPlayingVideo, landscape: isCentral && landscape, timecode: isCentral ? timecode : nil, playbackRate: isCentral ? playbackRate : 1.0, displayInfoOnTop: displayInfoOnTop, configuration: configuration, performAction: strongSelf.performAction, openActionOptions: strongSelf.openActionOptions, storeMediaPlaybackState: strongSelf.actionInteraction?.storeMediaPlaybackState ?? { _, _, _ in }, present: { [weak self] c, a in
if let strongSelf = self {
strongSelf.presentInGlobalOverlay(c, with: a)
}
@ -1040,6 +1042,9 @@ public class GalleryController: ViewController, StandalonePresentableController
self.galleryNode.baseNavigationController = { [weak baseNavigationController] in
return baseNavigationController
}
self.galleryNode.galleryController = { [weak self] in
return self
}
var displayInfoOnTop = false
if case .custom = source {
@ -1053,7 +1058,7 @@ public class GalleryController: ViewController, StandalonePresentableController
if entry.message.stableId == self.centralEntryStableId {
isCentral = true
}
if let item = galleryItemForEntry(context: self.context, presentationData: self.presentationData, entry: entry, streamVideos: self.streamVideos, fromPlayingVideo: isCentral && self.fromPlayingVideo, landscape: isCentral && self.landscape, timecode: isCentral ? self.timecode : nil, displayInfoOnTop: displayInfoOnTop, configuration: self.configuration, performAction: self.performAction, openActionOptions: self.openActionOptions, storeMediaPlaybackState: self.actionInteraction?.storeMediaPlaybackState ?? { _, _ in }, present: { [weak self] c, a in
if let item = galleryItemForEntry(context: self.context, presentationData: self.presentationData, entry: entry, streamVideos: self.streamVideos, fromPlayingVideo: isCentral && self.fromPlayingVideo, landscape: isCentral && self.landscape, timecode: isCentral ? self.timecode : nil, playbackRate: isCentral ? self.playbackRate : 1.0, displayInfoOnTop: displayInfoOnTop, configuration: self.configuration, performAction: self.performAction, openActionOptions: self.openActionOptions, storeMediaPlaybackState: self.actionInteraction?.storeMediaPlaybackState ?? { _, _, _ in }, present: { [weak self] c, a in
if let strongSelf = self {
strongSelf.presentInGlobalOverlay(c, with: a)
}
@ -1133,7 +1138,7 @@ public class GalleryController: ViewController, StandalonePresentableController
if entry.message.stableId == strongSelf.centralEntryStableId {
isCentral = true
}
if let item = galleryItemForEntry(context: strongSelf.context, presentationData: strongSelf.presentationData, entry: entry, isCentral: isCentral, streamVideos: false, fromPlayingVideo: isCentral && strongSelf.fromPlayingVideo, landscape: isCentral && strongSelf.landscape, timecode: isCentral ? strongSelf.timecode : nil, displayInfoOnTop: displayInfoOnTop, configuration: strongSelf.configuration, performAction: strongSelf.performAction, openActionOptions: strongSelf.openActionOptions, storeMediaPlaybackState: strongSelf.actionInteraction?.storeMediaPlaybackState ?? { _, _ in }, present: { [weak self] c, a in
if let item = galleryItemForEntry(context: strongSelf.context, presentationData: strongSelf.presentationData, entry: entry, isCentral: isCentral, streamVideos: false, fromPlayingVideo: isCentral && strongSelf.fromPlayingVideo, landscape: isCentral && strongSelf.landscape, timecode: isCentral ? strongSelf.timecode : nil, displayInfoOnTop: displayInfoOnTop, configuration: strongSelf.configuration, performAction: strongSelf.performAction, openActionOptions: strongSelf.openActionOptions, storeMediaPlaybackState: strongSelf.actionInteraction?.storeMediaPlaybackState ?? { _, _, _ in }, present: { [weak self] c, a in
if let strongSelf = self {
strongSelf.presentInGlobalOverlay(c, with: a)
}
@ -1185,7 +1190,7 @@ public class GalleryController: ViewController, StandalonePresentableController
if entry.message.stableId == strongSelf.centralEntryStableId {
isCentral = true
}
if let item = galleryItemForEntry(context: strongSelf.context, presentationData: strongSelf.presentationData, entry: entry, isCentral: isCentral, streamVideos: false, fromPlayingVideo: isCentral && strongSelf.fromPlayingVideo, landscape: isCentral && strongSelf.landscape, timecode: isCentral ? strongSelf.timecode : nil, displayInfoOnTop: displayInfoOnTop, configuration: strongSelf.configuration, performAction: strongSelf.performAction, openActionOptions: strongSelf.openActionOptions, storeMediaPlaybackState: strongSelf.actionInteraction?.storeMediaPlaybackState ?? { _, _ in }, present: { [weak self] c, a in
if let item = galleryItemForEntry(context: strongSelf.context, presentationData: strongSelf.presentationData, entry: entry, isCentral: isCentral, streamVideos: false, fromPlayingVideo: isCentral && strongSelf.fromPlayingVideo, landscape: isCentral && strongSelf.landscape, timecode: isCentral ? strongSelf.timecode : nil, displayInfoOnTop: displayInfoOnTop, configuration: strongSelf.configuration, performAction: strongSelf.performAction, openActionOptions: strongSelf.openActionOptions, storeMediaPlaybackState: strongSelf.actionInteraction?.storeMediaPlaybackState ?? { _, _, _ in }, present: { [weak self] c, a in
if let strongSelf = self {
strongSelf.presentInGlobalOverlay(c, with: a)
}

View File

@ -22,6 +22,7 @@ open class GalleryControllerNode: ASDisplayNode, UIScrollViewDelegate, UIGesture
public var beginCustomDismiss: () -> Void = { }
public var completeCustomDismiss: () -> Void = { }
public var baseNavigationController: () -> NavigationController? = { return nil }
public var galleryController: () -> ViewController? = { return nil }
private var presentationState = GalleryControllerPresentationState()
@ -120,6 +121,9 @@ open class GalleryControllerNode: ASDisplayNode, UIScrollViewDelegate, UIGesture
self.pager.baseNavigationController = { [weak self] in
return self?.baseNavigationController()
}
self.pager.galleryController = { [weak self] in
return self?.galleryController()
}
self.addSubnode(self.backgroundNode)

View File

@ -27,6 +27,7 @@ open class GalleryItemNode: ASDisplayNode {
public var beginCustomDismiss: () -> Void = { }
public var completeCustomDismiss: () -> Void = { }
public var baseNavigationController: () -> NavigationController? = { return nil }
public var galleryController: () -> ViewController? = { return nil }
public var alternativeDismiss: () -> Bool = { return false }
override public init() {

View File

@ -112,6 +112,7 @@ public final class GalleryPagerNode: ASDisplayNode, UIScrollViewDelegate, UIGest
public var beginCustomDismiss: () -> Void = { }
public var completeCustomDismiss: () -> Void = { }
public var baseNavigationController: () -> NavigationController? = { return nil }
public var galleryController: () -> ViewController? = { return nil }
public init(pageGap: CGFloat, disableTapNavigation: Bool) {
self.pageGap = pageGap
@ -480,6 +481,7 @@ public final class GalleryPagerNode: ASDisplayNode, UIScrollViewDelegate, UIGest
node.beginCustomDismiss = self.beginCustomDismiss
node.completeCustomDismiss = self.completeCustomDismiss
node.baseNavigationController = self.baseNavigationController
node.galleryController = self.galleryController
node.index = index
return node
}

View File

@ -15,6 +15,11 @@ import PresentationDataUtils
import OverlayStatusController
import StickerPackPreviewUI
import AppBundle
import AnimationUI
import ContextUI
import SaveToCameraRoll
import UndoUI
import TelegramUIPreferences
public enum UniversalVideoGalleryItemContentInfo {
case message(Message)
@ -40,14 +45,15 @@ public class UniversalVideoGalleryItem: GalleryItem {
let isSecret: Bool
let landscape: Bool
let timecode: Double?
let playbackRate: Double
let configuration: GalleryConfiguration?
let playbackCompleted: () -> Void
let performAction: (GalleryControllerInteractionTapAction) -> Void
let openActionOptions: (GalleryControllerInteractionTapAction) -> Void
let storeMediaPlaybackState: (MessageId, Double?) -> Void
let storeMediaPlaybackState: (MessageId, Double?, Double) -> Void
let present: (ViewController, Any?) -> Void
public init(context: AccountContext, presentationData: PresentationData, content: UniversalVideoContent, originData: GalleryItemOriginData?, indexData: GalleryItemIndexData?, contentInfo: UniversalVideoGalleryItemContentInfo?, caption: NSAttributedString, credit: NSAttributedString? = nil, displayInfoOnTop: Bool = false, hideControls: Bool = false, fromPlayingVideo: Bool = false, isSecret: Bool = false, landscape: Bool = false, timecode: Double? = nil, configuration: GalleryConfiguration? = nil, playbackCompleted: @escaping () -> Void = {}, performAction: @escaping (GalleryControllerInteractionTapAction) -> Void, openActionOptions: @escaping (GalleryControllerInteractionTapAction) -> Void, storeMediaPlaybackState: @escaping (MessageId, Double?) -> Void, present: @escaping (ViewController, Any?) -> Void) {
public init(context: AccountContext, presentationData: PresentationData, content: UniversalVideoContent, originData: GalleryItemOriginData?, indexData: GalleryItemIndexData?, contentInfo: UniversalVideoGalleryItemContentInfo?, caption: NSAttributedString, credit: NSAttributedString? = nil, displayInfoOnTop: Bool = false, hideControls: Bool = false, fromPlayingVideo: Bool = false, isSecret: Bool = false, landscape: Bool = false, timecode: Double? = nil, playbackRate: Double = 1.0, configuration: GalleryConfiguration? = nil, playbackCompleted: @escaping () -> Void = {}, performAction: @escaping (GalleryControllerInteractionTapAction) -> Void, openActionOptions: @escaping (GalleryControllerInteractionTapAction) -> Void, storeMediaPlaybackState: @escaping (MessageId, Double?, Double) -> Void, present: @escaping (ViewController, Any?) -> Void) {
self.context = context
self.presentationData = presentationData
self.content = content
@ -62,6 +68,7 @@ public class UniversalVideoGalleryItem: GalleryItem {
self.isSecret = isSecret
self.landscape = landscape
self.timecode = timecode
self.playbackRate = playbackRate
self.configuration = configuration
self.playbackCompleted = playbackCompleted
self.performAction = performAction
@ -131,6 +138,8 @@ public class UniversalVideoGalleryItem: GalleryItem {
private let pictureInPictureImage = UIImage(bundleImageName: "Media Gallery/PictureInPictureIcon")?.precomposed()
private let pictureInPictureButtonImage = generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/PictureInPictureButton"), color: .white)
private let moreButtonImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/More"), color: .white)
private let placeholderFont = Font.regular(16.0)
private final class UniversalVideoGalleryItemPictureInPictureNode: ASDisplayNode {
@ -253,6 +262,196 @@ private struct FetchControls {
let cancel: () -> Void
}
func optionsBackgroundImage(dark: Bool) -> UIImage? {
return generateImage(CGSize(width: 28.0, height: 28.0), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(UIColor(rgb: dark ? 0x1c1c1e : 0x2c2c2e).cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
})?.stretchableImage(withLeftCapWidth: 14, topCapHeight: 14)
}
private func optionsCircleImage(dark: Bool) -> UIImage? {
return generateImage(CGSize(width: 22.0, height: 22.0), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setStrokeColor(UIColor.white.cgColor)
let lineWidth: CGFloat = 1.3
context.setLineWidth(lineWidth)
context.strokeEllipse(in: CGRect(origin: CGPoint(), size: size).insetBy(dx: lineWidth, dy: lineWidth))
})
}
private func optionsRateImage(rate: String, isLarge: Bool, color: UIColor = .white) -> UIImage? {
return generateImage(isLarge ? CGSize(width: 30.0, height: 30.0) : CGSize(width: 24.0, height: 24.0), rotatedContext: { size, context in
UIGraphicsPushContext(context)
context.clear(CGRect(origin: CGPoint(), size: size))
if let image = generateTintedImage(image: UIImage(bundleImageName: isLarge ? "Chat/Context Menu/Playspeed30" : "Chat/Context Menu/Playspeed24"), color: .white) {
image.draw(at: CGPoint(x: 0.0, y: 0.0))
}
let string = NSMutableAttributedString(string: rate, font: Font.with(size: isLarge ? 11.0 : 10.0, design: .round, weight: .semibold), textColor: color)
var offset = CGPoint(x: 1.0, y: 0.0)
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
}
} else {
offset.x += -0.3
}
if !isLarge {
offset.x *= 0.5
offset.y *= 0.5
}
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 + floor((size.height - boundingRect.height) / 2.0)))
UIGraphicsPopContext()
})
}
private final class MoreHeaderButton: HighlightableButtonNode {
enum Content {
case image(UIImage?)
case more(UIImage?)
}
let referenceNode: ContextReferenceContentNode
let containerNode: ContextControllerSourceNode
private let iconNode: ASImageNode
private var animationNode: AnimationNode?
var contextAction: ((ASDisplayNode, ContextGesture?) -> Void)?
private let wide: Bool
init(wide: Bool = false) {
self.wide = wide
self.referenceNode = ContextReferenceContentNode()
self.containerNode = ContextControllerSourceNode()
self.containerNode.animateScale = false
self.iconNode = ASImageNode()
self.iconNode.displaysAsynchronously = false
self.iconNode.displayWithoutProcessing = true
self.iconNode.contentMode = .scaleToFill
super.init()
self.containerNode.addSubnode(self.referenceNode)
self.referenceNode.addSubnode(self.iconNode)
self.addSubnode(self.containerNode)
self.containerNode.shouldBegin = { [weak self] location in
guard let strongSelf = self, let _ = strongSelf.contextAction else {
return false
}
return true
}
self.containerNode.activated = { [weak self] gesture, _ in
guard let strongSelf = self else {
return
}
strongSelf.contextAction?(strongSelf.containerNode, gesture)
}
self.containerNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: wide ? 32.0 : 22.0, height: 22.0))
self.referenceNode.frame = self.containerNode.bounds
self.iconNode.image = optionsCircleImage(dark: false)
if let image = self.iconNode.image {
self.iconNode.frame = CGRect(origin: CGPoint(x: floor((self.containerNode.bounds.width - image.size.width) / 2.0), y: floor((self.containerNode.bounds.height - image.size.height) / 2.0)), size: image.size)
}
}
private var content: Content?
func setContent(_ content: Content, animated: Bool = false) {
if case .more = content, self.animationNode == nil {
let iconColor = UIColor(rgb: 0xffffff)
let animationNode = AnimationNode(animation: "anim_profilemore", colors: ["Point 2.Group 1.Fill 1": iconColor,
"Point 3.Group 1.Fill 1": iconColor,
"Point 1.Group 1.Fill 1": iconColor], scale: 1.0)
animationNode.frame = self.containerNode.bounds
self.addSubnode(animationNode)
self.animationNode = animationNode
}
if animated {
if let snapshotView = self.referenceNode.view.snapshotContentTree() {
snapshotView.frame = self.referenceNode.frame
self.view.addSubview(snapshotView)
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak snapshotView] _ in
snapshotView?.removeFromSuperview()
})
snapshotView.layer.animateScale(from: 1.0, to: 0.1, duration: 0.3, removeOnCompletion: false)
self.iconNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
self.iconNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.3)
self.animationNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
self.animationNode?.layer.animateScale(from: 0.1, to: 1.0, duration: 0.3)
}
switch content {
case let .image(image):
if let image = image {
self.iconNode.frame = CGRect(origin: CGPoint(x: floor((self.containerNode.bounds.width - image.size.width) / 2.0), y: floor((self.containerNode.bounds.height - image.size.height) / 2.0)), size: image.size)
}
self.iconNode.image = image
self.iconNode.isHidden = false
self.animationNode?.isHidden = true
case let .more(image):
if let image = image {
self.iconNode.frame = CGRect(origin: CGPoint(x: floor((self.containerNode.bounds.width - image.size.width) / 2.0), y: floor((self.containerNode.bounds.height - image.size.height) / 2.0)), size: image.size)
}
self.iconNode.image = image
self.iconNode.isHidden = false
self.animationNode?.isHidden = false
}
} else {
self.content = content
switch content {
case let .image(image):
self.iconNode.image = image
self.iconNode.isHidden = false
self.animationNode?.isHidden = true
case let .more(image):
self.iconNode.image = image
self.iconNode.isHidden = false
self.animationNode?.isHidden = false
}
}
}
override func didLoad() {
super.didLoad()
self.view.isOpaque = false
}
override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
return CGSize(width: wide ? 32.0 : 22.0, height: 22.0)
}
func onLayout() {
}
func play() {
self.animationNode?.playOnce()
}
}
final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
private let context: AccountContext
private let presentationData: PresentationData
@ -266,6 +465,10 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
private let scrubberView: ChatVideoGalleryItemScrubberView
private let footerContentNode: ChatItemGalleryFooterContentNode
private let overlayContentNode: UniversalVideoGalleryItemOverlayNode
private let moreBarButton: MoreHeaderButton
private var moreBarButtonRate: Double = 1.0
private var moreBarButtonRateTimestamp: Double?
private var videoNode: UniversalVideoNode?
private var videoNodeUserInteractionEnabled: Bool = false
@ -294,6 +497,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
private var item: UniversalVideoGalleryItem?
private let statusDisposable = MetaDisposable()
private let moreButtonStateDisposable = MetaDisposable()
private let mediaPlaybackStateDisposable = MetaDisposable()
private let fetchDisposable = MetaDisposable()
@ -307,6 +511,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
private let isPlayingPromise = ValuePromise<Bool>(false, ignoreRepeated: true)
private let isInteractingPromise = ValuePromise<Bool>(false, ignoreRepeated: true)
private let controlsVisiblePromise = ValuePromise<Bool>(true, ignoreRepeated: true)
private let isShowingContextMenuPromise = ValuePromise<Bool>(false, ignoreRepeated: true)
private var hideControlsDisposable: Disposable?
var playbackCompleted: (() -> Void)?
@ -330,8 +535,14 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
self.statusNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 50.0, height: 50.0))
self._title.set(.single(""))
self.moreBarButton = MoreHeaderButton()
self.moreBarButton.isUserInteractionEnabled = true
self.moreBarButton.setContent(.more(optionsCircleImage(dark: false)))
super.init()
self.moreBarButton.addTarget(self, action: #selector(self.moreButtonPressed), forControlEvents: .touchUpInside)
self.footerContentNode.interacting = { [weak self] value in
self?.isInteractingPromise.set(value)
@ -428,6 +639,19 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
break
}
}
self.footerContentNode.toggleFullscreen = { [weak self] in
guard let strongSelf = self else {
return
}
var toLandscape = false
let size = strongSelf.bounds.size
if size.width < size.height {
toLandscape = true
}
strongSelf.updateControlsVisibility(!toLandscape)
strongSelf.updateOrientation(toLandscape ? .landscapeRight : .portrait)
}
self.scrubbingFrameDisposable = (self.scrubbingFrame.get()
|> deliverOnMainQueue).start(next: { [weak self] result in
@ -453,12 +677,19 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
strongSelf.pictureInPictureButtonPressed()
return true
}
self.moreBarButton.contextAction = { [weak self] sourceNode, gesture in
self?.openMoreMenu(sourceNode: sourceNode, gesture: gesture)
}
self.titleContentView = GalleryTitleView(frame: CGRect())
self._titleView.set(.single(self.titleContentView))
let shouldHideControlsSignal: Signal<Void, NoError> = combineLatest(self.isPlayingPromise.get(), self.isInteractingPromise.get(), self.controlsVisiblePromise.get())
|> mapToSignal { isPlaying, isIntracting, controlsVisible -> Signal<Void, NoError> in
let shouldHideControlsSignal: Signal<Void, NoError> = combineLatest(self.isPlayingPromise.get(), self.isInteractingPromise.get(), self.controlsVisiblePromise.get(), self.isShowingContextMenuPromise.get())
|> mapToSignal { isPlaying, isIntracting, controlsVisible, isShowingContextMenu -> Signal<Void, NoError> in
if isShowingContextMenu {
return .complete()
}
if isPlaying && !isIntracting && controlsVisible {
return .single(Void())
|> delay(4.0, queue: Queue.mainQueue())
@ -477,6 +708,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
deinit {
self.statusDisposable.dispose()
self.moreButtonStateDisposable.dispose()
self.mediaPlaybackStateDisposable.dispose()
self.scrubbingFrameDisposable?.dispose()
self.hideControlsDisposable?.dispose()
@ -646,7 +878,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
if status.timestamp > 5.0 && status.timestamp < status.duration - 5.0 {
timestamp = status.timestamp
}
item.storeMediaPlaybackState(message.id, timestamp)
item.storeMediaPlaybackState(message.id, timestamp, status.baseRate)
}
}))
}
@ -687,6 +919,56 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
}
}
self.moreButtonStateDisposable.set(combineLatest(queue: .mainQueue(),
videoNode.status,
self.isShowingContextMenuPromise.get()
).start(next: { [weak self] status, isShowingContextMenu in
guard let strongSelf = self else {
return
}
guard let status = status else {
return
}
let effectiveBaseRate: Double
if isShowingContextMenu {
effectiveBaseRate = 1.0
} else {
effectiveBaseRate = status.baseRate
}
if abs(effectiveBaseRate - strongSelf.moreBarButtonRate) > 0.01 {
strongSelf.moreBarButtonRate = effectiveBaseRate
let animated: Bool
if let moreBarButtonRateTimestamp = strongSelf.moreBarButtonRateTimestamp {
animated = CFAbsoluteTimeGetCurrent() > (moreBarButtonRateTimestamp + 0.2)
} else {
animated = false
}
strongSelf.moreBarButtonRateTimestamp = CFAbsoluteTimeGetCurrent()
if abs(effectiveBaseRate - 1.0) > 0.01 {
let rateString: String
if abs(effectiveBaseRate - 0.5) < 0.01 {
rateString = "0.5x"
} else if abs(effectiveBaseRate - 1.5) < 0.01 {
rateString = "1.5x"
} else if abs(effectiveBaseRate - 2.0) < 0.01 {
rateString = "2x"
} else {
rateString = "x"
}
strongSelf.moreBarButton.setContent(.image(optionsRateImage(rate: rateString, isLarge: true)), animated: animated)
} else {
strongSelf.moreBarButton.setContent(.more(optionsCircleImage(dark: false)), animated: animated)
}
} else {
if strongSelf.moreBarButtonRateTimestamp == nil {
strongSelf.moreBarButtonRateTimestamp = CFAbsoluteTimeGetCurrent()
}
}
}))
self.statusDisposable.set((combineLatest(queue: .mainQueue(), videoNode.status, mediaFileStatus)
|> deliverOnMainQueue).start(next: { [weak self] value, fetchStatus in
if let strongSelf = self {
@ -825,6 +1107,27 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
} else {
self.hasPictureInPicture = false
}
if let contentInfo = item.contentInfo, case let .message(message) = contentInfo {
var file: TelegramMediaFile?
var isWebpage = false
for m in message.media {
if let m = m as? TelegramMediaFile, m.isVideo {
file = m
break
} else if let m = m as? TelegramMediaWebpage, case let .Loaded(content) = m.content, let f = content.file, f.isVideo {
file = f
isWebpage = true
break
}
}
if !isWebpage, let file = file, !file.isAnimated {
let moreMenuItem = UIBarButtonItem(customDisplayNode: self.moreBarButton)!
barButtonItems.append(moreMenuItem)
}
}
self._rightBarButtonItems.set(.single(barButtonItems))
videoNode.playbackCompleted = { [weak self, weak videoNode] in
@ -1001,18 +1304,21 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
var isAnimated = false
var seek = MediaPlayerSeek.start
var playbackRate: Double = 1.0
if let item = self.item {
if let content = item.content as? NativeVideoContent {
isAnimated = content.fileReference.media.isAnimated
if let time = item.timecode {
seek = .timecode(time)
}
playbackRate = item.playbackRate
} else if let _ = item.content as? WebEmbedVideoContent {
if let time = item.timecode {
seek = .timecode(time)
}
}
}
videoNode.setBaseRate(playbackRate)
if isAnimated {
videoNode.seek(0.0)
videoNode.play()
@ -1615,7 +1921,208 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
}
}
}
private func contentInfo() -> (message: Message, file: TelegramMediaFile, isWebpage: Bool)? {
guard let item = self.item else {
return nil
}
if let contentInfo = item.contentInfo, case let .message(message) = contentInfo {
var file: TelegramMediaFile?
var isWebpage = false
for m in message.media {
if let m = m as? TelegramMediaFile, m.isVideo {
file = m
break
} else if let m = m as? TelegramMediaWebpage, case let .Loaded(content) = m.content, let f = content.file, f.isVideo {
file = f
isWebpage = true
break
}
}
if let file = file {
return (message, file, isWebpage)
}
}
return nil
}
private func canDelete() -> Bool {
guard let (message, _, _) = self.contentInfo() else {
return false
}
var canDelete = false
if let peer = message.peers[message.id.peerId] {
if peer is TelegramUser || peer is TelegramSecretChat {
canDelete = true
} else if let _ = peer as? TelegramGroup {
canDelete = true
} else if let channel = peer as? TelegramChannel {
if message.flags.contains(.Incoming) {
canDelete = channel.hasPermission(.deleteAllMessages)
} else {
canDelete = true
}
} else {
canDelete = false
}
} else {
canDelete = false
}
return canDelete
}
@objc private func moreButtonPressed() {
self.moreBarButton.play()
self.moreBarButton.contextAction?(self.moreBarButton.containerNode, nil)
}
private func openMoreMenu(sourceNode: ASDisplayNode, gesture: ContextGesture?) {
let items: Signal<[ContextMenuItem], NoError> = self.contextMenuMainItems()
guard let controller = self.baseNavigationController()?.topViewController as? ViewController else {
return
}
let contextController = ContextController(account: self.context.account, presentationData: self.presentationData.withUpdated(theme: defaultDarkColorPresentationTheme), source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceNode: self.moreBarButton.referenceNode)), items: items, reactionItems: [], gesture: gesture)
self.isShowingContextMenuPromise.set(true)
controller.presentInGlobalOverlay(contextController)
contextController.dismissed = { [weak self] in
Queue.mainQueue().after(0.1, {
self?.isShowingContextMenuPromise.set(false)
})
}
}
private func speedList() -> [(String, String, Double)] {
let speedList: [(String, String, Double)] = [
("0.5x", "0.5x", 0.5),
("Normal", "1x", 1.0),
("1.5x", "1.5x", 1.5),
("2x", "2x", 2.0)
]
return speedList
}
private func contextMenuMainItems() -> Signal<[ContextMenuItem], NoError> {
guard let videoNode = self.videoNode else {
return .single([])
}
return videoNode.status
|> take(1)
|> deliverOnMainQueue
|> map { [weak self] status -> [ContextMenuItem] in
guard let status = status, let strongSelf = self else {
return []
}
var items: [ContextMenuItem] = []
var speedValue: String = "Normal"
var speedIconText: String = "1x"
for (text, iconText, speed) in strongSelf.speedList() {
if abs(speed - status.baseRate) < 0.01 {
speedValue = text
speedIconText = iconText
break
}
}
items.append(.action(ContextMenuActionItem(text: "Playback Speed", textLayout: .secondLineWithValue(speedValue), icon: { theme in
return optionsRateImage(rate: speedIconText, isLarge: false, color: theme.contextMenu.primaryColor)
}, action: { c, _ in
guard let strongSelf = self else {
c.dismiss(completion: nil)
return
}
c.setItems(strongSelf.contextMenuSpeedItems())
})))
if let (message, file, isWebpage) = strongSelf.contentInfo(), !isWebpage {
items.append(.action(ContextMenuActionItem(text: "Save to Gallery", icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Download"), color: theme.actionSheet.primaryTextColor) }, action: { _, f in
f(.default)
if let strongSelf = self {
let _ = (SaveToCameraRoll.saveToCameraRoll(context: strongSelf.context, postbox: strongSelf.context.account.postbox, mediaReference: .message(message: MessageReference(message), media: file))
|> deliverOnMainQueue).start(completed: {
guard let strongSelf = self else {
return
}
guard let controller = strongSelf.galleryController() else {
return
}
//TODO:localize
controller.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .mediaSaved(text: "Video Saved"), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root))
})
}
})))
}
if strongSelf.canDelete() {
items.append(.action(ContextMenuActionItem(text: "Delete", textColor: .destructive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { _, f in
f(.default)
if let strongSelf = self {
strongSelf.footerContentNode.deleteButtonPressed()
}
})))
}
return items
}
}
private func contextMenuSpeedItems() -> Signal<[ContextMenuItem], NoError> {
guard let videoNode = self.videoNode else {
return .single([])
}
return videoNode.status
|> take(1)
|> deliverOnMainQueue
|> map { [weak self] status -> [ContextMenuItem] in
guard let status = status, let strongSelf = self else {
return []
}
var items: [ContextMenuItem] = []
for (text, _, rate) in strongSelf.speedList() {
let isSelected = abs(status.baseRate - rate) < 0.01
items.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: { _, f in
f(.default)
guard let strongSelf = self, let videoNode = strongSelf.videoNode else {
return
}
videoNode.setBaseRate(rate)
})))
}
items.append(.separator)
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Common_Back, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.actionSheet.primaryTextColor)
}, action: { c, _ in
guard let strongSelf = self else {
c.dismiss(completion: nil)
return
}
c.setItems(strongSelf.contextMenuMainItems())
})))
return items
}
}
@objc func openStickersButtonPressed() {
if let content = self.item?.content as? NativeVideoContent {
let media = content.fileReference.abstract
@ -1667,6 +2174,20 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
}
override func footerContent() -> Signal<(GalleryFooterContentNode?, GalleryOverlayContentNode?), NoError> {
return .single((self.footerContentNode, self.overlayContentNode))
return .single((self.footerContentNode, nil))
}
}
private final class HeaderContextReferenceContentSource: ContextReferenceContentSource {
private let controller: ViewController
private let sourceNode: ContextReferenceContentNode
init(controller: ViewController, sourceNode: ContextReferenceContentNode) {
self.controller = controller
self.sourceNode = sourceNode
}
func transitionInfo() -> ContextControllerReferenceViewInfo? {
return ContextControllerReferenceViewInfo(referenceNode: self.sourceNode, contentAreaInScreenSpace: UIScreen.main.bounds)
}
}

View File

@ -113,7 +113,7 @@ public struct InstantPageGalleryEntry: Equatable {
nativeId = .instantPage(self.pageId, file.fileId)
}
return UniversalVideoGalleryItem(context: context, presentationData: presentationData, content: NativeVideoContent(id: nativeId, fileReference: .webPage(webPage: WebpageReference(webPage), media: file), streamVideo: isMediaStreamable(media: file) ? .conservative : .none), originData: nil, indexData: indexData, contentInfo: .webPage(webPage, file, nil), caption: caption, credit: credit, fromPlayingVideo: fromPlayingVideo, landscape: landscape, performAction: { _ in }, openActionOptions: { _ in }, storeMediaPlaybackState: { _, _ in }, present: { _, _ in })
return UniversalVideoGalleryItem(context: context, presentationData: presentationData, content: NativeVideoContent(id: nativeId, fileReference: .webPage(webPage: WebpageReference(webPage), media: file), streamVideo: isMediaStreamable(media: file) ? .conservative : .none), originData: nil, indexData: indexData, contentInfo: .webPage(webPage, file, nil), caption: caption, credit: credit, fromPlayingVideo: fromPlayingVideo, landscape: landscape, performAction: { _ in }, openActionOptions: { _ in }, storeMediaPlaybackState: { _, _, _ in }, present: { _, _ in })
} else {
var representations: [TelegramMediaImageRepresentation] = []
representations.append(contentsOf: file.previewRepresentations)
@ -135,12 +135,12 @@ public struct InstantPageGalleryEntry: Equatable {
present(gallery, InstantPageGalleryControllerPresentationArguments(transitionArguments: { entry -> GalleryTransitionArguments? in
return makeArguments()
}))
}), caption: NSAttributedString(string: ""), fromPlayingVideo: fromPlayingVideo, landscape: landscape, performAction: { _ in }, openActionOptions: { _ in }, storeMediaPlaybackState: { _, _ in }, present: { _, _ in })
}), caption: NSAttributedString(string: ""), fromPlayingVideo: fromPlayingVideo, landscape: landscape, performAction: { _ in }, openActionOptions: { _ in }, storeMediaPlaybackState: { _, _, _ in }, present: { _, _ in })
} else {
if let content = WebEmbedVideoContent(webPage: embedWebpage, webpageContent: webpageContent, openUrl: { url in
}) {
return UniversalVideoGalleryItem(context: context, presentationData: presentationData, content: content, originData: nil, indexData: nil, contentInfo: .webPage(webPage, embedWebpage, nil), caption: NSAttributedString(string: ""), fromPlayingVideo: fromPlayingVideo, landscape: landscape, performAction: { _ in }, openActionOptions: { _ in }, storeMediaPlaybackState: { _, _ in }, present: { _, _ in })
return UniversalVideoGalleryItem(context: context, presentationData: presentationData, content: content, originData: nil, indexData: nil, contentInfo: .webPage(webPage, embedWebpage, nil), caption: NSAttributedString(string: ""), fromPlayingVideo: fromPlayingVideo, landscape: landscape, performAction: { _ in }, openActionOptions: { _ in }, storeMediaPlaybackState: { _, _, _ in }, present: { _, _ in })
} else {
preconditionFailure()
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "smallscreen_30.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,161 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 5.334991 5.334999 cm
0.000000 0.000000 0.000000 scn
6.665000 19.330002 m
7.032269 19.330002 7.330000 19.032270 7.330000 18.665001 c
7.330000 15.865002 l
7.330000 15.837518 l
7.330000 15.837514 l
7.330000 15.837511 l
7.330009 15.300820 7.330016 14.857977 7.300543 14.497252 c
7.269935 14.122624 7.204253 13.778399 7.039532 13.455116 c
6.784030 12.953665 6.376337 12.545971 5.874885 12.290468 c
5.551602 12.125748 5.207376 12.060066 4.832748 12.029457 c
4.472024 11.999985 4.029181 11.999992 3.492490 12.000001 c
3.492487 12.000001 l
3.492483 12.000001 l
3.465000 12.000001 l
0.665000 12.000001 l
0.297731 12.000001 0.000000 12.297731 0.000000 12.665001 c
0.000000 13.032270 0.297731 13.330001 0.665000 13.330001 c
3.465000 13.330001 l
4.036026 13.330001 4.424301 13.330519 4.724444 13.355041 c
5.016824 13.378929 5.166537 13.422241 5.271077 13.475508 c
5.522274 13.603498 5.726503 13.807728 5.854494 14.058924 c
5.907760 14.163464 5.951072 14.313177 5.974960 14.605556 c
5.999483 14.905701 6.000000 15.293976 6.000000 15.865002 c
6.000000 18.665001 l
6.000000 19.032270 6.297730 19.330002 6.665000 19.330002 c
h
13.330000 18.665001 m
13.330000 19.032270 13.032269 19.330002 12.665000 19.330002 c
12.297730 19.330002 12.000000 19.032270 12.000000 18.665001 c
12.000000 15.865002 l
12.000000 15.837526 l
12.000000 15.837497 l
11.999991 15.300814 11.999985 14.857974 12.029457 14.497252 c
12.060065 14.122624 12.125747 13.778399 12.290467 13.455116 c
12.545970 12.953665 12.953663 12.545971 13.455115 12.290468 c
13.778399 12.125748 14.122623 12.060066 14.497252 12.029457 c
14.857978 11.999985 15.300823 11.999992 15.837517 12.000001 c
15.865000 12.000001 l
18.665001 12.000001 l
19.032270 12.000001 19.330002 12.297731 19.330002 12.665001 c
19.330002 13.032270 19.032270 13.330001 18.665001 13.330001 c
15.865000 13.330001 l
15.293975 13.330001 14.905700 13.330519 14.605556 13.355041 c
14.313176 13.378929 14.163464 13.422241 14.058923 13.475508 c
13.807726 13.603498 13.603498 13.807728 13.475507 14.058924 c
13.422240 14.163464 13.378928 14.313177 13.355040 14.605556 c
13.330517 14.905701 13.330000 15.293976 13.330000 15.865002 c
13.330000 18.665001 l
h
15.865000 7.330001 m
15.837525 7.330001 l
15.837497 7.330001 l
15.300811 7.330009 14.857973 7.330016 14.497252 7.300544 c
14.122623 7.269936 13.778399 7.204254 13.455115 7.039534 c
12.953663 6.784031 12.545970 6.376338 12.290467 5.874886 c
12.125747 5.551602 12.060065 5.207377 12.029457 4.832749 c
11.999985 4.472028 11.999991 4.029190 12.000000 3.492504 c
12.000000 3.492476 l
12.000000 3.465000 l
12.000000 0.665001 l
12.000000 0.297731 12.297730 0.000000 12.665000 0.000000 c
13.032269 0.000000 13.330000 0.297731 13.330000 0.665001 c
13.330000 3.465000 l
13.330000 4.036026 13.330517 4.424301 13.355040 4.724444 c
13.378928 5.016825 13.422240 5.166537 13.475507 5.271078 c
13.603498 5.522275 13.807726 5.726503 14.058923 5.854494 c
14.163464 5.907761 14.313176 5.951073 14.605556 5.974961 c
14.905700 5.999484 15.293975 6.000001 15.865000 6.000001 c
18.665001 6.000001 l
19.032270 6.000001 19.330002 6.297731 19.330002 6.665001 c
19.330002 7.032270 19.032270 7.330001 18.665001 7.330001 c
15.865000 7.330001 l
h
3.465000 6.000001 m
4.036026 6.000001 4.424301 5.999484 4.724444 5.974961 c
5.016824 5.951073 5.166537 5.907761 5.271077 5.854494 c
5.522274 5.726503 5.726503 5.522275 5.854494 5.271078 c
5.907760 5.166537 5.951072 5.016825 5.974960 4.724444 c
5.999483 4.424301 6.000000 4.036026 6.000000 3.465000 c
6.000000 0.665001 l
6.000000 0.297731 6.297730 0.000000 6.665000 0.000000 c
7.032269 0.000000 7.330000 0.297731 7.330000 0.665001 c
7.330000 3.465000 l
7.330000 3.492484 l
7.330009 4.029178 7.330016 4.472023 7.300543 4.832749 c
7.269935 5.207377 7.204253 5.551602 7.039532 5.874886 c
6.784030 6.376338 6.376337 6.784031 5.874885 7.039534 c
5.551602 7.204254 5.207376 7.269936 4.832748 7.300544 c
4.472027 7.330016 4.029188 7.330009 3.492504 7.330001 c
3.492474 7.330001 l
3.465000 7.330001 l
0.665000 7.330001 l
0.297731 7.330001 0.000000 7.032270 0.000000 6.665001 c
0.000000 6.297731 0.297731 6.000001 0.665000 6.000001 c
3.465000 6.000001 l
h
f*
n
Q
endstream
endobj
3 0 obj
4194
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 30.000000 30.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Type /Catalog
/Pages 5 0 R
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000004284 00000 n
0000004307 00000 n
0000004480 00000 n
0000004554 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
4613
%%EOF

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "down_24.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,92 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 3.334999 3.334999 cm
0.000000 0.000000 0.000000 scn
8.665000 16.000002 m
4.613991 16.000002 1.330000 12.716011 1.330000 8.665002 c
1.330000 4.613994 4.613991 1.330002 8.665000 1.330002 c
12.716008 1.330002 16.000000 4.613994 16.000000 8.665002 c
16.000000 12.716011 12.716008 16.000002 8.665000 16.000002 c
h
0.000000 8.665002 m
0.000000 13.450549 3.879453 17.330002 8.665000 17.330002 c
13.450547 17.330002 17.330002 13.450549 17.330002 8.665002 c
17.330002 3.879455 13.450547 0.000000 8.665000 0.000000 c
3.879453 0.000000 0.000000 3.879455 0.000000 8.665002 c
h
8.665000 12.830002 m
9.032269 12.830002 9.330000 12.532271 9.330000 12.165002 c
9.330000 6.770453 l
11.194775 8.635228 l
11.454473 8.894926 11.875527 8.894926 12.135225 8.635228 c
12.394924 8.375529 12.394924 7.954474 12.135225 7.694776 c
9.135226 4.694776 l
8.875527 4.435078 8.454473 4.435078 8.194774 4.694776 c
5.194774 7.694776 l
4.935075 7.954474 4.935075 8.375529 5.194774 8.635228 c
5.454473 8.894926 5.875527 8.894926 6.135226 8.635228 c
8.000000 6.770453 l
8.000000 12.165002 l
8.000000 12.532271 8.297730 12.830002 8.665000 12.830002 c
h
f*
n
Q
endstream
endobj
3 0 obj
1188
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 24.000000 24.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Type /Catalog
/Pages 5 0 R
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000001278 00000 n
0000001301 00000 n
0000001474 00000 n
0000001548 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
1607
%%EOF

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "fullscreen_30.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,161 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 5.334991 5.334970 cm
0.000000 0.000000 0.000000 scn
15.465001 18.000031 m
16.036026 18.000031 16.424301 17.999512 16.724445 17.974989 c
17.016825 17.951101 17.166538 17.907789 17.271078 17.854523 c
17.522274 17.726532 17.726503 17.522303 17.854494 17.271107 c
17.907761 17.166567 17.951073 17.016853 17.974960 16.724474 c
17.999483 16.424330 18.000000 16.036055 18.000000 15.465029 c
18.000000 12.665030 l
18.000000 12.297760 18.297731 12.000029 18.665001 12.000029 c
19.032270 12.000029 19.330002 12.297760 19.330002 12.665030 c
19.330002 15.465029 l
19.330002 15.492513 l
19.330002 15.492586 l
19.330009 16.029245 19.330015 16.472069 19.300545 16.832777 c
19.269936 17.207407 19.204254 17.551632 19.039534 17.874914 c
18.784031 18.376366 18.376337 18.784060 17.874886 19.039562 c
17.551603 19.204283 17.207378 19.269964 16.832748 19.300573 c
16.472025 19.330046 16.029184 19.330038 15.492496 19.330030 c
15.492476 19.330030 l
15.465001 19.330030 l
12.665001 19.330030 l
12.297731 19.330030 12.000001 19.032299 12.000001 18.665030 c
12.000001 18.297760 12.297731 18.000031 12.665001 18.000031 c
15.465001 18.000031 l
h
3.865001 19.330030 m
3.837527 19.330030 l
3.837508 19.330030 l
3.300819 19.330038 2.857976 19.330046 2.497253 19.300573 c
2.122624 19.269964 1.778399 19.204283 1.455116 19.039562 c
0.953664 18.784060 0.545971 18.376366 0.290469 17.874914 c
0.125748 17.551632 0.060066 17.207407 0.029458 16.832777 c
-0.000015 16.472054 -0.000008 16.029211 0.000000 15.492522 c
0.000000 15.492504 l
0.000000 15.465029 l
0.000000 12.665030 l
0.000000 12.297760 0.297732 12.000029 0.665001 12.000029 c
1.032270 12.000029 1.330001 12.297760 1.330001 12.665030 c
1.330001 15.465029 l
1.330001 16.036055 1.330518 16.424330 1.355041 16.724474 c
1.378929 17.016853 1.422241 17.166567 1.475507 17.271107 c
1.603498 17.522303 1.807727 17.726532 2.058923 17.854523 c
2.163464 17.907789 2.313177 17.951101 2.605557 17.974989 c
2.905700 17.999512 3.293975 18.000031 3.865001 18.000031 c
6.665001 18.000031 l
7.032271 18.000031 7.330001 18.297760 7.330001 18.665030 c
7.330001 19.032299 7.032271 19.330030 6.665001 19.330030 c
3.865001 19.330030 l
h
1.330001 6.665030 m
1.330001 7.032299 1.032270 7.330029 0.665001 7.330029 c
0.297732 7.330029 0.000000 7.032299 0.000000 6.665030 c
0.000000 3.865030 l
0.000000 3.837555 l
0.000000 3.837535 l
-0.000008 3.300846 -0.000015 2.858006 0.029458 2.497282 c
0.060066 2.122652 0.125748 1.778427 0.290469 1.455145 c
0.545971 0.953693 0.953664 0.546000 1.455116 0.290497 c
1.778399 0.125776 2.122624 0.060095 2.497253 0.029486 c
2.857963 0.000015 3.300784 0.000021 3.837445 0.000029 c
3.837518 0.000029 l
3.865001 0.000029 l
6.665001 0.000029 l
7.032271 0.000029 7.330001 0.297760 7.330001 0.665030 c
7.330001 1.032299 7.032271 1.330030 6.665001 1.330030 c
3.865001 1.330030 l
3.293975 1.330030 2.905700 1.330547 2.605557 1.355070 c
2.313177 1.378958 2.163464 1.422270 2.058923 1.475536 c
1.807727 1.603527 1.603498 1.807756 1.475507 2.058952 c
1.422241 2.163492 1.378929 2.313206 1.355041 2.605585 c
1.330518 2.905729 1.330001 3.294004 1.330001 3.865030 c
1.330001 6.665030 l
h
18.665001 7.330029 m
19.032270 7.330029 19.330002 7.032299 19.330002 6.665030 c
19.330002 3.865030 l
19.330002 3.837546 l
19.330002 3.837475 l
19.330009 3.300812 19.330015 2.857990 19.300545 2.497282 c
19.269936 2.122652 19.204254 1.778427 19.039534 1.455145 c
18.784031 0.953693 18.376337 0.546000 17.874886 0.290497 c
17.551603 0.125776 17.207378 0.060095 16.832748 0.029486 c
16.472040 0.000015 16.029217 0.000021 15.492556 0.000029 c
15.492484 0.000029 l
15.465001 0.000029 l
12.665001 0.000029 l
12.297731 0.000029 12.000001 0.297760 12.000001 0.665030 c
12.000001 1.032299 12.297731 1.330030 12.665001 1.330030 c
15.465001 1.330030 l
16.036026 1.330030 16.424301 1.330547 16.724445 1.355070 c
17.016825 1.378958 17.166538 1.422270 17.271078 1.475536 c
17.522274 1.603527 17.726503 1.807756 17.854494 2.058952 c
17.907761 2.163492 17.951073 2.313206 17.974960 2.605585 c
17.999483 2.905729 18.000000 3.294004 18.000000 3.865030 c
18.000000 6.665030 l
18.000000 7.032299 18.297731 7.330029 18.665001 7.330029 c
h
f*
n
Q
endstream
endobj
3 0 obj
4198
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 30.000000 30.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Type /Catalog
/Pages 5 0 R
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000004288 00000 n
0000004311 00000 n
0000004484 00000 n
0000004558 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
4617
%%EOF

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "playspeed_24.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,113 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 3.335007 3.334999 cm
0.000000 0.000000 0.000000 scn
0.000007 16.665001 m
0.000007 17.032270 0.297737 17.330002 0.665007 17.330002 c
1.165007 17.330002 l
1.532276 17.330002 1.830007 17.032270 1.830007 16.665001 c
1.830007 16.297733 1.532276 16.000002 1.165007 16.000002 c
0.665007 16.000002 l
0.297737 16.000002 0.000007 16.297733 0.000007 16.665001 c
h
9.665000 17.330002 m
9.297730 17.330002 9.000000 17.032270 9.000000 16.665001 c
9.000000 16.297731 9.297730 16.000000 9.665000 16.000000 c
16.665001 16.000000 l
17.032270 16.000000 17.330002 16.297731 17.330002 16.665001 c
17.330002 17.032270 17.032270 17.330002 16.665001 17.330002 c
9.665000 17.330002 l
h
9.665000 1.330002 m
9.297730 1.330002 9.000000 1.032270 9.000000 0.665001 c
9.000000 0.297731 9.297730 0.000000 9.665000 0.000000 c
16.665001 0.000000 l
17.032270 0.000000 17.330002 0.297731 17.330002 0.665001 c
17.330002 1.032270 17.032270 1.330002 16.665001 1.330002 c
9.665000 1.330002 l
h
0.665000 1.330002 m
0.297731 1.330002 0.000000 1.032270 0.000000 0.665001 c
0.000000 0.297731 0.297731 0.000000 0.665000 0.000000 c
1.165000 0.000000 l
1.532269 0.000000 1.830000 0.297731 1.830000 0.665001 c
1.830000 1.032270 1.532269 1.330002 1.165000 1.330002 c
0.665000 1.330002 l
h
3.000000 16.665001 m
3.000000 17.032270 3.297731 17.330002 3.665000 17.330002 c
7.165000 17.330002 l
7.532269 17.330002 7.830000 17.032270 7.830000 16.665001 c
7.830000 16.297731 7.532269 16.000000 7.165000 16.000000 c
3.665000 16.000000 l
3.297731 16.000000 3.000000 16.297731 3.000000 16.665001 c
h
3.665000 1.330002 m
3.297731 1.330002 3.000000 1.032270 3.000000 0.665001 c
3.000000 0.297731 3.297731 0.000000 3.665000 0.000000 c
7.165000 0.000000 l
7.532269 0.000000 7.830000 0.297731 7.830000 0.665001 c
7.830000 1.032270 7.532269 1.330002 7.165000 1.330002 c
3.665000 1.330002 l
h
f*
n
Q
endstream
endobj
3 0 obj
1901
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 24.000000 24.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Type /Catalog
/Pages 5 0 R
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000001991 00000 n
0000002014 00000 n
0000002187 00000 n
0000002261 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
2320
%%EOF

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "playspeed_30.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,113 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 5.335007 5.334999 cm
0.000000 0.000000 0.000000 scn
0.000000 18.665001 m
0.000000 19.032270 0.297731 19.330002 0.665000 19.330002 c
1.665000 19.330002 l
2.032269 19.330002 2.330000 19.032270 2.330000 18.665001 c
2.330000 18.297733 2.032269 18.000002 1.665000 18.000002 c
0.665000 18.000002 l
0.297731 18.000002 0.000000 18.297733 0.000000 18.665001 c
h
11.665000 19.330002 m
11.297730 19.330002 11.000000 19.032270 11.000000 18.665001 c
11.000000 18.297733 11.297730 18.000002 11.665000 18.000002 c
18.665001 18.000002 l
19.032269 18.000002 19.330000 18.297733 19.330000 18.665001 c
19.330000 19.032270 19.032269 19.330002 18.665001 19.330002 c
11.665000 19.330002 l
h
11.665000 1.330002 m
11.297730 1.330002 11.000000 1.032270 11.000000 0.665001 c
11.000000 0.297731 11.297730 0.000000 11.665000 0.000000 c
18.665001 0.000000 l
19.032269 0.000000 19.330000 0.297731 19.330000 0.665001 c
19.330000 1.032270 19.032269 1.330002 18.665001 1.330002 c
11.665000 1.330002 l
h
0.665000 1.330002 m
0.297731 1.330002 0.000000 1.032270 0.000000 0.665001 c
0.000000 0.297731 0.297731 0.000000 0.665000 0.000000 c
1.665000 0.000000 l
2.032269 0.000000 2.330000 0.297731 2.330000 0.665001 c
2.330000 1.032270 2.032269 1.330002 1.665000 1.330002 c
0.665000 1.330002 l
h
4.000000 18.665001 m
4.000000 19.032270 4.297730 19.330002 4.665000 19.330002 c
8.665000 19.330002 l
9.032269 19.330002 9.330000 19.032270 9.330000 18.665001 c
9.330000 18.297733 9.032269 18.000002 8.665000 18.000002 c
4.665000 18.000002 l
4.297730 18.000002 4.000000 18.297733 4.000000 18.665001 c
h
4.665000 1.330002 m
4.297730 1.330002 4.000000 1.032270 4.000000 0.665001 c
4.000000 0.297731 4.297730 0.000000 4.665000 0.000000 c
8.665000 0.000000 l
9.032269 0.000000 9.330000 0.297731 9.330000 0.665001 c
9.330000 1.032270 9.032269 1.330002 8.665000 1.330002 c
4.665000 1.330002 l
h
f*
n
Q
endstream
endobj
3 0 obj
1917
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 30.000000 30.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Type /Catalog
/Pages 5 0 R
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000002007 00000 n
0000002030 00000 n
0000002203 00000 n
0000002277 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
2336
%%EOF

View File

@ -804,13 +804,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
if let strongSelf = self {
strongSelf.controllerInteraction?.addContact(phoneNumber)
}
}, storeMediaPlaybackState: { [weak self] messageId, timestamp in
}, storeMediaPlaybackState: { [weak self] messageId, timestamp, playbackRate in
guard let strongSelf = self else {
return
}
var storedState: MediaPlaybackStoredState?
if let timestamp = timestamp {
storedState = MediaPlaybackStoredState(timestamp: timestamp, playbackRate: .x1)
storedState = MediaPlaybackStoredState(timestamp: timestamp, playbackRate: AudioPlaybackRate(playbackRate))
}
let _ = updateMediaPlaybackStoredStateInteractively(postbox: strongSelf.context.account.postbox, messageId: messageId, state: storedState).start()
}, editMedia: { [weak self] messageId, snapshots, transitionCompletion in

View File

@ -3072,13 +3072,13 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
}
}, completed: {})
}
}, storeMediaPlaybackState: { [weak self] messageId, timestamp in
}, storeMediaPlaybackState: { [weak self] messageId, timestamp, playbackRate in
guard let strongSelf = self else {
return
}
var storedState: MediaPlaybackStoredState?
if let timestamp = timestamp {
storedState = MediaPlaybackStoredState(timestamp: timestamp, playbackRate: .x1)
storedState = MediaPlaybackStoredState(timestamp: timestamp, playbackRate: AudioPlaybackRate(playbackRate) ?? .x1)
}
let _ = updateMediaPlaybackStoredStateInteractively(postbox: strongSelf.context.account.postbox, messageId: messageId, state: storedState).start()
}, editMedia: { [weak self] messageId, snapshots, transitionCompletion in

View File

@ -442,19 +442,7 @@ final class SharedMediaPlayer {
case let .setBaseRate(baseRate):
self.playbackRate = baseRate
if let playbackItem = self.playbackItem {
let rateValue: Double
switch baseRate {
case .x1:
rateValue = 1.0
case .x2:
rateValue = 1.8
case .x4:
rateValue = 4.0
case .x8:
rateValue = 8.0
case .x16:
rateValue = 16.0
}
let rateValue: Double = baseRate.doubleValue
switch playbackItem {
case let .audio(player):
player.setBaseRate(rateValue)

View File

@ -15,15 +15,25 @@ public enum MusicPlaybackSettingsLooping: Int32 {
}
public enum AudioPlaybackRate: Int32 {
case x0_5 = 500
case x1 = 1000
case x1_5 = 1500
case x2 = 2000
case x4 = 4000
case x8 = 8000
case x16 = 16000
var doubleValue: Double {
public var doubleValue: Double {
return Double(self.rawValue) / 1000.0
}
public init(_ value: Double) {
if let resolved = AudioPlaybackRate(rawValue: Int32(value * 1000.0)) {
self = resolved
} else {
self = .x1
}
}
}
public struct MusicPlaybackSettings: PreferencesEntry, Equatable {