From eabcd258f1668c6d3036dd485cb211f0df20b93f Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Thu, 30 Apr 2020 17:04:59 +0400 Subject: [PATCH] Add media chapters UI --- .../ChatItemGalleryFooterContentNode.swift | 4 +- .../ChatVideoGalleryItemScrubberView.swift | 71 ++++++++++++++---- .../GalleryUI/Sources/GalleryController.swift | 28 +++++-- .../Sources/Items/ChatImageGalleryItem.swift | 10 ++- .../Items/UniversalVideoGalleryItem.swift | 10 ++- .../SecretMediaPreviewController.swift | 2 +- .../Sources/InstantPageAudioNode.swift | 2 +- .../InstantPageGalleryController.swift | 4 +- .../Sources/MediaPlayerScrubbingNode.swift | 74 ++++++++++++++++--- .../MediaNavigationAccessoryHeaderNode.swift | 4 +- .../Sources/OverlayPlayerControlsNode.swift | 2 +- .../Sources/GenerateTextEntities.swift | 8 +- 12 files changed, 169 insertions(+), 50 deletions(-) diff --git a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift index 41f3cf1df7..78c643057f 100644 --- a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift +++ b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift @@ -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.presentationData = presentationData 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 // self?.updateIsTextSelectionActive?(value) }, present: { [weak self] c, a in -// self?.item?.controllerInteraction.presentGlobalOverlayController(c, a) + present(c, a) }, rootNode: self, performAction: { [weak self] text, action in // guard let strongSelf = self, let item = strongSelf.item else { // return diff --git a/submodules/GalleryUI/Sources/ChatVideoGalleryItemScrubberView.swift b/submodules/GalleryUI/Sources/ChatVideoGalleryItemScrubberView.swift index fc09e8a10e..4dc60b81e7 100644 --- a/submodules/GalleryUI/Sources/ChatVideoGalleryItemScrubberView.swift +++ b/submodules/GalleryUI/Sources/ChatVideoGalleryItemScrubberView.swift @@ -11,21 +11,28 @@ import TelegramPresentationData 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 { private var containerLayout: (CGSize, CGFloat, CGFloat)? private let leftTimestampNode: MediaPlayerTimeTextNode private let rightTimestampNode: MediaPlayerTimeTextNode - private let fileSizeNode: ASTextNode + private let infoNode: ASTextNode private let scrubberNode: MediaPlayerScrubbingNode private var playbackStatus: MediaPlayerStatus? + private var chapters: [MediaPlayerScrubbingChapter] = [] private var fetchStatusDisposable = MetaDisposable() private var scrubbingDisposable = MetaDisposable() + private var chapterDisposable = MetaDisposable() private var leftTimestampNodePushed = false private var rightTimestampNodePushed = false + private var infoNodePushed = false var hideWhenDurationIsUnknown = false { didSet { @@ -53,17 +60,17 @@ final class ChatVideoGalleryItemScrubberView: UIView { var seek: (Double) -> Void = { _ in } 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.rightTimestampNode = MediaPlayerTimeTextNode(textColor: .white) self.rightTimestampNode.alignment = .right self.rightTimestampNode.mode = .reversed - self.fileSizeNode = ASTextNode() - self.fileSizeNode.maximumNumberOfLines = 1 - self.fileSizeNode.isUserInteractionEnabled = false - self.fileSizeNode.displaysAsynchronously = false + self.infoNode = ASTextNode() + self.infoNode.maximumNumberOfLines = 1 + self.infoNode.isUserInteractionEnabled = false + self.infoNode.displaysAsynchronously = false super.init(frame: frame) @@ -97,11 +104,11 @@ final class ChatVideoGalleryItemScrubberView: UIView { } } } - + self.addSubnode(self.scrubberNode) self.addSubnode(self.leftTimestampNode) self.addSubnode(self.rightTimestampNode) - self.addSubnode(self.fileSizeNode) + self.addSubnode(self.infoNode) } required init?(coder aDecoder: NSCoder) { @@ -126,6 +133,29 @@ final class ChatVideoGalleryItemScrubberView: UIView { self.leftTimestampNode.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 |> deliverOnMainQueue).start(next: { [weak self] value in guard let strongSelf = self else { @@ -133,16 +163,20 @@ final class ChatVideoGalleryItemScrubberView: UIView { } let leftTimestampNodePushed: Bool let rightTimestampNodePushed: Bool + let infoNodePushed: Bool if let value = value { leftTimestampNodePushed = value < 0.16 rightTimestampNodePushed = value > 0.84 + infoNodePushed = value >= 0.16 && value <= 0.84 } else { leftTimestampNodePushed = 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.rightTimestampNodePushed = rightTimestampNodePushed + strongSelf.infoNodePushed = infoNodePushed if let layout = strongSelf.containerLayout { 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: 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 { strongSelf.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset, transition: .immediate) @@ -178,10 +212,10 @@ final class ChatVideoGalleryItemScrubberView: UIView { } })) } 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 { - self.fileSizeNode.attributedText = nil + self.infoNode.attributedText = nil } } @@ -192,22 +226,29 @@ final class ChatVideoGalleryItemScrubberView: UIView { let scrubberInset: CGFloat let leftTimestampOffset: CGFloat let rightTimestampOffset: CGFloat + let infoOffset: CGFloat if size.width > size.height { scrubberInset = 58.0 leftTimestampOffset = 4.0 rightTimestampOffset = 4.0 + infoOffset = 0.0 } else { scrubberInset = 13.0 leftTimestampOffset = 22.0 + (self.leftTimestampNodePushed ? 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.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) - self.fileSizeNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - fileSize.width) / 2.0), y: 22.0), size: fileSize) - self.fileSizeNode.alpha = size.width < size.height ? 1.0 : 0.0 + var infoConstrainedSize = size + infoConstrainedSize.width = size.width - scrubberInset * 2.0 - 100.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)) } diff --git a/submodules/GalleryUI/Sources/GalleryController.swift b/submodules/GalleryUI/Sources/GalleryController.swift index 448039e228..ddfb588d59 100644 --- a/submodules/GalleryUI/Sources/GalleryController.swift +++ b/submodules/GalleryUI/Sources/GalleryController.swift @@ -140,12 +140,12 @@ private func galleryMessageCaptionText(_ message: Message) -> String { return message.text } -public func galleryItemForEntry(context: AccountContext, presentationData: PresentationData, entry: MessageHistoryEntry, isCentral: Bool = false, streamVideos: Bool, loopVideos: Bool = false, hideControls: Bool = false, fromPlayingVideo: Bool = false, 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 location = entry.location if let (media, mediaImage) = mediaForMessage(message: message) { 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 { if file.isVideo { let content: UniversalVideoContent @@ -173,7 +173,7 @@ public func galleryItemForEntry(context: AccountContext, presentationData: Prese } let caption = galleryCaptionStringWithAppliedEntities(text, entities: entities) - return UniversalVideoGalleryItem(context: context, presentationData: presentationData, content: content, originData: GalleryItemOriginData(title: message.effectiveAuthor?.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), timestamp: message.timestamp), indexData: location.flatMap { GalleryItemIndexData(position: Int32($0.index), totalCount: Int32($0.count)) }, contentInfo: .message(message), caption: caption, 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 { if let fileName = file.fileName, (fileName as NSString).pathExtension.lowercased() == "json" { 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) } 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 { 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 { - 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 { return nil } @@ -501,7 +501,11 @@ public class GalleryController: ViewController, StandalonePresentableController if entry.message.stableId == strongSelf.centralEntryStableId { isCentral = true } - if let item = galleryItemForEntry(context: context, presentationData: strongSelf.presentationData, entry: entry, isCentral: isCentral, streamVideos: streamSingleVideo, fromPlayingVideo: isCentral && fromPlayingVideo, landscape: isCentral && landscape, timecode: isCentral ? timecode : nil, 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 { centralItemIndex = items.count } @@ -925,7 +929,11 @@ public class GalleryController: ViewController, StandalonePresentableController if entry.message.stableId == self.centralEntryStableId { isCentral = true } - if let item = galleryItemForEntry(context: self.context, presentationData: self.presentationData, entry: entry, streamVideos: self.streamVideos, fromPlayingVideo: isCentral && self.fromPlayingVideo, landscape: isCentral && self.landscape, timecode: isCentral ? self.timecode : nil, 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 { centralItemIndex = items.count } @@ -1001,7 +1009,11 @@ public class GalleryController: ViewController, StandalonePresentableController if entry.message.stableId == strongSelf.centralEntryStableId { isCentral = true } - if let item = galleryItemForEntry(context: strongSelf.context, presentationData: strongSelf.presentationData, entry: entry, isCentral: isCentral, streamVideos: false, fromPlayingVideo: isCentral && strongSelf.fromPlayingVideo, landscape: isCentral && strongSelf.landscape, timecode: isCentral ? strongSelf.timecode : nil, 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 { centralItemIndex = items.count } diff --git a/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift b/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift index e95bec133d..b023095467 100644 --- a/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift @@ -89,18 +89,20 @@ class ChatImageGalleryItem: GalleryItem { let location: MessageHistoryEntryLocation? let performAction: (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.presentationData = presentationData self.message = message self.location = location self.performAction = performAction self.openActionOptions = openActionOptions + self.present = present } 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 { if let image = media as? TelegramMediaImage { @@ -177,11 +179,11 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { private let dataDisposable = MetaDisposable() 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.imageNode = TransformImageNode() - self.footerContentNode = ChatItemGalleryFooterContentNode(context: context, presentationData: presentationData) + self.footerContentNode = ChatItemGalleryFooterContentNode(context: context, presentationData: presentationData, present: present) self.footerContentNode.performAction = performAction self.footerContentNode.openActionOptions = openActionOptions diff --git a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift index ba3a583887..17f95d92a3 100644 --- a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift @@ -40,8 +40,9 @@ public class UniversalVideoGalleryItem: GalleryItem { let performAction: (GalleryControllerInteractionTapAction) -> Void let openActionOptions: (GalleryControllerInteractionTapAction) -> 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.presentationData = presentationData self.content = content @@ -59,10 +60,11 @@ public class UniversalVideoGalleryItem: GalleryItem { self.performAction = performAction self.openActionOptions = openActionOptions self.storeMediaPlaybackState = storeMediaPlaybackState + self.present = present } 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 { 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)? - 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.presentationData = presentationData 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.performAction = performAction self.footerContentNode.openActionOptions = openActionOptions diff --git a/submodules/GalleryUI/Sources/SecretMediaPreviewController.swift b/submodules/GalleryUI/Sources/SecretMediaPreviewController.swift index 3c36aeaa18..b554c91e53 100644 --- a/submodules/GalleryUI/Sources/SecretMediaPreviewController.swift +++ b/submodules/GalleryUI/Sources/SecretMediaPreviewController.swift @@ -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 self?.dismiss(forceAway: false) - }) else { + }, present: { _, _ in }) else { self._ready.set(.single(true)) return } diff --git a/submodules/InstantPageUI/Sources/InstantPageAudioNode.swift b/submodules/InstantPageUI/Sources/InstantPageAudioNode.swift index ce5b312d17..b0e395ec01 100644 --- a/submodules/InstantPageUI/Sources/InstantPageAudioNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPageAudioNode.swift @@ -99,7 +99,7 @@ final class InstantPageAudioNode: ASDisplayNode, InstantPageNode { if brightness > 0.5 { 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 if let file = self.media.media as? TelegramMediaFile { diff --git a/submodules/InstantPageUI/Sources/InstantPageGalleryController.swift b/submodules/InstantPageUI/Sources/InstantPageGalleryController.swift index 6c7ef95f88..652a0ed9b7 100644 --- a/submodules/InstantPageUI/Sources/InstantPageGalleryController.swift +++ b/submodules/InstantPageUI/Sources/InstantPageGalleryController.swift @@ -113,7 +113,7 @@ public struct InstantPageGalleryEntry: Equatable { nativeId = .instantPage(self.pageId, file.fileId) } - return UniversalVideoGalleryItem(context: context, presentationData: presentationData, content: NativeVideoContent(id: nativeId, fileReference: .webPage(webPage: WebpageReference(webPage), media: file), streamVideo: isMediaStreamable(media: file) ? .conservative : .none), originData: nil, indexData: indexData, contentInfo: .webPage(webPage, file), 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 { var representations: [TelegramMediaImageRepresentation] = [] 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 { 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 { preconditionFailure() } diff --git a/submodules/MediaPlayer/Sources/MediaPlayerScrubbingNode.swift b/submodules/MediaPlayer/Sources/MediaPlayerScrubbingNode.swift index aec2dd2062..de63d90e11 100644 --- a/submodules/MediaPlayer/Sources/MediaPlayerScrubbingNode.swift +++ b/submodules/MediaPlayer/Sources/MediaPlayerScrubbingNode.swift @@ -18,6 +18,16 @@ private func generateHandleBackground(color: UIColor) -> UIImage? { })?.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 { var beginScrubbing: (() -> Void)? var endScrubbing: ((Bool) -> Void)? @@ -144,8 +154,8 @@ public enum MediaPlayerScrubbingNodeHandle { } public enum MediaPlayerScrubbingNodeContent { - case standard(lineHeight: CGFloat, lineCap: MediaPlayerScrubbingNodeCap, scrubberHandle: MediaPlayerScrubbingNodeHandle, backgroundColor: UIColor, foregroundColor: UIColor, bufferingColor: UIColor) - case custom(backgroundNode: ASDisplayNode, foregroundContentNode: ASDisplayNode) + case standard(lineHeight: CGFloat, lineCap: MediaPlayerScrubbingNodeCap, scrubberHandle: MediaPlayerScrubbingNodeHandle, backgroundColor: UIColor, foregroundColor: UIColor, bufferingColor: UIColor, chapters: [MediaPlayerScrubbingChapter]) + case custom(backgroundNode: CustomMediaPlayerScrubbingForegroundNode, foregroundContentNode: CustomMediaPlayerScrubbingForegroundNode) } private final class StandardMediaPlayerScrubbingNodeContentNode { @@ -155,18 +165,22 @@ private final class StandardMediaPlayerScrubbingNodeContentNode { let bufferingNode: MediaPlayerScrubbingBufferingNode let foregroundContentNode: ASImageNode let foregroundNode: MediaPlayerScrubbingForegroundNode + let chapterNodesContainer: ASDisplayNode? + let chapterNodes: [(MediaPlayerScrubbingChapter, ASDisplayNode)] let handle: MediaPlayerScrubbingNodeHandle let handleNode: ASDisplayNode? let highlightedHandleNode: ASDisplayNode? 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.lineCap = lineCap self.backgroundNode = backgroundNode self.bufferingNode = bufferingNode self.foregroundContentNode = foregroundContentNode self.foregroundNode = foregroundNode + self.chapterNodesContainer = chapterNodesContainer + self.chapterNodes = chapterNodes self.handle = handle self.handleNode = handleNode self.highlightedHandleNode = highlightedHandleNode @@ -174,13 +188,17 @@ private final class StandardMediaPlayerScrubbingNodeContentNode { } } +public protocol CustomMediaPlayerScrubbingForegroundNode: ASDisplayNode { + var progress: CGFloat? { get set } +} + private final class CustomMediaPlayerScrubbingNodeContentNode { - let backgroundNode: ASDisplayNode - let foregroundContentNode: ASDisplayNode + let backgroundNode: CustomMediaPlayerScrubbingForegroundNode + let foregroundContentNode: CustomMediaPlayerScrubbingForegroundNode let foregroundNode: MediaPlayerScrubbingForegroundNode 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.foregroundContentNode = foregroundContentNode self.foregroundNode = foregroundNode @@ -344,7 +362,7 @@ public final class MediaPlayerScrubbingNode: ASDisplayNode { private static func contentNodesFromContent(_ content: MediaPlayerScrubbingNodeContent, enableScrubbing: Bool) -> MediaPlayerScrubbingNodeContentNodes { switch content { - case let .standard(lineHeight, lineCap, scrubberHandle, backgroundColor, foregroundColor, bufferingColor): + case let .standard(lineHeight, lineCap, scrubberHandle, backgroundColor, foregroundColor, bufferingColor, chapters): let backgroundNode = ASImageNode() backgroundNode.isLayerBacked = true backgroundNode.displaysAsynchronously = false @@ -408,7 +426,27 @@ public final class MediaPlayerScrubbingNode: ASDisplayNode { 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): let foregroundNode = MediaPlayerScrubbingForegroundNode() foregroundNode.isLayerBacked = true @@ -464,6 +502,10 @@ public final class MediaPlayerScrubbingNode: ASDisplayNode { node.foregroundNode.addSubnode(node.foregroundContentNode) self.addSubnode(node.foregroundNode) + if let chapterNodesContainer = node.chapterNodesContainer { + self.addSubnode(chapterNodesContainer) + } + if let handleNodeContainer = node.handleNodeContainer { self.addSubnode(handleNodeContainer) handleNodeContainer.highlighted = { [weak self] highlighted in @@ -581,7 +623,7 @@ public final class MediaPlayerScrubbingNode: ASDisplayNode { } handleNodeContainer.updateMultiplier = { [weak self] multiplier in 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 } } @@ -735,6 +777,20 @@ public final class MediaPlayerScrubbingNode: ASDisplayNode { node.bufferingNode.frame = backgroundFrame 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 { var handleSize: CGSize = CGSize(width: 2.0, height: bounds.size.height) var handleOffset: CGFloat = 0.0 diff --git a/submodules/TelegramBaseController/Sources/MediaNavigationAccessoryHeaderNode.swift b/submodules/TelegramBaseController/Sources/MediaNavigationAccessoryHeaderNode.swift index a9fd7074dc..518bad26a3 100644 --- a/submodules/TelegramBaseController/Sources/MediaNavigationAccessoryHeaderNode.swift +++ b/submodules/TelegramBaseController/Sources/MediaNavigationAccessoryHeaderNode.swift @@ -261,7 +261,7 @@ final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, UIScrollViewDeleg self.actionPlayNode.image = PresentationResourcesRootController.navigationPlayerPlayIcon(self.theme) 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.isLayerBacked = true @@ -375,7 +375,7 @@ final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, UIScrollViewDeleg self.actionPlayNode.image = PresentationResourcesRootController.navigationPlayerPlayIcon(self.theme) self.actionPauseNode.image = PresentationResourcesRootController.navigationPlayerPauseIcon(self.theme) 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 { switch playbackBaseRate { diff --git a/submodules/TelegramUI/Sources/OverlayPlayerControlsNode.swift b/submodules/TelegramUI/Sources/OverlayPlayerControlsNode.swift index e866e57ec7..617d70e583 100644 --- a/submodules/TelegramUI/Sources/OverlayPlayerControlsNode.swift +++ b/submodules/TelegramUI/Sources/OverlayPlayerControlsNode.swift @@ -170,7 +170,7 @@ final class OverlayPlayerControlsNode: ASDisplayNode { self.shareNode = HighlightableButtonNode() 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.displaysAsynchronously = false self.leftDurationLabel.keepPreviousValueOnEmptyState = true diff --git a/submodules/TextFormat/Sources/GenerateTextEntities.swift b/submodules/TextFormat/Sources/GenerateTextEntities.swift index 862e92c82a..6fa025aa5d 100644 --- a/submodules/TextFormat/Sources/GenerateTextEntities.swift +++ b/submodules/TextFormat/Sources/GenerateTextEntities.swift @@ -41,6 +41,12 @@ private let validTimecodeSet: CharacterSet = { set.insert(":") return set }() +private let validTimecodePreviousSet: CharacterSet = { + var set = CharacterSet.whitespacesAndNewlines + set.insert("(") + set.insert("[") + return set +}() public struct ApplicationSpecificEntityType { public static let Timecode: Int32 = 1 @@ -320,7 +326,7 @@ public func addLocallyGeneratedEntities(_ text: String, enabledTypes: EnabledEnt notFound = false if let (type, range) = currentEntity, type == .timecode { 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) } }