Video playback improvements

This commit is contained in:
Isaac 2024-10-22 16:37:53 +04:00
parent df249f5302
commit a57d64cfe3
14 changed files with 594 additions and 322 deletions

View File

@ -104,8 +104,9 @@ private final class InnerActionsContainerNode: ASDisplayNode {
}
}
case let .custom(item, _):
itemNodes.append(.custom(item.node(presentationData: presentationData, getController: getController, actionSelected: actionSelected)))
if i != items.count - 1 {
let itemNode = item.node(presentationData: presentationData, getController: getController, actionSelected: actionSelected)
itemNodes.append(.custom(itemNode))
if i != items.count - 1 && itemNode.needsSeparator {
switch items[i + 1] {
case .action, .custom:
let separatorNode = ASDisplayNode()

View File

@ -226,6 +226,14 @@ public protocol ContextMenuCustomNode: ASDisplayNode {
func canBeHighlighted() -> Bool
func updateIsHighlighted(isHighlighted: Bool)
func performAction()
var needsSeparator: Bool { get }
}
public extension ContextMenuCustomNode {
var needsSeparator: Bool {
return true
}
}
public protocol ContextMenuCustomItem {

View File

@ -629,7 +629,7 @@ private final class ContextControllerActionsListCustomItemNode: ASDisplayNode, C
private let requestDismiss: (ContextMenuActionResult) -> Void
private var presentationData: PresentationData?
private var itemNode: ContextMenuCustomNode?
private(set) var itemNode: ContextMenuCustomNode?
init(
getController: @escaping () -> ContextControllerProtocol?,
@ -862,18 +862,28 @@ public final class ContextControllerActionsListStackItem: ContextControllerActio
if let separatorNode = item.separatorNode {
itemTransition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: itemFrame.minX, y: itemFrame.maxY), size: CGSize(width: itemFrame.width, height: UIScreenPixel)), beginWithCurrentState: true)
var separatorHidden = false
if i != self.itemNodes.count - 1 {
switch self.items[i + 1] {
case .separator:
separatorNode.isHidden = true
separatorHidden = true
case .action:
separatorNode.isHidden = false
separatorHidden = false
case .custom:
separatorNode.isHidden = false
separatorHidden = false
}
} else {
separatorNode.isHidden = true
separatorHidden = true
}
if let itemContainerNode = item.node as? ContextControllerActionsListCustomItemNode, let itemNode = itemContainerNode.itemNode {
if !itemNode.needsSeparator {
separatorHidden = true
}
}
separatorNode.isHidden = separatorHidden
}
itemNodeLayout.apply(itemSize, itemTransition)

View File

@ -45,7 +45,8 @@ swift_library(
"//submodules/TelegramUI/Components/TextNodeWithEntities:TextNodeWithEntities",
"//submodules/TelegramUI/Components/AnimationCache:AnimationCache",
"//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer",
"//submodules/TelegramUI/Components/SliderContextItem:SliderContextItem",
"//submodules/TelegramUI/Components/SliderContextItem",
"//submodules/TelegramUI/Components/SectionTitleContextItem",
"//submodules/TooltipUI",
"//submodules/TelegramNotices",
"//submodules/Pasteboard",

View File

@ -28,6 +28,7 @@ import AdUI
import AdsInfoScreen
import AdsReportScreen
import SaveProgressScreen
import SectionTitleContextItem
public enum UniversalVideoGalleryItemContentInfo {
case message(Message, Int?)
@ -503,6 +504,111 @@ final class MoreHeaderButton: HighlightableButtonNode {
}
}
final class SettingsHeaderButton: HighlightableButtonNode {
let referenceNode: ContextReferenceContentNode
let containerNode: ContextControllerSourceNode
private let iconNode: ASImageNode
private let iconDotNode: ASImageNode
private var isMenuOpen: Bool = false
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
self.iconDotNode = ASImageNode()
self.iconDotNode.displaysAsynchronously = false
self.iconDotNode.displayWithoutProcessing = true
super.init()
self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/NavigationSettingsNoDot"), color: .white)
self.iconDotNode.image = generateFilledCircleImage(diameter: 4.0, color: .white)
self.containerNode.addSubnode(self.referenceNode)
self.referenceNode.addSubnode(self.iconNode)
self.referenceNode.addSubnode(self.iconDotNode)
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: 26.0, height: 44.0))
self.referenceNode.frame = self.containerNode.bounds
self.hitTestSlop = UIEdgeInsets(top: 0.0, left: -4.0, bottom: 0.0, right: -4.0)
if let image = self.iconNode.image {
let iconFrame = 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.position = iconFrame.center
self.iconNode.bounds = CGRect(origin: CGPoint(), size: iconFrame.size)
if let dotImage = self.iconDotNode.image {
let dotFrame = CGRect(origin: CGPoint(x: iconFrame.minX + floorToScreenPixels((iconFrame.width - dotImage.size.width) * 0.5), y: iconFrame.minY + floorToScreenPixels((iconFrame.height - dotImage.size.height) * 0.5)), size: dotImage.size)
self.iconDotNode.position = dotFrame.center
self.iconDotNode.bounds = CGRect(origin: CGPoint(), size: dotFrame.size)
}
}
}
override func didLoad() {
super.didLoad()
self.view.isOpaque = false
}
override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
return CGSize(width: wide ? 32.0 : 22.0, height: 44.0)
}
func onLayout() {
}
func setIsMenuOpen(isMenuOpen: Bool) {
if self.isMenuOpen == isMenuOpen {
return
}
self.isMenuOpen = isMenuOpen
let rotationTransition: ContainedViewLayoutTransition = .animated(duration: 0.35, curve: .spring)
rotationTransition.updateTransform(node: self.iconNode, transform: CGAffineTransformMakeRotation(isMenuOpen ? (CGFloat.pi * 2.0 / 6.0) : 0.0))
self.iconNode.layer.animateScale(from: 1.0, to: 1.07, duration: 0.1, removeOnCompletion: false, completion: { [weak self] finished in
guard let self, finished else {
return
}
self.iconNode.layer.animateScale(from: 1.07, to: 1.0, duration: 0.1, removeOnCompletion: false)
})
self.iconDotNode.layer.animateScale(from: 1.0, to: 0.8, duration: 0.1, removeOnCompletion: false, completion: { [weak self] finished in
guard let self, finished else {
return
}
self.iconDotNode.layer.animateScale(from: 0.8, to: 1.0, duration: 0.1, removeOnCompletion: false)
})
}
}
@available(iOS 15.0, *)
private final class PictureInPictureContentImpl: NSObject, PictureInPictureContent, AVPictureInPictureControllerDelegate {
private final class PlaybackDelegate: NSObject, AVPictureInPictureSampleBufferPlaybackDelegate {
@ -1062,7 +1168,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
private var moreBarButtonRate: Double = 1.0
private var moreBarButtonRateTimestamp: Double?
private let settingsBarButton: MoreHeaderButton
private let settingsBarButton: SettingsHeaderButton
private var videoNode: UniversalVideoNode?
private var videoNodeUserInteractionEnabled: Bool = false
@ -1098,6 +1204,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
private let statusDisposable = MetaDisposable()
private let moreButtonStateDisposable = MetaDisposable()
private let settingsButtonStateDisposable = MetaDisposable()
private let mediaPlaybackStateDisposable = MetaDisposable()
private let fetchDisposable = MetaDisposable()
@ -1113,6 +1220,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
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 let isShowingSettingsMenuPromise = ValuePromise<Bool>(false, ignoreRepeated: true)
private let hasExpandedCaptionPromise = Promise<Bool>()
private var hideControlsDisposable: Disposable?
private var automaticPictureInPictureDisposable: Disposable?
@ -1150,7 +1258,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
self.moreBarButton.isUserInteractionEnabled = true
self.moreBarButton.setContent(.more(optionsCircleImage(dark: false)))
self.settingsBarButton = MoreHeaderButton()
self.settingsBarButton = SettingsHeaderButton()
self.settingsBarButton.isUserInteractionEnabled = true
super.init()
@ -1282,9 +1390,9 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
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(), self.isShowingContextMenuPromise.get(), self.hasExpandedCaptionPromise.get())
|> mapToSignal { isPlaying, isInteracting, controlsVisible, isShowingContextMenu, hasExpandedCaptionPromise -> Signal<Void, NoError> in
if isShowingContextMenu || hasExpandedCaptionPromise {
let shouldHideControlsSignal: Signal<Void, NoError> = combineLatest(self.isPlayingPromise.get(), self.isInteractingPromise.get(), self.controlsVisiblePromise.get(), self.isShowingContextMenuPromise.get(), self.isShowingSettingsMenuPromise.get(), self.hasExpandedCaptionPromise.get())
|> mapToSignal { isPlaying, isInteracting, controlsVisible, isShowingContextMenu, isShowingSettingsMenu, hasExpandedCaptionPromise -> Signal<Void, NoError> in
if isShowingContextMenu || isShowingSettingsMenu || hasExpandedCaptionPromise {
return .complete()
}
if isPlaying && !isInteracting && controlsVisible {
@ -1306,6 +1414,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
deinit {
self.statusDisposable.dispose()
self.moreButtonStateDisposable.dispose()
self.settingsButtonStateDisposable.dispose()
self.mediaPlaybackStateDisposable.dispose()
self.scrubbingFrameDisposable?.dispose()
self.hideControlsDisposable?.dispose()
@ -1453,11 +1562,9 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
isAdaptive = true
}
if isAdaptive {
self.settingsBarButton.setContent(.image(generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/NavigationSettingsQAuto"), color: .white)))
} else {
self.settingsBarButton.setContent(.image(generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/NavigationSettings"), color: .white)))
}
//TODO:release
//self.settingsBarButton.setContent(.image(generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/NavigationSettingsQAuto"), color: .white)))
let _ = isAdaptive
let dimensions = item.content.dimensions
if dimensions.height > 0.0 {
@ -1639,6 +1746,14 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
}
}))*/
self.settingsButtonStateDisposable.set((self.isShowingSettingsMenuPromise.get()
|> deliverOnMainQueue).start(next: { [weak self] isShowingSettingsMenu in
guard let self else {
return
}
self.settingsBarButton.setIsMenuOpen(isMenuOpen: isShowingSettingsMenu)
}))
self.statusDisposable.set((combineLatest(queue: .mainQueue(), videoNode.status, mediaFileStatus)
|> deliverOnMainQueue).start(next: { [weak self] value, fetchStatus in
if let strongSelf = self {
@ -2951,25 +3066,42 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
guard let controller = self.baseNavigationController()?.topViewController as? ViewController else {
return
}
var dismissImpl: (() -> Void)?
let items: Signal<[ContextMenuItem], NoError>
let items: Signal<(items: [ContextMenuItem], topItems: [ContextMenuItem]), NoError>
if case let .message(message, _) = self.item?.contentInfo, let _ = message.adAttribute {
items = self.adMenuMainItems()
items = self.adMenuMainItems() |> map { items in
return (items, [])
}
} else {
items = self.contextMenuMainItems(isSettings: isSettings, dismiss: {
dismissImpl?()
})
}
let contextController = ContextController(presentationData: self.presentationData.withUpdated(theme: defaultDarkColorPresentationTheme), source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceNode: sourceNode)), items: items |> map { ContextController.Items(content: .list($0)) }, gesture: gesture)
self.isShowingContextMenuPromise.set(true)
let contextController = ContextController(presentationData: self.presentationData.withUpdated(theme: defaultDarkColorPresentationTheme), source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceNode: sourceNode)), items: items |> map { items in
if !items.topItems.isEmpty {
return ContextController.Items(content: .twoLists(items.items, items.topItems))
} else {
return ContextController.Items(content: .list(items.items))
}
}, gesture: gesture)
if isSettings {
self.isShowingSettingsMenuPromise.set(true)
} else {
self.isShowingContextMenuPromise.set(true)
}
controller.presentInGlobalOverlay(contextController)
dismissImpl = { [weak contextController] in
contextController?.dismiss()
}
contextController.dismissed = { [weak self] in
Queue.mainQueue().after(0.1, {
self?.isShowingContextMenuPromise.set(false)
Queue.mainQueue().after(isSettings ? 0.0 : 0.1, {
if isSettings {
self?.isShowingSettingsMenuPromise.set(false)
} else {
self?.isShowingContextMenuPromise.set(false)
}
})
}
}
@ -3112,9 +3244,9 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
}
private func contextMenuMainItems(isSettings: Bool, dismiss: @escaping () -> Void) -> Signal<[ContextMenuItem], NoError> {
private func contextMenuMainItems(isSettings: Bool, dismiss: @escaping () -> Void) -> Signal<(items: [ContextMenuItem], topItems: [ContextMenuItem]), NoError> {
guard let videoNode = self.videoNode, let item = self.item else {
return .single([])
return .single(([], []))
}
let peer: Signal<EnginePeer?, NoError>
@ -3126,129 +3258,131 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
return combineLatest(queue: Queue.mainQueue(), videoNode.status, peer)
|> take(1)
|> map { [weak self] status, peer -> [ContextMenuItem] in
|> map { [weak self] status, peer -> (items: [ContextMenuItem], topItems: [ContextMenuItem]) in
guard let status = status, let strongSelf = self else {
return []
return ([], [])
}
var topItems: [ContextMenuItem] = []
var items: [ContextMenuItem] = []
if isSettings {
var speedValue: String = strongSelf.presentationData.strings.PlaybackSpeed_Normal
var speedIconText: String = "1x"
var didSetSpeedValue = false
for (text, iconText, speed) in strongSelf.speedList(strings: strongSelf.presentationData.strings) {
if abs(speed - status.baseRate) < 0.01 {
speedValue = text
speedIconText = iconText
didSetSpeedValue = true
break
}
}
if !didSetSpeedValue && status.baseRate != 1.0 {
speedValue = String(format: "%.1fx", status.baseRate)
speedIconText = speedValue
}
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.PlaybackSpeed_Title, 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)
let sliderValuePromise = ValuePromise<Double?>(nil)
topItems.append(.custom(SliderContextItem(title: "Speed", minValue: 0.2, maxValue: 2.5, value: status.baseRate, valueChanged: { [weak self] newValue, _ in
guard let strongSelf = self, let videoNode = strongSelf.videoNode else {
return
}
c?.pushItems(items: strongSelf.contextMenuSpeedItems(dismiss: dismiss) |> map { ContextController.Items(content: .list($0)) })
})))
let newValue = normalizeValue(newValue)
videoNode.setBaseRate(newValue)
if let controller = strongSelf.galleryController() as? GalleryController {
controller.updateSharedPlaybackRate(newValue)
}
sliderValuePromise.set(newValue)
}), true))
if let videoQualityState = strongSelf.videoNode?.videoQualityState(), !videoQualityState.available.isEmpty {
items.append(.separator)
} else {
items.append(.custom(SectionTitleContextItem(text: "PLAYBACK SPEED"), false))
for (text, _, rate) in strongSelf.speedList(strings: strongSelf.presentationData.strings) {
let isSelected = abs(status.baseRate - rate) < 0.01
items.append(.action(ContextMenuActionItem(text: text, icon: { _ in return nil }, iconSource: ContextMenuActionItemIconSource(size: CGSize(width: 24.0, height: 24.0), signal: sliderValuePromise.get()
|> map { value in
if isSelected && value == nil {
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: .white)
} else {
return nil
}
}), action: { _, f in
f(.default)
guard let strongSelf = self, let videoNode = strongSelf.videoNode else {
return
}
videoNode.setBaseRate(rate)
if let controller = strongSelf.galleryController() as? GalleryController {
controller.updateSharedPlaybackRate(rate)
}
})))
}
}
if let videoQualityState = strongSelf.videoNode?.videoQualityState(), !videoQualityState.available.isEmpty {
items.append(.custom(SectionTitleContextItem(text: "VIDEO QUALITY"), false))
//TODO:localize
let qualityText: String
switch videoQualityState.preferred {
case .auto:
do {
let isSelected = videoQualityState.preferred == .auto
let qualityText: String = "Auto"
let textLayout: ContextMenuActionItemTextLayout
if videoQualityState.current != 0 {
qualityText = "Auto (\(videoQualityState.current)p)"
textLayout = .secondLineWithValue("\(videoQualityState.current)p")
} else {
qualityText = "Auto"
textLayout = .singleLine
}
case let .quality(value):
qualityText = "\(value)p"
items.append(.action(ContextMenuActionItem(text: qualityText, textLayout: textLayout, icon: { _ in
if isSelected {
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: .white)
} else {
return nil
}
}, action: { [weak self] _, f in
f(.default)
guard let self, let videoNode = self.videoNode else {
return
}
videoNode.setVideoQuality(.auto)
//TODO:release
//self.settingsBarButton.setContent(.image(generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/NavigationSettingsQAuto"), color: .white)))
/*if let controller = strongSelf.galleryController() as? GalleryController {
controller.updateSharedPlaybackRate(rate)
}*/
})))
}
items.append(.action(ContextMenuActionItem(text: "Video Quality", textLayout: .secondLineWithValue(qualityText), icon: { _ in
return nil
}, action: { c, _ in
guard let strongSelf = self else {
c?.dismiss(completion: nil)
return
for quality in videoQualityState.available {
let isSelected = videoQualityState.preferred == .quality(quality)
let qualityTitle: String
if quality <= 360 {
qualityTitle = "Low"
} else if quality <= 480 {
qualityTitle = "Medium"
} else if quality <= 720 {
qualityTitle = "High"
} else {
qualityTitle = "Ultra-High"
}
items.append(.action(ContextMenuActionItem(text: qualityTitle, textLayout: .secondLineWithValue("\(quality)p"), icon: { _ in
if isSelected {
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: .white)
} else {
return nil
}
}, action: { [weak self] _, f in
f(.default)
c?.pushItems(items: .single(ContextController.Items(content: .list(strongSelf.contextMenuVideoQualityItems(dismiss: dismiss)))))
})))
guard let self, let videoNode = self.videoNode else {
return
}
videoNode.setVideoQuality(.quality(quality))
//TODO:release
/*if quality >= 700 {
self.settingsBarButton.setContent(.image(generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/NavigationSettingsQHD"), color: .white)))
} else {
self.settingsBarButton.setContent(.image(generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/NavigationSettingsQSD"), color: .white)))
}*/
/*if let controller = strongSelf.galleryController() as? GalleryController {
controller.updateSharedPlaybackRate(rate)
}*/
})))
}
}
} else {
if let (message, _, _) = strongSelf.contentInfo() {
let context = strongSelf.context
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.SharedMedia_ViewInChat, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/GoToMessage"), color: theme.contextMenu.primaryColor)}, action: { [weak self] _, f in
guard let strongSelf = self, let peer = peer else {
return
}
if let navigationController = strongSelf.baseNavigationController() {
strongSelf.beginCustomDismiss(true)
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), subject: .message(id: .id(message.id), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: false)))
Queue.mainQueue().after(0.3) {
strongSelf.completeCustomDismiss(false)
}
}
f(.default)
})))
}
// if #available(iOS 11.0, *) {
// items.append(.action(ContextMenuActionItem(text: "AirPlay", textColor: .primary, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/AirPlay"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in
// f(.default)
// guard let strongSelf = self else {
// return
// }
// strongSelf.beginAirPlaySetup()
// })))
// }
if let (message, _, _) = strongSelf.contentInfo() {
for media in message.media {
if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content {
let url = content.url
let item = OpenInItem.url(url: url)
let openText = strongSelf.presentationData.strings.Conversation_FileOpenIn
items.append(.action(ContextMenuActionItem(text: openText, textColor: .primary, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Share"), color: theme.contextMenu.primaryColor) }, action: { _, f in
f(.default)
if let strongSelf = self, let controller = strongSelf.galleryController() {
var presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
if !presentationData.theme.overallDarkAppearance {
presentationData = presentationData.withUpdated(theme: defaultDarkColorPresentationTheme)
}
let actionSheet = OpenInActionSheetController(context: strongSelf.context, forceTheme: presentationData.theme, item: item, openUrl: { [weak self] url in
if let strongSelf = self {
strongSelf.context.sharedContext.openExternalUrl(context: strongSelf.context, urlContext: .generic, url: url, forceExternal: true, presentationData: presentationData, navigationController: strongSelf.baseNavigationController(), dismissInput: {})
}
})
controller.present(actionSheet, in: .window(.root))
}
})))
break
}
}
}
if let (message, maybeFile, _) = strongSelf.contentInfo(), let file = maybeFile, !message.isCopyProtected() && !item.peerIsCopyProtected && message.paidContent == nil {
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Gallery_SaveVideo, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Download"), color: theme.actionSheet.primaryTextColor) }, action: { c, _ in
items.append(.action(ContextMenuActionItem(text: "Save to Gallery", icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Download"), color: theme.actionSheet.primaryTextColor) }, action: { c, _ in
guard let self else {
c?.dismiss(result: .default, completion: nil)
return
@ -3281,7 +3415,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
continue
}
let fileSizeString = dataSizeString(qualityFileSize, formatting: DataSizeStringFormatting(presentationData: self.presentationData))
items.append(.action(ContextMenuActionItem(text: "\(quality)p (\(fileSizeString))", icon: { _ in
items.append(.action(ContextMenuActionItem(text: "Save in \(quality)p", textLayout: .secondLineWithValue(fileSizeString), icon: { _ in
return nil
}, action: { [weak self] c, _ in
c?.dismiss(result: .default, completion: nil)
@ -3351,6 +3485,66 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
})))
}
if !items.isEmpty {
items.append(.separator)
}
if let (message, _, _) = strongSelf.contentInfo() {
let context = strongSelf.context
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.SharedMedia_ViewInChat, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/GoToMessage"), color: theme.contextMenu.primaryColor)}, action: { [weak self] _, f in
guard let strongSelf = self, let peer = peer else {
return
}
if let navigationController = strongSelf.baseNavigationController() {
strongSelf.beginCustomDismiss(true)
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), subject: .message(id: .id(message.id), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: false)))
Queue.mainQueue().after(0.3) {
strongSelf.completeCustomDismiss(false)
}
}
f(.default)
})))
}
// if #available(iOS 11.0, *) {
// items.append(.action(ContextMenuActionItem(text: "AirPlay", textColor: .primary, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/AirPlay"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in
// f(.default)
// guard let strongSelf = self else {
// return
// }
// strongSelf.beginAirPlaySetup()
// })))
// }
if let (message, _, _) = strongSelf.contentInfo() {
for media in message.media {
if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content {
let url = content.url
let item = OpenInItem.url(url: url)
let openText = strongSelf.presentationData.strings.Conversation_FileOpenIn
items.append(.action(ContextMenuActionItem(text: openText, textColor: .primary, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Share"), color: theme.contextMenu.primaryColor) }, action: { _, f in
f(.default)
if let strongSelf = self, let controller = strongSelf.galleryController() {
var presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
if !presentationData.theme.overallDarkAppearance {
presentationData = presentationData.withUpdated(theme: defaultDarkColorPresentationTheme)
}
let actionSheet = OpenInActionSheetController(context: strongSelf.context, forceTheme: presentationData.theme, item: item, openUrl: { [weak self] url in
if let strongSelf = self {
strongSelf.context.sharedContext.openExternalUrl(context: strongSelf.context, urlContext: .generic, url: url, forceExternal: true, presentationData: presentationData, navigationController: strongSelf.baseNavigationController(), dismissInput: {})
}
})
controller.present(actionSheet, in: .window(.root))
}
})))
break
}
}
}
if let peer, let (message, _, _) = strongSelf.contentInfo(), canSendMessagesToPeer(peer._asPeer()) {
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Conversation_ContextMenuReply, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Reply"), color: theme.contextMenu.primaryColor)}, action: { [weak self] _, f in
if let self, let navigationController = self.baseNavigationController() {
@ -3377,148 +3571,10 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
}
}
return items
return (items, topItems)
}
}
private func contextMenuSpeedItems(dismiss: @escaping () -> Void) -> 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] = []
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)
}, iconPosition: .left, action: { c, _ in
c?.popItems()
})))
let sliderValuePromise = ValuePromise<Double?>(nil)
items.append(.custom(SliderContextItem(minValue: 0.2, maxValue: 2.5, value: status.baseRate, valueChanged: { [weak self] newValue, _ in
guard let strongSelf = self, let videoNode = strongSelf.videoNode else {
return
}
let newValue = normalizeValue(newValue)
videoNode.setBaseRate(newValue)
if let controller = strongSelf.galleryController() as? GalleryController {
controller.updateSharedPlaybackRate(newValue)
}
sliderValuePromise.set(newValue)
}), true))
items.append(.separator)
for (text, _, rate) in strongSelf.speedList(strings: strongSelf.presentationData.strings) {
let isSelected = abs(status.baseRate - rate) < 0.01
items.append(.action(ContextMenuActionItem(text: text, icon: { _ in return nil }, iconSource: ContextMenuActionItemIconSource(size: CGSize(width: 24.0, height: 24.0), signal: sliderValuePromise.get()
|> map { value in
if isSelected && value == nil {
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: .white)
} else {
return nil
}
}), action: { _, f in
f(.default)
guard let strongSelf = self, let videoNode = strongSelf.videoNode else {
return
}
videoNode.setBaseRate(rate)
if let controller = strongSelf.galleryController() as? GalleryController {
controller.updateSharedPlaybackRate(rate)
}
})))
}
return items
}
}
private func contextMenuVideoQualityItems(dismiss: @escaping () -> Void) -> [ContextMenuItem] {
guard let videoNode = self.videoNode else {
return []
}
guard let qualityState = videoNode.videoQualityState(), !qualityState.available.isEmpty else {
return []
}
var items: [ContextMenuItem] = []
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Common_Back, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.actionSheet.primaryTextColor)
}, iconPosition: .left, action: { c, _ in
c?.popItems()
})))
do {
let isSelected = qualityState.preferred == .auto
let qualityText: String
if qualityState.current != 0 {
qualityText = "Auto (\(qualityState.current)p)"
} else {
qualityText = "Auto"
}
items.append(.action(ContextMenuActionItem(text: qualityText, icon: { _ in
if isSelected {
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: .white)
} else {
return nil
}
}, action: { [weak self] _, f in
f(.default)
guard let self, let videoNode = self.videoNode else {
return
}
videoNode.setVideoQuality(.auto)
self.settingsBarButton.setContent(.image(generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/NavigationSettingsQAuto"), color: .white)))
/*if let controller = strongSelf.galleryController() as? GalleryController {
controller.updateSharedPlaybackRate(rate)
}*/
})))
}
for quality in qualityState.available {
let isSelected = qualityState.preferred == .quality(quality)
items.append(.action(ContextMenuActionItem(text: "\(quality)p", icon: { _ in
if isSelected {
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: .white)
} else {
return nil
}
}, action: { [weak self] _, f in
f(.default)
guard let self, let videoNode = self.videoNode else {
return
}
videoNode.setVideoQuality(.quality(quality))
if quality >= 700 {
self.settingsBarButton.setContent(.image(generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/NavigationSettingsQHD"), color: .white)))
} else {
self.settingsBarButton.setContent(.image(generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/NavigationSettingsQSD"), color: .white)))
}
/*if let controller = strongSelf.galleryController() as? GalleryController {
controller.updateSharedPlaybackRate(rate)
}*/
})))
}
return items
}
private var isAirPlayActive = false
private var externalVideoPlayer: ExternalVideoPlayer?
func beginAirPlaySetup() {

View File

@ -709,6 +709,10 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
private var forceStopAnimations: Bool = false
typealias Params = (item: ChatMessageItem, params: ListViewItemLayoutParams, mergedTop: ChatMessageMerge, mergedBottom: ChatMessageMerge, dateHeaderAtBottom: Bool)
private var currentInputParams: Params?
private var currentApplyParams: ListViewItemApply?
required public init(rotated: Bool) {
self.mainContextSourceNode = ContextExtractedContentContainingNode()
self.mainContainerNode = ContextControllerSourceNode()
@ -1363,6 +1367,13 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
}
}
private func internalUpdateLayout() {
if let inputParams = self.currentInputParams, let currentApplyParams = self.currentApplyParams {
let (_, applyLayout) = self.asyncLayout()(inputParams.item, inputParams.params, inputParams.mergedTop, inputParams.mergedBottom, inputParams.dateHeaderAtBottom)
applyLayout(.None, ListViewItemApply(isOnScreen: currentApplyParams.isOnScreen, timestamp: nil), false)
}
}
override public func asyncLayout() -> (_ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: ChatMessageMerge, _ mergedBottom: ChatMessageMerge, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation, ListViewItemApply, Bool) -> Void) {
var currentContentClassesPropertiesAndLayouts: [(Message, AnyClass, Bool, Int?, (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))))] = []
for contentNode in self.contentNodes {
@ -1421,7 +1432,12 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
}
private static func beginLayout(
selfReference: Weak<ChatMessageBubbleItemNode>, _ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: ChatMessageMerge, _ mergedBottom: ChatMessageMerge, _ dateHeaderAtBottom: Bool,
selfReference: Weak<ChatMessageBubbleItemNode>,
_ item: ChatMessageItem,
_ params: ListViewItemLayoutParams,
_ mergedTop: ChatMessageMerge,
_ mergedBottom: ChatMessageMerge,
_ dateHeaderAtBottom: Bool,
currentContentClassesPropertiesAndLayouts: [(Message, AnyClass, Bool, Int?, (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))))],
authorNameLayout: (TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode),
viaMeasureLayout: (TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode),
@ -3095,6 +3111,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
return (layout, { animation, applyInfo, synchronousLoads in
return ChatMessageBubbleItemNode.applyLayout(selfReference: selfReference, animation, synchronousLoads,
inputParams: (item, params, mergedTop, mergedBottom, dateHeaderAtBottom),
params: params,
applyInfo: applyInfo,
layout: layout,
@ -3153,6 +3170,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
private static func applyLayout(selfReference: Weak<ChatMessageBubbleItemNode>,
_ animation: ListViewItemUpdateAnimation,
_ synchronousLoads: Bool,
inputParams: Params,
params: ListViewItemLayoutParams,
applyInfo: ListViewItemApply,
layout: ListViewItemNodeLayout,
@ -3209,6 +3227,9 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
return
}
strongSelf.currentInputParams = inputParams
strongSelf.currentApplyParams = applyInfo
if item.message.id.namespace == Namespaces.Message.Local || item.message.id.namespace == Namespaces.Message.ScheduledLocal || item.message.id.namespace == Namespaces.Message.QuickReplyLocal {
strongSelf.wasPending = true
}
@ -4025,10 +4046,11 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
contextSourceNode?.updateDistractionFreeMode?(value)
}
contentNode.requestInlineUpdate = { [weak strongSelf] in
guard let strongSelf, let item = strongSelf.item else {
guard let strongSelf else {
return
}
item.controllerInteraction.requestMessageUpdate(item.message.id, false)
strongSelf.internalUpdateLayout()
}
contentNode.updateIsExtractedToContextPreview(contextSourceNode.isExtractedToContextPreview)
}

View File

@ -627,7 +627,11 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
}
public func isAvailableForGalleryTransition() -> Bool {
return self.automaticPlayback ?? false
if let automaticPlayback = self.automaticPlayback, automaticPlayback, self.decoration != nil {
return true
} else {
return false
}
}
public func isAvailableForInstantPageTransition() -> Bool {
@ -1094,9 +1098,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
} else {
mediaUpdated = true
}
if hlsInlinePlaybackRange != appliedHlsInlinePlaybackRange {
mediaUpdated = true
}
let inlinePlaybackRangeUpdated = hlsInlinePlaybackRange != appliedHlsInlinePlaybackRange
var isSendingUpdated = false
if let currentMessage = currentMessage {
@ -1154,7 +1156,8 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
}
}
if mediaUpdated || isSendingUpdated || automaticPlaybackUpdated {
let reloadMedia = mediaUpdated || isSendingUpdated || automaticPlaybackUpdated
if mediaUpdated || isSendingUpdated || automaticPlaybackUpdated || inlinePlaybackRangeUpdated {
var media = media
var extendedMedia: TelegramExtendedMedia?
@ -1381,31 +1384,6 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
chatMessageWebFileCancelInteractiveFetch(account: context.account, image: image)
})
} else if var file = media as? TelegramMediaFile {
if isSecretMedia {
updateImageSignal = { synchronousLoad, _ in
return chatSecretMessageVideo(account: context.account, userLocation: .peer(message.id.peerId), videoReference: .message(message: MessageReference(message), media: file))
}
} else {
if file.isAnimatedSticker {
let dimensions = file.dimensions ?? PixelDimensions(width: 512, height: 512)
updateImageSignal = { synchronousLoad, _ in
return chatMessageAnimatedSticker(postbox: context.account.postbox, userLocation: .peer(message.id.peerId), file: file, small: false, size: dimensions.cgSize.aspectFitted(CGSize(width: 400.0, height: 400.0)))
}
} else if file.isSticker || file.isVideoSticker {
updateImageSignal = { synchronousLoad, _ in
return chatMessageSticker(account: context.account, userLocation: .peer(message.id.peerId), file: file, small: false)
}
} else {
onlyFullSizeVideoThumbnail = isSendingUpdated
updateImageSignal = { synchronousLoad, _ in
return mediaGridMessageVideo(postbox: context.account.postbox, userLocation: .peer(message.id.peerId), videoReference: .message(message: MessageReference(message), media: file), onlyFullSize: currentMedia?.id?.namespace == Namespaces.Media.LocalFile, autoFetchFullSizeThumbnail: true)
}
updateBlurredImageSignal = { synchronousLoad, _ in
return chatSecretMessageVideo(account: context.account, userLocation: .peer(message.id.peerId), videoReference: .message(message: MessageReference(message), media: file), synchronousLoad: true)
}
}
}
var uploading = false
if file.resource is VideoLibraryMediaResource {
uploading = true
@ -1463,6 +1441,31 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
}
}
if isSecretMedia {
updateImageSignal = { synchronousLoad, _ in
return chatSecretMessageVideo(account: context.account, userLocation: .peer(message.id.peerId), videoReference: .message(message: MessageReference(message), media: file))
}
} else {
if file.isAnimatedSticker {
let dimensions = file.dimensions ?? PixelDimensions(width: 512, height: 512)
updateImageSignal = { synchronousLoad, _ in
return chatMessageAnimatedSticker(postbox: context.account.postbox, userLocation: .peer(message.id.peerId), file: file, small: false, size: dimensions.cgSize.aspectFitted(CGSize(width: 400.0, height: 400.0)))
}
} else if file.isSticker || file.isVideoSticker {
updateImageSignal = { synchronousLoad, _ in
return chatMessageSticker(account: context.account, userLocation: .peer(message.id.peerId), file: file, small: false)
}
} else {
onlyFullSizeVideoThumbnail = isSendingUpdated
updateImageSignal = { synchronousLoad, _ in
return mediaGridMessageVideo(postbox: context.account.postbox, userLocation: .peer(message.id.peerId), videoReference: .message(message: MessageReference(message), media: file), onlyFullSize: currentMedia?.id?.namespace == Namespaces.Media.LocalFile, autoFetchFullSizeThumbnail: true)
}
updateBlurredImageSignal = { synchronousLoad, _ in
return chatSecretMessageVideo(account: context.account, userLocation: .peer(message.id.peerId), videoReference: .message(message: MessageReference(message), media: file), synchronousLoad: true)
}
}
}
updatedFetchControls = FetchControls(fetch: { manual in
if let strongSelf = self {
if file.isAnimated {
@ -1531,6 +1534,9 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
}
}
}
if !reloadMedia {
updateImageSignal = nil
}
var isExtendedMedia = false
if statusUpdated {
@ -1988,19 +1994,18 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
guard let strongSelf else {
return
}
let hlsInlinePlaybackRange: Range<Int64>?
if let preloadData {
strongSelf.hlsInlinePlaybackRange = preloadData.1
hlsInlinePlaybackRange = preloadData.1
} else {
strongSelf.hlsInlinePlaybackRange = nil
hlsInlinePlaybackRange = nil
}
if strongSelf.hlsInlinePlaybackRange != hlsInlinePlaybackRange {
strongSelf.hlsInlinePlaybackRange = hlsInlinePlaybackRange
strongSelf.requestInlineUpdate?()
}
strongSelf.requestInlineUpdate?()
})
}
} else {
if let hlsInlinePlaybackRangeDisposable = strongSelf.hlsInlinePlaybackRangeDisposable {
strongSelf.hlsInlinePlaybackRangeDisposable = nil
hlsInlinePlaybackRangeDisposable.dispose()
}
}
}
})

View File

@ -0,0 +1,21 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "SectionTitleContextItem",
module_name = "SectionTitleContextItem",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/ContextUI",
"//submodules/TelegramPresentationData",
],
visibility = [
"//visibility:public",
],
)

View File

@ -0,0 +1,89 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import ContextUI
import TelegramPresentationData
public final class SectionTitleContextItem: ContextMenuCustomItem {
let text: String
public init(text: String) {
self.text = text
}
public func node(presentationData: PresentationData, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) -> ContextMenuCustomNode {
return SectionTitleContextItemNode(presentationData: presentationData, item: self, getController: getController, actionSelected: actionSelected)
}
}
private final class SectionTitleContextItemNode: ASDisplayNode, ContextMenuCustomNode {
private let item: SectionTitleContextItem
private let presentationData: PresentationData
private let getController: () -> ContextControllerProtocol?
private let actionSelected: (ContextMenuActionResult) -> Void
private let backgroundNode: ASDisplayNode
private let textNode: ImmediateTextNode
var needsSeparator: Bool {
return false
}
init(presentationData: PresentationData, item: SectionTitleContextItem, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) {
self.item = item
self.presentationData = presentationData
self.getController = getController
self.actionSelected = actionSelected
let textFont = Font.regular(presentationData.listsFontSize.baseDisplaySize * 12.0 / 17.0)
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isAccessibilityElement = false
self.backgroundNode.backgroundColor = presentationData.theme.contextMenu.sectionSeparatorColor
self.textNode = ImmediateTextNode()
self.textNode.isAccessibilityElement = false
self.textNode.isUserInteractionEnabled = false
self.textNode.displaysAsynchronously = false
self.textNode.attributedText = NSAttributedString(string: item.text, font: textFont, textColor: presentationData.theme.contextMenu.secondaryColor)
self.textNode.maximumNumberOfLines = 1
super.init()
self.addSubnode(self.backgroundNode)
self.addSubnode(self.textNode)
}
func updateLayout(constrainedWidth: CGFloat, constrainedHeight: CGFloat) -> (CGSize, (CGSize, ContainedViewLayoutTransition) -> Void) {
let sideInset: CGFloat = 16.0
let textSize = self.textNode.updateLayout(CGSize(width: constrainedWidth - sideInset - sideInset, height: .greatestFiniteMagnitude))
let height: CGFloat = 28.0
return (CGSize(width: textSize.width + sideInset + sideInset, height: height), { size, transition in
let verticalOrigin = floor((size.height - textSize.height) / 2.0)
let textFrame = CGRect(origin: CGPoint(x: sideInset, y: verticalOrigin), size: textSize)
transition.updateFrameAdditive(node: self.textNode, frame: textFrame)
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height)))
})
}
func updateTheme(presentationData: PresentationData) {
self.backgroundNode.backgroundColor = presentationData.theme.contextMenu.sectionSeparatorColor
let textFont = Font.regular(presentationData.listsFontSize.baseDisplaySize * 12.0 / 17.0)
self.textNode.attributedText = NSAttributedString(string: self.textNode.attributedText?.string ?? "", font: textFont, textColor: presentationData.theme.contextMenu.secondaryColor)
}
func canBeHighlighted() -> Bool {
return false
}
func updateIsHighlighted(isHighlighted: Bool) {
}
func performAction() {
}
}

View File

@ -8,12 +8,14 @@ import TelegramPresentationData
import AnimatedCountLabelNode
public final class SliderContextItem: ContextMenuCustomItem {
private let title: String?
private let minValue: CGFloat
private let maxValue: CGFloat
private let value: CGFloat
private let valueChanged: (CGFloat, Bool) -> Void
public init(minValue: CGFloat, maxValue: CGFloat, value: CGFloat, valueChanged: @escaping (CGFloat, Bool) -> Void) {
public init(title: String? = nil, minValue: CGFloat, maxValue: CGFloat, value: CGFloat, valueChanged: @escaping (CGFloat, Bool) -> Void) {
self.title = title
self.minValue = minValue
self.maxValue = maxValue
self.value = value
@ -21,7 +23,7 @@ public final class SliderContextItem: ContextMenuCustomItem {
}
public func node(presentationData: PresentationData, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) -> ContextMenuCustomNode {
return SliderContextItemNode(presentationData: presentationData, getController: getController, minValue: self.minValue, maxValue: self.maxValue, value: self.value, valueChanged: self.valueChanged)
return SliderContextItemNode(presentationData: presentationData, getController: getController, title: self.title, minValue: self.minValue, maxValue: self.maxValue, value: self.value, valueChanged: self.valueChanged)
}
}
@ -31,12 +33,18 @@ private final class SliderContextItemNode: ASDisplayNode, ContextMenuCustomNode,
private var presentationData: PresentationData
private(set) var vibrancyEffectView: UIVisualEffectView?
private let backgroundTitleNode: ImmediateTextNode
private let dimBackgroundTitleNode: ImmediateTextNode
private let foregroundTitleNode: ImmediateTextNode
private let backgroundTextNode: ImmediateAnimatedCountLabelNode
private let dimBackgroundTextNode: ImmediateAnimatedCountLabelNode
private let foregroundNode: ASDisplayNode
private let foregroundTextNode: ImmediateAnimatedCountLabelNode
let title: String?
let minValue: CGFloat
let maxValue: CGFloat
var value: CGFloat = 1.0 {
@ -49,13 +57,18 @@ private final class SliderContextItemNode: ASDisplayNode, ContextMenuCustomNode,
private let hapticFeedback = HapticFeedback()
init(presentationData: PresentationData, getController: @escaping () -> ContextControllerProtocol?, minValue: CGFloat, maxValue: CGFloat, value: CGFloat, valueChanged: @escaping (CGFloat, Bool) -> Void) {
init(presentationData: PresentationData, getController: @escaping () -> ContextControllerProtocol?, title: String?, minValue: CGFloat, maxValue: CGFloat, value: CGFloat, valueChanged: @escaping (CGFloat, Bool) -> Void) {
self.presentationData = presentationData
self.title = title
self.minValue = minValue
self.maxValue = maxValue
self.value = value
self.valueChanged = valueChanged
self.backgroundTitleNode = ImmediateTextNode()
self.dimBackgroundTitleNode = ImmediateTextNode()
self.foregroundTitleNode = ImmediateTextNode()
self.backgroundTextNode = ImmediateAnimatedCountLabelNode()
self.backgroundTextNode.alwaysOneDirection = true
@ -76,7 +89,6 @@ private final class SliderContextItemNode: ASDisplayNode, ContextMenuCustomNode,
self.isUserInteractionEnabled = true
if presentationData.theme.overallDarkAppearance {
} else {
let style: UIBlurEffect.Style
style = .extraLight
@ -87,9 +99,12 @@ private final class SliderContextItemNode: ASDisplayNode, ContextMenuCustomNode,
self.vibrancyEffectView = vibrancyEffectView
}
self.addSubnode(self.backgroundTitleNode)
self.addSubnode(self.dimBackgroundTitleNode)
self.addSubnode(self.backgroundTextNode)
self.addSubnode(self.dimBackgroundTextNode)
self.addSubnode(self.foregroundNode)
self.foregroundNode.addSubnode(self.foregroundTitleNode)
self.foregroundNode.addSubnode(self.foregroundTextNode)
let stringValue = "1.0x"
@ -114,6 +129,11 @@ private final class SliderContextItemNode: ASDisplayNode, ContextMenuCustomNode,
textCount += 1
}
}
self.backgroundTitleNode.attributedText = NSAttributedString(string: self.title ?? "", font: textFont, textColor: backgroundTextColor)
self.dimBackgroundTitleNode.attributedText = NSAttributedString(string: self.title ?? "", font: textFont, textColor: dimBackgroundTextColor)
self.foregroundTitleNode.attributedText = NSAttributedString(string: self.title ?? "", font: textFont, textColor: foregroundTextColor)
self.dimBackgroundTextNode.segments = dimBackgroundSegments
self.backgroundTextNode.segments = backgroundSegments
self.foregroundTextNode.segments = foregroundSegments
@ -179,6 +199,10 @@ private final class SliderContextItemNode: ASDisplayNode, ContextMenuCustomNode,
self.backgroundTextNode.segments = backgroundSegments
self.foregroundTextNode.segments = foregroundSegments
self.backgroundTitleNode.attributedText = NSAttributedString(string: self.title ?? "", font: textFont, textColor: backgroundTextColor)
self.dimBackgroundTitleNode.attributedText = NSAttributedString(string: self.title ?? "", font: textFont, textColor: dimBackgroundTextColor)
self.foregroundTitleNode.attributedText = NSAttributedString(string: self.title ?? "", font: textFont, textColor: foregroundTextColor)
let _ = self.dimBackgroundTextNode.updateLayout(size: CGSize(width: 70.0, height: .greatestFiniteMagnitude), animated: transition.isAnimated)
let _ = self.backgroundTextNode.updateLayout(size: CGSize(width: 70.0, height: .greatestFiniteMagnitude), animated: transition.isAnimated)
let _ = self.foregroundTextNode.updateLayout(size: CGSize(width: 70.0, height: .greatestFiniteMagnitude), animated: transition.isAnimated)
@ -188,20 +212,41 @@ private final class SliderContextItemNode: ASDisplayNode, ContextMenuCustomNode,
let valueWidth: CGFloat = 70.0
let height: CGFloat = 45.0
var backgroundTextSize = self.backgroundTextNode.updateLayout(size: CGSize(width: 70.0, height: .greatestFiniteMagnitude), animated: true)
let originalBackgroundTextSize = self.backgroundTextNode.updateLayout(size: CGSize(width: 70.0, height: .greatestFiniteMagnitude), animated: true)
var backgroundTextSize = originalBackgroundTextSize
backgroundTextSize.width = valueWidth
let backgroundTitleSize = self.backgroundTitleNode.updateLayout(CGSize(width: 120.0, height: 100.0))
let _ = self.dimBackgroundTitleNode.updateLayout(CGSize(width: 120.0, height: 100.0))
let _ = self.foregroundTitleNode.updateLayout(CGSize(width: 120.0, height: 100.0))
return (CGSize(width: height * 3.0, height: height), { size, transition in
let leftInset: CGFloat = 17.0
self.vibrancyEffectView?.frame = CGRect(origin: .zero, size: size)
let textFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - backgroundTextSize.height) / 2.0)), size: backgroundTextSize)
let backgroundTextWidth = self.backgroundTextNode.updateLayout(size: CGSize(width: 70.0, height: .greatestFiniteMagnitude), animated: true).width
self.updateValue(transition: transition)
let titleFrame: CGRect
let textFrame: CGRect
titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - backgroundTitleSize.height) / 2.0)), size: backgroundTitleSize)
if self.title != nil {
textFrame = CGRect(origin: CGPoint(x: size.width - leftInset - backgroundTextWidth, y: floor((height - backgroundTextSize.height) / 2.0)), size: backgroundTextSize)
} else {
textFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - backgroundTextSize.height) / 2.0)), size: backgroundTextSize)
}
transition.updateFrameAdditive(node: self.dimBackgroundTitleNode, frame: titleFrame)
transition.updateFrameAdditive(node: self.backgroundTitleNode, frame: titleFrame)
transition.updateFrameAdditive(node: self.foregroundTitleNode, frame: titleFrame)
transition.updateFrameAdditive(node: self.dimBackgroundTextNode, frame: textFrame)
transition.updateFrameAdditive(node: self.backgroundTextNode, frame: textFrame)
transition.updateFrameAdditive(node: self.foregroundTextNode, frame: textFrame)
self.updateValue(transition: transition)
})
}

View File

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

View File

@ -26,7 +26,7 @@ public final class HLSQualitySet {
for attribute in alternativeFile.attributes {
if case let .Video(_, size, _, _, _, videoCodec) = attribute {
if let videoCodec, NativeVideoContent.isVideoCodecSupported(videoCodec: videoCodec) {
let key = Int(size.height)
let key = Int(min(size.width, size.height))
if let currentFile = qualityFiles[key] {
var currentCodec: String?
for attribute in currentFile.media.attributes {

View File

@ -1127,7 +1127,9 @@ final class HLSVideoJSNativeContentNode: ASDisplayNode, UniversalVideoContentNod
self.playerNode.frame = CGRect(origin: CGPoint(), size: self.intrinsicDimensions)
self.imageNode.setSignal(internalMediaGridMessageVideo(postbox: postbox, userLocation: self.userLocation, videoReference: fileReference, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true) |> map { [weak self] getSize, getData in
let thumbnailVideoReference = HLSVideoContent.minimizedHLSQuality(file: fileReference)?.file ?? fileReference
self.imageNode.setSignal(internalMediaGridMessageVideo(postbox: postbox, userLocation: self.userLocation, videoReference: thumbnailVideoReference, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true) |> map { [weak self] getSize, getData in
Queue.mainQueue().async {
if let strongSelf = self, strongSelf.dimensions == nil {
if let dimensions = getSize() {
@ -1557,7 +1559,7 @@ final class HLSVideoJSNativeContentNode: ASDisplayNode, UniversalVideoContentNod
case .auto:
self.requestedLevelIndex = nil
case let .quality(quality):
if let level = self.playerAvailableLevels.first(where: { $0.value.height == quality }) {
if let level = self.playerAvailableLevels.first(where: { min($0.value.width, $0.value.height) == quality }) {
self.requestedLevelIndex = level.key
} else {
self.requestedLevelIndex = nil
@ -1577,10 +1579,10 @@ final class HLSVideoJSNativeContentNode: ASDisplayNode, UniversalVideoContentNod
return nil
}
var available = self.playerAvailableLevels.values.map(\.height)
var available = self.playerAvailableLevels.values.map { min($0.width, $0.height) }
available.sort(by: { $0 > $1 })
return (currentLevel.height, self.preferredVideoQuality, available)
return (min(currentLevel.width, currentLevel.height), self.preferredVideoQuality, available)
}
func addPlaybackCompleted(_ f: @escaping () -> Void) -> Int {