mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-11-25 17:43:18 +00:00
Add media chapters UI
This commit is contained in:
parent
115c896a96
commit
eabcd258f1
@ -249,7 +249,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
init(context: AccountContext, presentationData: PresentationData) {
|
init(context: AccountContext, presentationData: PresentationData, present: @escaping (ViewController, Any?) -> Void = { _, _ in }) {
|
||||||
self.context = context
|
self.context = context
|
||||||
self.presentationData = presentationData
|
self.presentationData = presentationData
|
||||||
self.theme = presentationData.theme
|
self.theme = presentationData.theme
|
||||||
@ -367,7 +367,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
|
|||||||
let textSelectionNode = TextSelectionNode(theme: TextSelectionTheme(selection: accentColor.withAlphaComponent(0.2), knob: accentColor), strings: presentationData.strings, textNode: self.textNode, updateIsActive: { [weak self] value in
|
let textSelectionNode = TextSelectionNode(theme: TextSelectionTheme(selection: accentColor.withAlphaComponent(0.2), knob: accentColor), strings: presentationData.strings, textNode: self.textNode, updateIsActive: { [weak self] value in
|
||||||
// self?.updateIsTextSelectionActive?(value)
|
// self?.updateIsTextSelectionActive?(value)
|
||||||
}, present: { [weak self] c, a in
|
}, present: { [weak self] c, a in
|
||||||
// self?.item?.controllerInteraction.presentGlobalOverlayController(c, a)
|
present(c, a)
|
||||||
}, rootNode: self, performAction: { [weak self] text, action in
|
}, rootNode: self, performAction: { [weak self] text, action in
|
||||||
// guard let strongSelf = self, let item = strongSelf.item else {
|
// guard let strongSelf = self, let item = strongSelf.item else {
|
||||||
// return
|
// return
|
||||||
|
|||||||
@ -11,21 +11,28 @@ import TelegramPresentationData
|
|||||||
|
|
||||||
private let textFont = Font.regular(13.0)
|
private let textFont = Font.regular(13.0)
|
||||||
|
|
||||||
|
private let scrubberBackgroundColor = UIColor(white: 1.0, alpha: 0.42)
|
||||||
|
private let scrubberForegroundColor = UIColor.white
|
||||||
|
private let scrubberBufferingColor = UIColor(rgb: 0xffffff, alpha: 0.5)
|
||||||
|
|
||||||
final class ChatVideoGalleryItemScrubberView: UIView {
|
final class ChatVideoGalleryItemScrubberView: UIView {
|
||||||
private var containerLayout: (CGSize, CGFloat, CGFloat)?
|
private var containerLayout: (CGSize, CGFloat, CGFloat)?
|
||||||
|
|
||||||
private let leftTimestampNode: MediaPlayerTimeTextNode
|
private let leftTimestampNode: MediaPlayerTimeTextNode
|
||||||
private let rightTimestampNode: MediaPlayerTimeTextNode
|
private let rightTimestampNode: MediaPlayerTimeTextNode
|
||||||
private let fileSizeNode: ASTextNode
|
private let infoNode: ASTextNode
|
||||||
private let scrubberNode: MediaPlayerScrubbingNode
|
private let scrubberNode: MediaPlayerScrubbingNode
|
||||||
|
|
||||||
private var playbackStatus: MediaPlayerStatus?
|
private var playbackStatus: MediaPlayerStatus?
|
||||||
|
private var chapters: [MediaPlayerScrubbingChapter] = []
|
||||||
|
|
||||||
private var fetchStatusDisposable = MetaDisposable()
|
private var fetchStatusDisposable = MetaDisposable()
|
||||||
private var scrubbingDisposable = MetaDisposable()
|
private var scrubbingDisposable = MetaDisposable()
|
||||||
|
private var chapterDisposable = MetaDisposable()
|
||||||
|
|
||||||
private var leftTimestampNodePushed = false
|
private var leftTimestampNodePushed = false
|
||||||
private var rightTimestampNodePushed = false
|
private var rightTimestampNodePushed = false
|
||||||
|
private var infoNodePushed = false
|
||||||
|
|
||||||
var hideWhenDurationIsUnknown = false {
|
var hideWhenDurationIsUnknown = false {
|
||||||
didSet {
|
didSet {
|
||||||
@ -53,17 +60,17 @@ final class ChatVideoGalleryItemScrubberView: UIView {
|
|||||||
var seek: (Double) -> Void = { _ in }
|
var seek: (Double) -> Void = { _ in }
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
override init(frame: CGRect) {
|
||||||
self.scrubberNode = MediaPlayerScrubbingNode(content: .standard(lineHeight: 5.0, lineCap: .round, scrubberHandle: .circle, backgroundColor: UIColor(white: 1.0, alpha: 0.42), foregroundColor: .white, bufferingColor: UIColor(rgb: 0xffffff, alpha: 0.5)))
|
self.scrubberNode = MediaPlayerScrubbingNode(content: .standard(lineHeight: 5.0, lineCap: .round, scrubberHandle: .circle, backgroundColor: scrubberBackgroundColor, foregroundColor: scrubberForegroundColor, bufferingColor: scrubberBufferingColor, chapters: self.chapters))
|
||||||
|
|
||||||
self.leftTimestampNode = MediaPlayerTimeTextNode(textColor: .white)
|
self.leftTimestampNode = MediaPlayerTimeTextNode(textColor: .white)
|
||||||
self.rightTimestampNode = MediaPlayerTimeTextNode(textColor: .white)
|
self.rightTimestampNode = MediaPlayerTimeTextNode(textColor: .white)
|
||||||
self.rightTimestampNode.alignment = .right
|
self.rightTimestampNode.alignment = .right
|
||||||
self.rightTimestampNode.mode = .reversed
|
self.rightTimestampNode.mode = .reversed
|
||||||
|
|
||||||
self.fileSizeNode = ASTextNode()
|
self.infoNode = ASTextNode()
|
||||||
self.fileSizeNode.maximumNumberOfLines = 1
|
self.infoNode.maximumNumberOfLines = 1
|
||||||
self.fileSizeNode.isUserInteractionEnabled = false
|
self.infoNode.isUserInteractionEnabled = false
|
||||||
self.fileSizeNode.displaysAsynchronously = false
|
self.infoNode.displaysAsynchronously = false
|
||||||
|
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
|
|
||||||
@ -101,7 +108,7 @@ final class ChatVideoGalleryItemScrubberView: UIView {
|
|||||||
self.addSubnode(self.scrubberNode)
|
self.addSubnode(self.scrubberNode)
|
||||||
self.addSubnode(self.leftTimestampNode)
|
self.addSubnode(self.leftTimestampNode)
|
||||||
self.addSubnode(self.rightTimestampNode)
|
self.addSubnode(self.rightTimestampNode)
|
||||||
self.addSubnode(self.fileSizeNode)
|
self.addSubnode(self.infoNode)
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder aDecoder: NSCoder) {
|
required init?(coder aDecoder: NSCoder) {
|
||||||
@ -126,6 +133,29 @@ final class ChatVideoGalleryItemScrubberView: UIView {
|
|||||||
self.leftTimestampNode.status = mappedStatus
|
self.leftTimestampNode.status = mappedStatus
|
||||||
self.rightTimestampNode.status = mappedStatus
|
self.rightTimestampNode.status = mappedStatus
|
||||||
|
|
||||||
|
if let mappedStatus = mappedStatus {
|
||||||
|
self.chapterDisposable.set((mappedStatus
|
||||||
|
|> deliverOnMainQueue).start(next: { [weak self] status in
|
||||||
|
if let strongSelf = self, status.duration > 0.0 {
|
||||||
|
var text: String = ""
|
||||||
|
|
||||||
|
for chapter in self.chapters {
|
||||||
|
if chapter.start > status.timestamp {
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
text = chapter.title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
strongSelf.infoNode.attributedText = NSAttributedString(string: text, font: textFont, textColor: .white)
|
||||||
|
|
||||||
|
if let (size, leftInset, rightInset) = strongSelf.containerLayout {
|
||||||
|
strongSelf.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset, transition: .immediate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
self.scrubbingDisposable.set((self.scrubberNode.scrubbingPosition
|
self.scrubbingDisposable.set((self.scrubberNode.scrubbingPosition
|
||||||
|> deliverOnMainQueue).start(next: { [weak self] value in
|
|> deliverOnMainQueue).start(next: { [weak self] value in
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
@ -133,16 +163,20 @@ final class ChatVideoGalleryItemScrubberView: UIView {
|
|||||||
}
|
}
|
||||||
let leftTimestampNodePushed: Bool
|
let leftTimestampNodePushed: Bool
|
||||||
let rightTimestampNodePushed: Bool
|
let rightTimestampNodePushed: Bool
|
||||||
|
let infoNodePushed: Bool
|
||||||
if let value = value {
|
if let value = value {
|
||||||
leftTimestampNodePushed = value < 0.16
|
leftTimestampNodePushed = value < 0.16
|
||||||
rightTimestampNodePushed = value > 0.84
|
rightTimestampNodePushed = value > 0.84
|
||||||
|
infoNodePushed = value >= 0.16 && value <= 0.84
|
||||||
} else {
|
} else {
|
||||||
leftTimestampNodePushed = false
|
leftTimestampNodePushed = false
|
||||||
rightTimestampNodePushed = false
|
rightTimestampNodePushed = false
|
||||||
|
infoNodePushed = false
|
||||||
}
|
}
|
||||||
if leftTimestampNodePushed != strongSelf.leftTimestampNodePushed || rightTimestampNodePushed != strongSelf.rightTimestampNodePushed {
|
if leftTimestampNodePushed != strongSelf.leftTimestampNodePushed || rightTimestampNodePushed != strongSelf.rightTimestampNodePushed || infoNodePushed != strongSelf.infoNodePushed {
|
||||||
strongSelf.leftTimestampNodePushed = leftTimestampNodePushed
|
strongSelf.leftTimestampNodePushed = leftTimestampNodePushed
|
||||||
strongSelf.rightTimestampNodePushed = rightTimestampNodePushed
|
strongSelf.rightTimestampNodePushed = rightTimestampNodePushed
|
||||||
|
strongSelf.infoNodePushed = infoNodePushed
|
||||||
|
|
||||||
if let layout = strongSelf.containerLayout {
|
if let layout = strongSelf.containerLayout {
|
||||||
strongSelf.updateLayout(size: layout.0, leftInset: layout.1, rightInset: layout.2, transition: .animated(duration: 0.35, curve: .spring))
|
strongSelf.updateLayout(size: layout.0, leftInset: layout.1, rightInset: layout.2, transition: .animated(duration: 0.35, curve: .spring))
|
||||||
@ -170,7 +204,7 @@ final class ChatVideoGalleryItemScrubberView: UIView {
|
|||||||
default:
|
default:
|
||||||
text = ""
|
text = ""
|
||||||
}
|
}
|
||||||
strongSelf.fileSizeNode.attributedText = NSAttributedString(string: text, font: textFont, textColor: .white)
|
strongSelf.infoNode.attributedText = NSAttributedString(string: text, font: textFont, textColor: .white)
|
||||||
|
|
||||||
if let (size, leftInset, rightInset) = strongSelf.containerLayout {
|
if let (size, leftInset, rightInset) = strongSelf.containerLayout {
|
||||||
strongSelf.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset, transition: .immediate)
|
strongSelf.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset, transition: .immediate)
|
||||||
@ -178,10 +212,10 @@ final class ChatVideoGalleryItemScrubberView: UIView {
|
|||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
} else {
|
} else {
|
||||||
self.fileSizeNode.attributedText = NSAttributedString(string: dataSizeString(fileSize, forceDecimal: true, decimalSeparator: decimalSeparator), font: textFont, textColor: .white)
|
self.infoNode.attributedText = NSAttributedString(string: dataSizeString(fileSize, forceDecimal: true, decimalSeparator: decimalSeparator), font: textFont, textColor: .white)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
self.fileSizeNode.attributedText = nil
|
self.infoNode.attributedText = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -192,22 +226,29 @@ final class ChatVideoGalleryItemScrubberView: UIView {
|
|||||||
let scrubberInset: CGFloat
|
let scrubberInset: CGFloat
|
||||||
let leftTimestampOffset: CGFloat
|
let leftTimestampOffset: CGFloat
|
||||||
let rightTimestampOffset: CGFloat
|
let rightTimestampOffset: CGFloat
|
||||||
|
let infoOffset: CGFloat
|
||||||
if size.width > size.height {
|
if size.width > size.height {
|
||||||
scrubberInset = 58.0
|
scrubberInset = 58.0
|
||||||
leftTimestampOffset = 4.0
|
leftTimestampOffset = 4.0
|
||||||
rightTimestampOffset = 4.0
|
rightTimestampOffset = 4.0
|
||||||
|
infoOffset = 0.0
|
||||||
} else {
|
} else {
|
||||||
scrubberInset = 13.0
|
scrubberInset = 13.0
|
||||||
leftTimestampOffset = 22.0 + (self.leftTimestampNodePushed ? 8.0 : 0.0)
|
leftTimestampOffset = 22.0 + (self.leftTimestampNodePushed ? 8.0 : 0.0)
|
||||||
rightTimestampOffset = 22.0 + (self.rightTimestampNodePushed ? 8.0 : 0.0)
|
rightTimestampOffset = 22.0 + (self.rightTimestampNodePushed ? 8.0 : 0.0)
|
||||||
|
infoOffset = 22.0 + (self.infoNodePushed ? 8.0 : 0.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
transition.updateFrame(node: self.leftTimestampNode, frame: CGRect(origin: CGPoint(x: 12.0, y: leftTimestampOffset), size: CGSize(width: 60.0, height: 20.0)))
|
transition.updateFrame(node: self.leftTimestampNode, frame: CGRect(origin: CGPoint(x: 12.0, y: leftTimestampOffset), size: CGSize(width: 60.0, height: 20.0)))
|
||||||
transition.updateFrame(node: self.rightTimestampNode, frame: CGRect(origin: CGPoint(x: size.width - leftInset - rightInset - 60.0 - 12.0, y: rightTimestampOffset), size: CGSize(width: 60.0, height: 20.0)))
|
transition.updateFrame(node: self.rightTimestampNode, frame: CGRect(origin: CGPoint(x: size.width - leftInset - rightInset - 60.0 - 12.0, y: rightTimestampOffset), size: CGSize(width: 60.0, height: 20.0)))
|
||||||
|
|
||||||
let fileSize = self.fileSizeNode.measure(size)
|
var infoConstrainedSize = size
|
||||||
self.fileSizeNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - fileSize.width) / 2.0), y: 22.0), size: fileSize)
|
infoConstrainedSize.width = size.width - scrubberInset * 2.0 - 100.0
|
||||||
self.fileSizeNode.alpha = size.width < size.height ? 1.0 : 0.0
|
|
||||||
|
let infoSize = self.infoNode.measure(infoConstrainedSize)
|
||||||
|
self.infoNode.bounds = CGRect(origin: CGPoint(), size: infoSize)
|
||||||
|
transition.updatePosition(node: self.infoNode, position: CGPoint(x: size.width / 2.0, y: infoOffset + infoSize.height / 2.0))
|
||||||
|
self.infoNode.alpha = size.width < size.height ? 1.0 : 0.0
|
||||||
|
|
||||||
self.scrubberNode.frame = CGRect(origin: CGPoint(x: scrubberInset, y: 6.0), size: CGSize(width: size.width - leftInset - rightInset - scrubberInset * 2.0, height: scrubberHeight))
|
self.scrubberNode.frame = CGRect(origin: CGPoint(x: scrubberInset, y: 6.0), size: CGSize(width: size.width - leftInset - rightInset - scrubberInset * 2.0, height: scrubberHeight))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -140,12 +140,12 @@ private func galleryMessageCaptionText(_ message: Message) -> String {
|
|||||||
return message.text
|
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, landscape: Bool = false, timecode: Double? = nil, 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 }) -> GalleryItem? {
|
public func galleryItemForEntry(context: AccountContext, presentationData: PresentationData, entry: MessageHistoryEntry, isCentral: Bool = false, streamVideos: Bool, loopVideos: Bool = false, hideControls: Bool = false, fromPlayingVideo: Bool = false, landscape: Bool = false, timecode: Double? = nil, 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? {
|
||||||
let message = entry.message
|
let message = entry.message
|
||||||
let location = entry.location
|
let location = entry.location
|
||||||
if let (media, mediaImage) = mediaForMessage(message: message) {
|
if let (media, mediaImage) = mediaForMessage(message: message) {
|
||||||
if let _ = media as? TelegramMediaImage {
|
if let _ = media as? TelegramMediaImage {
|
||||||
return ChatImageGalleryItem(context: context, presentationData: presentationData, message: message, location: location, performAction: performAction, openActionOptions: openActionOptions)
|
return ChatImageGalleryItem(context: context, presentationData: presentationData, message: message, location: location, performAction: performAction, openActionOptions: openActionOptions, present: present)
|
||||||
} else if let file = media as? TelegramMediaFile {
|
} else if let file = media as? TelegramMediaFile {
|
||||||
if file.isVideo {
|
if file.isVideo {
|
||||||
let content: UniversalVideoContent
|
let content: UniversalVideoContent
|
||||||
@ -173,7 +173,7 @@ public func galleryItemForEntry(context: AccountContext, presentationData: Prese
|
|||||||
}
|
}
|
||||||
|
|
||||||
let caption = galleryCaptionStringWithAppliedEntities(text, entities: entities)
|
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, hideControls: hideControls, fromPlayingVideo: fromPlayingVideo, landscape: landscape, timecode: timecode, configuration: configuration, playbackCompleted: playbackCompleted, performAction: performAction, openActionOptions: openActionOptions, storeMediaPlaybackState: storeMediaPlaybackState)
|
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, hideControls: hideControls, fromPlayingVideo: fromPlayingVideo, landscape: landscape, timecode: timecode, configuration: configuration, playbackCompleted: playbackCompleted, performAction: performAction, openActionOptions: openActionOptions, storeMediaPlaybackState: storeMediaPlaybackState, present: present)
|
||||||
} else {
|
} else {
|
||||||
if let fileName = file.fileName, (fileName as NSString).pathExtension.lowercased() == "json" {
|
if let fileName = file.fileName, (fileName as NSString).pathExtension.lowercased() == "json" {
|
||||||
return ChatAnimationGalleryItem(context: context, presentationData: presentationData, message: message, location: location)
|
return ChatAnimationGalleryItem(context: context, presentationData: presentationData, message: message, location: location)
|
||||||
@ -184,7 +184,7 @@ public func galleryItemForEntry(context: AccountContext, presentationData: Prese
|
|||||||
pixelsCount = Int(dimensions.width) * Int(dimensions.height)
|
pixelsCount = Int(dimensions.width) * Int(dimensions.height)
|
||||||
}
|
}
|
||||||
if (file.size == nil || file.size! < 4 * 1024 * 1024) && pixelsCount < 4096 * 4096 {
|
if (file.size == nil || file.size! < 4 * 1024 * 1024) && pixelsCount < 4096 * 4096 {
|
||||||
return ChatImageGalleryItem(context: context, presentationData: presentationData, message: message, location: location, performAction: performAction, openActionOptions: openActionOptions)
|
return ChatImageGalleryItem(context: context, presentationData: presentationData, message: message, location: location, performAction: performAction, openActionOptions: openActionOptions, present: present)
|
||||||
} else {
|
} else {
|
||||||
return ChatDocumentGalleryItem(context: context, presentationData: presentationData, message: message, location: location)
|
return ChatDocumentGalleryItem(context: context, presentationData: presentationData, message: message, location: location)
|
||||||
}
|
}
|
||||||
@ -212,7 +212,7 @@ public func galleryItemForEntry(context: AccountContext, presentationData: Prese
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let content = content {
|
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: ""), fromPlayingVideo: fromPlayingVideo, landscape: landscape, timecode: timecode, configuration: configuration, performAction: performAction, openActionOptions: openActionOptions, storeMediaPlaybackState: storeMediaPlaybackState)
|
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: ""), fromPlayingVideo: fromPlayingVideo, landscape: landscape, timecode: timecode, configuration: configuration, performAction: performAction, openActionOptions: openActionOptions, storeMediaPlaybackState: storeMediaPlaybackState, present: present)
|
||||||
} else {
|
} else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -501,7 +501,11 @@ public class GalleryController: ViewController, StandalonePresentableController
|
|||||||
if entry.message.stableId == strongSelf.centralEntryStableId {
|
if entry.message.stableId == strongSelf.centralEntryStableId {
|
||||||
isCentral = true
|
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, configuration: configuration, performAction: strongSelf.performAction, openActionOptions: strongSelf.openActionOptions, storeMediaPlaybackState: strongSelf.actionInteraction?.storeMediaPlaybackState ?? { _, _ 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, 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)
|
||||||
|
}
|
||||||
|
}) {
|
||||||
if isCentral {
|
if isCentral {
|
||||||
centralItemIndex = items.count
|
centralItemIndex = items.count
|
||||||
}
|
}
|
||||||
@ -925,7 +929,11 @@ public class GalleryController: ViewController, StandalonePresentableController
|
|||||||
if entry.message.stableId == self.centralEntryStableId {
|
if entry.message.stableId == self.centralEntryStableId {
|
||||||
isCentral = true
|
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, configuration: self.configuration, performAction: self.performAction, openActionOptions: self.openActionOptions, storeMediaPlaybackState: self.actionInteraction?.storeMediaPlaybackState ?? { _, _ 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, 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)
|
||||||
|
}
|
||||||
|
}) {
|
||||||
if isCentral {
|
if isCentral {
|
||||||
centralItemIndex = items.count
|
centralItemIndex = items.count
|
||||||
}
|
}
|
||||||
@ -1001,7 +1009,11 @@ public class GalleryController: ViewController, StandalonePresentableController
|
|||||||
if entry.message.stableId == strongSelf.centralEntryStableId {
|
if entry.message.stableId == strongSelf.centralEntryStableId {
|
||||||
isCentral = true
|
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, configuration: strongSelf.configuration, performAction: strongSelf.performAction, openActionOptions: strongSelf.openActionOptions, storeMediaPlaybackState: strongSelf.actionInteraction?.storeMediaPlaybackState ?? { _, _ 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, 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)
|
||||||
|
}
|
||||||
|
}) {
|
||||||
if isCentral {
|
if isCentral {
|
||||||
centralItemIndex = items.count
|
centralItemIndex = items.count
|
||||||
}
|
}
|
||||||
|
|||||||
@ -89,18 +89,20 @@ class ChatImageGalleryItem: GalleryItem {
|
|||||||
let location: MessageHistoryEntryLocation?
|
let location: MessageHistoryEntryLocation?
|
||||||
let performAction: (GalleryControllerInteractionTapAction) -> Void
|
let performAction: (GalleryControllerInteractionTapAction) -> Void
|
||||||
let openActionOptions: (GalleryControllerInteractionTapAction) -> Void
|
let openActionOptions: (GalleryControllerInteractionTapAction) -> Void
|
||||||
|
let present: (ViewController, Any?) -> Void
|
||||||
|
|
||||||
init(context: AccountContext, presentationData: PresentationData, message: Message, location: MessageHistoryEntryLocation?, performAction: @escaping (GalleryControllerInteractionTapAction) -> Void, openActionOptions: @escaping (GalleryControllerInteractionTapAction) -> Void) {
|
init(context: AccountContext, presentationData: PresentationData, message: Message, location: MessageHistoryEntryLocation?, performAction: @escaping (GalleryControllerInteractionTapAction) -> Void, openActionOptions: @escaping (GalleryControllerInteractionTapAction) -> Void, present: @escaping (ViewController, Any?) -> Void) {
|
||||||
self.context = context
|
self.context = context
|
||||||
self.presentationData = presentationData
|
self.presentationData = presentationData
|
||||||
self.message = message
|
self.message = message
|
||||||
self.location = location
|
self.location = location
|
||||||
self.performAction = performAction
|
self.performAction = performAction
|
||||||
self.openActionOptions = openActionOptions
|
self.openActionOptions = openActionOptions
|
||||||
|
self.present = present
|
||||||
}
|
}
|
||||||
|
|
||||||
func node() -> GalleryItemNode {
|
func node() -> GalleryItemNode {
|
||||||
let node = ChatImageGalleryItemNode(context: self.context, presentationData: self.presentationData, performAction: self.performAction, openActionOptions: self.openActionOptions)
|
let node = ChatImageGalleryItemNode(context: self.context, presentationData: self.presentationData, performAction: self.performAction, openActionOptions: self.openActionOptions, present: self.present)
|
||||||
|
|
||||||
for media in self.message.media {
|
for media in self.message.media {
|
||||||
if let image = media as? TelegramMediaImage {
|
if let image = media as? TelegramMediaImage {
|
||||||
@ -177,11 +179,11 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||||||
private let dataDisposable = MetaDisposable()
|
private let dataDisposable = MetaDisposable()
|
||||||
private var status: MediaResourceStatus?
|
private var status: MediaResourceStatus?
|
||||||
|
|
||||||
init(context: AccountContext, presentationData: PresentationData, performAction: @escaping (GalleryControllerInteractionTapAction) -> Void, openActionOptions: @escaping (GalleryControllerInteractionTapAction) -> Void) {
|
init(context: AccountContext, presentationData: PresentationData, performAction: @escaping (GalleryControllerInteractionTapAction) -> Void, openActionOptions: @escaping (GalleryControllerInteractionTapAction) -> Void, present: @escaping (ViewController, Any?) -> Void) {
|
||||||
self.context = context
|
self.context = context
|
||||||
|
|
||||||
self.imageNode = TransformImageNode()
|
self.imageNode = TransformImageNode()
|
||||||
self.footerContentNode = ChatItemGalleryFooterContentNode(context: context, presentationData: presentationData)
|
self.footerContentNode = ChatItemGalleryFooterContentNode(context: context, presentationData: presentationData, present: present)
|
||||||
self.footerContentNode.performAction = performAction
|
self.footerContentNode.performAction = performAction
|
||||||
self.footerContentNode.openActionOptions = openActionOptions
|
self.footerContentNode.openActionOptions = openActionOptions
|
||||||
|
|
||||||
|
|||||||
@ -40,8 +40,9 @@ public class UniversalVideoGalleryItem: GalleryItem {
|
|||||||
let performAction: (GalleryControllerInteractionTapAction) -> Void
|
let performAction: (GalleryControllerInteractionTapAction) -> Void
|
||||||
let openActionOptions: (GalleryControllerInteractionTapAction) -> Void
|
let openActionOptions: (GalleryControllerInteractionTapAction) -> Void
|
||||||
let storeMediaPlaybackState: (MessageId, Double?) -> Void
|
let storeMediaPlaybackState: (MessageId, 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, hideControls: Bool = false, fromPlayingVideo: 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) {
|
public init(context: AccountContext, presentationData: PresentationData, content: UniversalVideoContent, originData: GalleryItemOriginData?, indexData: GalleryItemIndexData?, contentInfo: UniversalVideoGalleryItemContentInfo?, caption: NSAttributedString, credit: NSAttributedString? = nil, hideControls: Bool = false, fromPlayingVideo: 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) {
|
||||||
self.context = context
|
self.context = context
|
||||||
self.presentationData = presentationData
|
self.presentationData = presentationData
|
||||||
self.content = content
|
self.content = content
|
||||||
@ -59,10 +60,11 @@ public class UniversalVideoGalleryItem: GalleryItem {
|
|||||||
self.performAction = performAction
|
self.performAction = performAction
|
||||||
self.openActionOptions = openActionOptions
|
self.openActionOptions = openActionOptions
|
||||||
self.storeMediaPlaybackState = storeMediaPlaybackState
|
self.storeMediaPlaybackState = storeMediaPlaybackState
|
||||||
|
self.present = present
|
||||||
}
|
}
|
||||||
|
|
||||||
public func node() -> GalleryItemNode {
|
public func node() -> GalleryItemNode {
|
||||||
let node = UniversalVideoGalleryItemNode(context: self.context, presentationData: self.presentationData, performAction: self.performAction, openActionOptions: self.openActionOptions)
|
let node = UniversalVideoGalleryItemNode(context: self.context, presentationData: self.presentationData, performAction: self.performAction, openActionOptions: self.openActionOptions, present: self.present)
|
||||||
|
|
||||||
if let indexData = self.indexData {
|
if let indexData = self.indexData {
|
||||||
node._title.set(.single(self.presentationData.strings.Items_NOfM("\(indexData.position + 1)", "\(indexData.totalCount)").0))
|
node._title.set(.single(self.presentationData.strings.Items_NOfM("\(indexData.position + 1)", "\(indexData.totalCount)").0))
|
||||||
@ -284,12 +286,12 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||||||
|
|
||||||
var playbackCompleted: (() -> Void)?
|
var playbackCompleted: (() -> Void)?
|
||||||
|
|
||||||
init(context: AccountContext, presentationData: PresentationData, performAction: @escaping (GalleryControllerInteractionTapAction) -> Void, openActionOptions: @escaping (GalleryControllerInteractionTapAction) -> Void) {
|
init(context: AccountContext, presentationData: PresentationData, performAction: @escaping (GalleryControllerInteractionTapAction) -> Void, openActionOptions: @escaping (GalleryControllerInteractionTapAction) -> Void, present: @escaping (ViewController, Any?) -> Void) {
|
||||||
self.context = context
|
self.context = context
|
||||||
self.presentationData = presentationData
|
self.presentationData = presentationData
|
||||||
self.scrubberView = ChatVideoGalleryItemScrubberView()
|
self.scrubberView = ChatVideoGalleryItemScrubberView()
|
||||||
|
|
||||||
self.footerContentNode = ChatItemGalleryFooterContentNode(context: context, presentationData: presentationData)
|
self.footerContentNode = ChatItemGalleryFooterContentNode(context: context, presentationData: presentationData, present: present)
|
||||||
self.footerContentNode.scrubberView = self.scrubberView
|
self.footerContentNode.scrubberView = self.scrubberView
|
||||||
self.footerContentNode.performAction = performAction
|
self.footerContentNode.performAction = performAction
|
||||||
self.footerContentNode.openActionOptions = openActionOptions
|
self.footerContentNode.openActionOptions = openActionOptions
|
||||||
|
|||||||
@ -433,7 +433,7 @@ public final class SecretMediaPreviewController: ViewController {
|
|||||||
|
|
||||||
guard let item = galleryItemForEntry(context: self.context, presentationData: self.presentationData, entry: MessageHistoryEntry(message: message, isRead: false, location: nil, monthLocation: nil, attributes: MutableMessageHistoryEntryAttributes(authorIsContact: false)), streamVideos: false, hideControls: true, tempFilePath: tempFilePath, playbackCompleted: { [weak self] in
|
guard let item = galleryItemForEntry(context: self.context, presentationData: self.presentationData, entry: MessageHistoryEntry(message: message, isRead: false, location: nil, monthLocation: nil, attributes: MutableMessageHistoryEntryAttributes(authorIsContact: false)), streamVideos: false, hideControls: true, tempFilePath: tempFilePath, playbackCompleted: { [weak self] in
|
||||||
self?.dismiss(forceAway: false)
|
self?.dismiss(forceAway: false)
|
||||||
}) else {
|
}, present: { _, _ in }) else {
|
||||||
self._ready.set(.single(true))
|
self._ready.set(.single(true))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@ -99,7 +99,7 @@ final class InstantPageAudioNode: ASDisplayNode, InstantPageNode {
|
|||||||
if brightness > 0.5 {
|
if brightness > 0.5 {
|
||||||
backgroundAlpha = 0.4
|
backgroundAlpha = 0.4
|
||||||
}
|
}
|
||||||
self.scrubbingNode = MediaPlayerScrubbingNode(content: .standard(lineHeight: 3.0, lineCap: .round, scrubberHandle: .line, backgroundColor: theme.textCategories.paragraph.color.withAlphaComponent(backgroundAlpha), foregroundColor: theme.textCategories.paragraph.color, bufferingColor: theme.textCategories.paragraph.color.withAlphaComponent(0.5)))
|
self.scrubbingNode = MediaPlayerScrubbingNode(content: .standard(lineHeight: 3.0, lineCap: .round, scrubberHandle: .line, backgroundColor: theme.textCategories.paragraph.color.withAlphaComponent(backgroundAlpha), foregroundColor: theme.textCategories.paragraph.color, bufferingColor: theme.textCategories.paragraph.color.withAlphaComponent(0.5), chapters: []))
|
||||||
|
|
||||||
let playlistType: MediaManagerPlayerType
|
let playlistType: MediaManagerPlayerType
|
||||||
if let file = self.media.media as? TelegramMediaFile {
|
if let file = self.media.media as? TelegramMediaFile {
|
||||||
|
|||||||
@ -113,7 +113,7 @@ public struct InstantPageGalleryEntry: Equatable {
|
|||||||
nativeId = .instantPage(self.pageId, file.fileId)
|
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), caption: caption, credit: credit, fromPlayingVideo: fromPlayingVideo, landscape: landscape, performAction: { _ in }, openActionOptions: { _ in }, storeMediaPlaybackState: { _, _ 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), caption: caption, credit: credit, fromPlayingVideo: fromPlayingVideo, landscape: landscape, performAction: { _ in }, openActionOptions: { _ in }, storeMediaPlaybackState: { _, _ in }, present: { _, _ in })
|
||||||
} else {
|
} else {
|
||||||
var representations: [TelegramMediaImageRepresentation] = []
|
var representations: [TelegramMediaImageRepresentation] = []
|
||||||
representations.append(contentsOf: file.previewRepresentations)
|
representations.append(contentsOf: file.previewRepresentations)
|
||||||
@ -125,7 +125,7 @@ public struct InstantPageGalleryEntry: Equatable {
|
|||||||
}
|
}
|
||||||
} else if let embedWebpage = self.media.media as? TelegramMediaWebpage, case let .Loaded(webpageContent) = embedWebpage.content {
|
} else if let embedWebpage = self.media.media as? TelegramMediaWebpage, case let .Loaded(webpageContent) = embedWebpage.content {
|
||||||
if let content = WebEmbedVideoContent(webPage: embedWebpage, webpageContent: webpageContent) {
|
if let content = WebEmbedVideoContent(webPage: embedWebpage, webpageContent: webpageContent) {
|
||||||
return UniversalVideoGalleryItem(context: context, presentationData: presentationData, content: content, originData: nil, indexData: nil, contentInfo: .webPage(webPage, embedWebpage), caption: NSAttributedString(string: ""), fromPlayingVideo: fromPlayingVideo, landscape: landscape, performAction: { _ in }, openActionOptions: { _ in }, storeMediaPlaybackState: { _, _ in })
|
return UniversalVideoGalleryItem(context: context, presentationData: presentationData, content: content, originData: nil, indexData: nil, contentInfo: .webPage(webPage, embedWebpage), caption: NSAttributedString(string: ""), fromPlayingVideo: fromPlayingVideo, landscape: landscape, performAction: { _ in }, openActionOptions: { _ in }, storeMediaPlaybackState: { _, _ in }, present: { _, _ in })
|
||||||
} else {
|
} else {
|
||||||
preconditionFailure()
|
preconditionFailure()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,6 +18,16 @@ private func generateHandleBackground(color: UIColor) -> UIImage? {
|
|||||||
})?.stretchableImage(withLeftCapWidth: 0, topCapHeight: 2)
|
})?.stretchableImage(withLeftCapWidth: 0, topCapHeight: 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public struct MediaPlayerScrubbingChapter {
|
||||||
|
public let title: String
|
||||||
|
public let start: Double
|
||||||
|
|
||||||
|
public init(title: String, start: Double) {
|
||||||
|
self.title = title
|
||||||
|
self.start = start
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private final class MediaPlayerScrubbingNodeButton: ASDisplayNode, UIGestureRecognizerDelegate {
|
private final class MediaPlayerScrubbingNodeButton: ASDisplayNode, UIGestureRecognizerDelegate {
|
||||||
var beginScrubbing: (() -> Void)?
|
var beginScrubbing: (() -> Void)?
|
||||||
var endScrubbing: ((Bool) -> Void)?
|
var endScrubbing: ((Bool) -> Void)?
|
||||||
@ -144,8 +154,8 @@ public enum MediaPlayerScrubbingNodeHandle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public enum MediaPlayerScrubbingNodeContent {
|
public enum MediaPlayerScrubbingNodeContent {
|
||||||
case standard(lineHeight: CGFloat, lineCap: MediaPlayerScrubbingNodeCap, scrubberHandle: MediaPlayerScrubbingNodeHandle, backgroundColor: UIColor, foregroundColor: UIColor, bufferingColor: UIColor)
|
case standard(lineHeight: CGFloat, lineCap: MediaPlayerScrubbingNodeCap, scrubberHandle: MediaPlayerScrubbingNodeHandle, backgroundColor: UIColor, foregroundColor: UIColor, bufferingColor: UIColor, chapters: [MediaPlayerScrubbingChapter])
|
||||||
case custom(backgroundNode: ASDisplayNode, foregroundContentNode: ASDisplayNode)
|
case custom(backgroundNode: CustomMediaPlayerScrubbingForegroundNode, foregroundContentNode: CustomMediaPlayerScrubbingForegroundNode)
|
||||||
}
|
}
|
||||||
|
|
||||||
private final class StandardMediaPlayerScrubbingNodeContentNode {
|
private final class StandardMediaPlayerScrubbingNodeContentNode {
|
||||||
@ -155,18 +165,22 @@ private final class StandardMediaPlayerScrubbingNodeContentNode {
|
|||||||
let bufferingNode: MediaPlayerScrubbingBufferingNode
|
let bufferingNode: MediaPlayerScrubbingBufferingNode
|
||||||
let foregroundContentNode: ASImageNode
|
let foregroundContentNode: ASImageNode
|
||||||
let foregroundNode: MediaPlayerScrubbingForegroundNode
|
let foregroundNode: MediaPlayerScrubbingForegroundNode
|
||||||
|
let chapterNodesContainer: ASDisplayNode?
|
||||||
|
let chapterNodes: [(MediaPlayerScrubbingChapter, ASDisplayNode)]
|
||||||
let handle: MediaPlayerScrubbingNodeHandle
|
let handle: MediaPlayerScrubbingNodeHandle
|
||||||
let handleNode: ASDisplayNode?
|
let handleNode: ASDisplayNode?
|
||||||
let highlightedHandleNode: ASDisplayNode?
|
let highlightedHandleNode: ASDisplayNode?
|
||||||
let handleNodeContainer: MediaPlayerScrubbingNodeButton?
|
let handleNodeContainer: MediaPlayerScrubbingNodeButton?
|
||||||
|
|
||||||
init(lineHeight: CGFloat, lineCap: MediaPlayerScrubbingNodeCap, backgroundNode: ASImageNode, bufferingNode: MediaPlayerScrubbingBufferingNode, foregroundContentNode: ASImageNode, foregroundNode: MediaPlayerScrubbingForegroundNode, handle: MediaPlayerScrubbingNodeHandle, handleNode: ASDisplayNode?, highlightedHandleNode: ASDisplayNode?, handleNodeContainer: MediaPlayerScrubbingNodeButton?) {
|
init(lineHeight: CGFloat, lineCap: MediaPlayerScrubbingNodeCap, backgroundNode: ASImageNode, bufferingNode: MediaPlayerScrubbingBufferingNode, foregroundContentNode: ASImageNode, foregroundNode: MediaPlayerScrubbingForegroundNode, chapterNodesContainer: ASDisplayNode?, chapterNodes: [(MediaPlayerScrubbingChapter, ASDisplayNode)], handle: MediaPlayerScrubbingNodeHandle, handleNode: ASDisplayNode?, highlightedHandleNode: ASDisplayNode?, handleNodeContainer: MediaPlayerScrubbingNodeButton?) {
|
||||||
self.lineHeight = lineHeight
|
self.lineHeight = lineHeight
|
||||||
self.lineCap = lineCap
|
self.lineCap = lineCap
|
||||||
self.backgroundNode = backgroundNode
|
self.backgroundNode = backgroundNode
|
||||||
self.bufferingNode = bufferingNode
|
self.bufferingNode = bufferingNode
|
||||||
self.foregroundContentNode = foregroundContentNode
|
self.foregroundContentNode = foregroundContentNode
|
||||||
self.foregroundNode = foregroundNode
|
self.foregroundNode = foregroundNode
|
||||||
|
self.chapterNodesContainer = chapterNodesContainer
|
||||||
|
self.chapterNodes = chapterNodes
|
||||||
self.handle = handle
|
self.handle = handle
|
||||||
self.handleNode = handleNode
|
self.handleNode = handleNode
|
||||||
self.highlightedHandleNode = highlightedHandleNode
|
self.highlightedHandleNode = highlightedHandleNode
|
||||||
@ -174,13 +188,17 @@ private final class StandardMediaPlayerScrubbingNodeContentNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public protocol CustomMediaPlayerScrubbingForegroundNode: ASDisplayNode {
|
||||||
|
var progress: CGFloat? { get set }
|
||||||
|
}
|
||||||
|
|
||||||
private final class CustomMediaPlayerScrubbingNodeContentNode {
|
private final class CustomMediaPlayerScrubbingNodeContentNode {
|
||||||
let backgroundNode: ASDisplayNode
|
let backgroundNode: CustomMediaPlayerScrubbingForegroundNode
|
||||||
let foregroundContentNode: ASDisplayNode
|
let foregroundContentNode: CustomMediaPlayerScrubbingForegroundNode
|
||||||
let foregroundNode: MediaPlayerScrubbingForegroundNode
|
let foregroundNode: MediaPlayerScrubbingForegroundNode
|
||||||
let handleNodeContainer: MediaPlayerScrubbingNodeButton?
|
let handleNodeContainer: MediaPlayerScrubbingNodeButton?
|
||||||
|
|
||||||
init(backgroundNode: ASDisplayNode, foregroundContentNode: ASDisplayNode, foregroundNode: MediaPlayerScrubbingForegroundNode, handleNodeContainer: MediaPlayerScrubbingNodeButton?) {
|
init(backgroundNode: CustomMediaPlayerScrubbingForegroundNode, foregroundContentNode: CustomMediaPlayerScrubbingForegroundNode, foregroundNode: MediaPlayerScrubbingForegroundNode, handleNodeContainer: MediaPlayerScrubbingNodeButton?) {
|
||||||
self.backgroundNode = backgroundNode
|
self.backgroundNode = backgroundNode
|
||||||
self.foregroundContentNode = foregroundContentNode
|
self.foregroundContentNode = foregroundContentNode
|
||||||
self.foregroundNode = foregroundNode
|
self.foregroundNode = foregroundNode
|
||||||
@ -344,7 +362,7 @@ public final class MediaPlayerScrubbingNode: ASDisplayNode {
|
|||||||
|
|
||||||
private static func contentNodesFromContent(_ content: MediaPlayerScrubbingNodeContent, enableScrubbing: Bool) -> MediaPlayerScrubbingNodeContentNodes {
|
private static func contentNodesFromContent(_ content: MediaPlayerScrubbingNodeContent, enableScrubbing: Bool) -> MediaPlayerScrubbingNodeContentNodes {
|
||||||
switch content {
|
switch content {
|
||||||
case let .standard(lineHeight, lineCap, scrubberHandle, backgroundColor, foregroundColor, bufferingColor):
|
case let .standard(lineHeight, lineCap, scrubberHandle, backgroundColor, foregroundColor, bufferingColor, chapters):
|
||||||
let backgroundNode = ASImageNode()
|
let backgroundNode = ASImageNode()
|
||||||
backgroundNode.isLayerBacked = true
|
backgroundNode.isLayerBacked = true
|
||||||
backgroundNode.displaysAsynchronously = false
|
backgroundNode.displaysAsynchronously = false
|
||||||
@ -408,7 +426,27 @@ public final class MediaPlayerScrubbingNode: ASDisplayNode {
|
|||||||
|
|
||||||
handleNodeContainerImpl?.isUserInteractionEnabled = enableScrubbing
|
handleNodeContainerImpl?.isUserInteractionEnabled = enableScrubbing
|
||||||
|
|
||||||
return .standard(StandardMediaPlayerScrubbingNodeContentNode(lineHeight: lineHeight, lineCap: lineCap, backgroundNode: backgroundNode, bufferingNode: bufferingNode, foregroundContentNode: foregroundContentNode, foregroundNode: foregroundNode, handle: scrubberHandle, handleNode: handleNodeImpl, highlightedHandleNode: highlightedHandleNodeImpl, handleNodeContainer: handleNodeContainerImpl))
|
var chapterNodesContainerImpl: ASDisplayNode?
|
||||||
|
var chapterNodes: [(MediaPlayerScrubbingChapter, ASDisplayNode)] = []
|
||||||
|
|
||||||
|
if !chapters.isEmpty {
|
||||||
|
let chapterNodesContainer = ASDisplayNode()
|
||||||
|
chapterNodesContainer.isUserInteractionEnabled = false
|
||||||
|
chapterNodesContainerImpl = chapterNodesContainer
|
||||||
|
|
||||||
|
for i in 0 ..< chapters.count {
|
||||||
|
let chapterNode = ASDisplayNode()
|
||||||
|
chapterNode.backgroundColor = .black
|
||||||
|
|
||||||
|
if i > 0 {
|
||||||
|
chapterNodesContainer.addSubnode(chapterNode)
|
||||||
|
}
|
||||||
|
chapterNodes.append((chapters[i], chapterNode))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return .standard(StandardMediaPlayerScrubbingNodeContentNode(lineHeight: lineHeight, lineCap: lineCap, backgroundNode: backgroundNode, bufferingNode: bufferingNode, foregroundContentNode: foregroundContentNode, foregroundNode: foregroundNode, chapterNodesContainer: chapterNodesContainerImpl, chapterNodes: chapterNodes, handle: scrubberHandle, handleNode: handleNodeImpl, highlightedHandleNode: highlightedHandleNodeImpl, handleNodeContainer: handleNodeContainerImpl))
|
||||||
case let .custom(backgroundNode, foregroundContentNode):
|
case let .custom(backgroundNode, foregroundContentNode):
|
||||||
let foregroundNode = MediaPlayerScrubbingForegroundNode()
|
let foregroundNode = MediaPlayerScrubbingForegroundNode()
|
||||||
foregroundNode.isLayerBacked = true
|
foregroundNode.isLayerBacked = true
|
||||||
@ -464,6 +502,10 @@ public final class MediaPlayerScrubbingNode: ASDisplayNode {
|
|||||||
node.foregroundNode.addSubnode(node.foregroundContentNode)
|
node.foregroundNode.addSubnode(node.foregroundContentNode)
|
||||||
self.addSubnode(node.foregroundNode)
|
self.addSubnode(node.foregroundNode)
|
||||||
|
|
||||||
|
if let chapterNodesContainer = node.chapterNodesContainer {
|
||||||
|
self.addSubnode(chapterNodesContainer)
|
||||||
|
}
|
||||||
|
|
||||||
if let handleNodeContainer = node.handleNodeContainer {
|
if let handleNodeContainer = node.handleNodeContainer {
|
||||||
self.addSubnode(handleNodeContainer)
|
self.addSubnode(handleNodeContainer)
|
||||||
handleNodeContainer.highlighted = { [weak self] highlighted in
|
handleNodeContainer.highlighted = { [weak self] highlighted in
|
||||||
@ -581,7 +623,7 @@ public final class MediaPlayerScrubbingNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
handleNodeContainer.updateMultiplier = { [weak self] multiplier in
|
handleNodeContainer.updateMultiplier = { [weak self] multiplier in
|
||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
if let statusValue = strongSelf.statusValue, let scrubbingBeginTimestamp = strongSelf.scrubbingBeginTimestamp, Double(0.0).isLess(than: statusValue.duration) {
|
if let statusValue = strongSelf.statusValue, let _ = strongSelf.scrubbingBeginTimestamp, Double(0.0).isLess(than: statusValue.duration) {
|
||||||
strongSelf.scrubbingBeginTimestamp = strongSelf.scrubbingTimestampValue
|
strongSelf.scrubbingBeginTimestamp = strongSelf.scrubbingTimestampValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -735,6 +777,20 @@ public final class MediaPlayerScrubbingNode: ASDisplayNode {
|
|||||||
node.bufferingNode.frame = backgroundFrame
|
node.bufferingNode.frame = backgroundFrame
|
||||||
node.bufferingNode.updateLayout(size: backgroundFrame.size, transition: .immediate)
|
node.bufferingNode.updateLayout(size: backgroundFrame.size, transition: .immediate)
|
||||||
|
|
||||||
|
if let chapterNodesContainer = node.chapterNodesContainer, let duration = timestampAndDuration?.duration, duration > 0.0, backgroundFrame.width > 0.0 {
|
||||||
|
chapterNodesContainer.frame = backgroundFrame
|
||||||
|
|
||||||
|
for i in 0 ..< node.chapterNodes.count {
|
||||||
|
let (chapter, chapterNode) = node.chapterNodes[i]
|
||||||
|
if i == 0 || chapter.start > duration {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
let chapterPosition: CGFloat = floor(backgroundFrame.width * CGFloat(chapter.start / duration))
|
||||||
|
let chapterLineWidth: CGFloat = 1.5
|
||||||
|
chapterNode.frame = CGRect(x: chapterPosition - chapterLineWidth / 2.0, y: 0.0, width: chapterLineWidth, height: backgroundFrame.size.height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let handleNode = node.handleNode {
|
if let handleNode = node.handleNode {
|
||||||
var handleSize: CGSize = CGSize(width: 2.0, height: bounds.size.height)
|
var handleSize: CGSize = CGSize(width: 2.0, height: bounds.size.height)
|
||||||
var handleOffset: CGFloat = 0.0
|
var handleOffset: CGFloat = 0.0
|
||||||
|
|||||||
@ -261,7 +261,7 @@ final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, UIScrollViewDeleg
|
|||||||
self.actionPlayNode.image = PresentationResourcesRootController.navigationPlayerPlayIcon(self.theme)
|
self.actionPlayNode.image = PresentationResourcesRootController.navigationPlayerPlayIcon(self.theme)
|
||||||
self.actionPlayNode.isHidden = true
|
self.actionPlayNode.isHidden = true
|
||||||
|
|
||||||
self.scrubbingNode = MediaPlayerScrubbingNode(content: .standard(lineHeight: 2.0, lineCap: .square, scrubberHandle: .none, backgroundColor: .clear, foregroundColor: self.theme.rootController.navigationBar.accentTextColor, bufferingColor: self.theme.rootController.navigationBar.accentTextColor.withAlphaComponent(0.5)))
|
self.scrubbingNode = MediaPlayerScrubbingNode(content: .standard(lineHeight: 2.0, lineCap: .square, scrubberHandle: .none, backgroundColor: .clear, foregroundColor: self.theme.rootController.navigationBar.accentTextColor, bufferingColor: self.theme.rootController.navigationBar.accentTextColor.withAlphaComponent(0.5), chapters: []))
|
||||||
|
|
||||||
self.separatorNode = ASDisplayNode()
|
self.separatorNode = ASDisplayNode()
|
||||||
self.separatorNode.isLayerBacked = true
|
self.separatorNode.isLayerBacked = true
|
||||||
@ -375,7 +375,7 @@ final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, UIScrollViewDeleg
|
|||||||
self.actionPlayNode.image = PresentationResourcesRootController.navigationPlayerPlayIcon(self.theme)
|
self.actionPlayNode.image = PresentationResourcesRootController.navigationPlayerPlayIcon(self.theme)
|
||||||
self.actionPauseNode.image = PresentationResourcesRootController.navigationPlayerPauseIcon(self.theme)
|
self.actionPauseNode.image = PresentationResourcesRootController.navigationPlayerPauseIcon(self.theme)
|
||||||
self.separatorNode.backgroundColor = self.theme.rootController.navigationBar.separatorColor
|
self.separatorNode.backgroundColor = self.theme.rootController.navigationBar.separatorColor
|
||||||
self.scrubbingNode.updateContent(.standard(lineHeight: 2.0, lineCap: .square, scrubberHandle: .none, backgroundColor: .clear, foregroundColor: self.theme.rootController.navigationBar.accentTextColor, bufferingColor: self.theme.rootController.navigationBar.accentTextColor.withAlphaComponent(0.5)))
|
self.scrubbingNode.updateContent(.standard(lineHeight: 2.0, lineCap: .square, scrubberHandle: .none, backgroundColor: .clear, foregroundColor: self.theme.rootController.navigationBar.accentTextColor, bufferingColor: self.theme.rootController.navigationBar.accentTextColor.withAlphaComponent(0.5), chapters: []))
|
||||||
|
|
||||||
if let playbackBaseRate = self.playbackBaseRate {
|
if let playbackBaseRate = self.playbackBaseRate {
|
||||||
switch playbackBaseRate {
|
switch playbackBaseRate {
|
||||||
|
|||||||
@ -170,7 +170,7 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
|
|||||||
self.shareNode = HighlightableButtonNode()
|
self.shareNode = HighlightableButtonNode()
|
||||||
self.shareNode.setImage(generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/Share"), color: presentationData.theme.list.itemAccentColor), for: [])
|
self.shareNode.setImage(generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/Share"), color: presentationData.theme.list.itemAccentColor), for: [])
|
||||||
|
|
||||||
self.scrubberNode = MediaPlayerScrubbingNode(content: .standard(lineHeight: 3.0, lineCap: .round, scrubberHandle: .circle, backgroundColor: presentationData.theme.list.controlSecondaryColor, foregroundColor: presentationData.theme.list.itemAccentColor, bufferingColor: presentationData.theme.list.itemAccentColor.withAlphaComponent(0.4)))
|
self.scrubberNode = MediaPlayerScrubbingNode(content: .standard(lineHeight: 3.0, lineCap: .round, scrubberHandle: .circle, backgroundColor: presentationData.theme.list.controlSecondaryColor, foregroundColor: presentationData.theme.list.itemAccentColor, bufferingColor: presentationData.theme.list.itemAccentColor.withAlphaComponent(0.4), chapters: []))
|
||||||
self.leftDurationLabel = MediaPlayerTimeTextNode(textColor: presentationData.theme.list.itemSecondaryTextColor)
|
self.leftDurationLabel = MediaPlayerTimeTextNode(textColor: presentationData.theme.list.itemSecondaryTextColor)
|
||||||
self.leftDurationLabel.displaysAsynchronously = false
|
self.leftDurationLabel.displaysAsynchronously = false
|
||||||
self.leftDurationLabel.keepPreviousValueOnEmptyState = true
|
self.leftDurationLabel.keepPreviousValueOnEmptyState = true
|
||||||
|
|||||||
@ -41,6 +41,12 @@ private let validTimecodeSet: CharacterSet = {
|
|||||||
set.insert(":")
|
set.insert(":")
|
||||||
return set
|
return set
|
||||||
}()
|
}()
|
||||||
|
private let validTimecodePreviousSet: CharacterSet = {
|
||||||
|
var set = CharacterSet.whitespacesAndNewlines
|
||||||
|
set.insert("(")
|
||||||
|
set.insert("[")
|
||||||
|
return set
|
||||||
|
}()
|
||||||
|
|
||||||
public struct ApplicationSpecificEntityType {
|
public struct ApplicationSpecificEntityType {
|
||||||
public static let Timecode: Int32 = 1
|
public static let Timecode: Int32 = 1
|
||||||
@ -320,7 +326,7 @@ public func addLocallyGeneratedEntities(_ text: String, enabledTypes: EnabledEnt
|
|||||||
notFound = false
|
notFound = false
|
||||||
if let (type, range) = currentEntity, type == .timecode {
|
if let (type, range) = currentEntity, type == .timecode {
|
||||||
currentEntity = (.timecode, range.lowerBound ..< utf16.index(after: index))
|
currentEntity = (.timecode, range.lowerBound ..< utf16.index(after: index))
|
||||||
} else if previousScalar == nil || CharacterSet.whitespacesAndNewlines.contains(previousScalar!) {
|
} else if previousScalar == nil || validTimecodePreviousSet.contains(previousScalar!) {
|
||||||
currentEntity = (.timecode, index ..< index)
|
currentEntity = (.timecode, index ..< index)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user