mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-16 11:20:18 +00:00
Story video playback speed
This commit is contained in:
parent
a25e41ff73
commit
83e0f2c4a3
@ -94,7 +94,8 @@ swift_library(
|
||||
"//submodules/Components/BalancedTextComponent",
|
||||
"//submodules/AnimatedCountLabelNode",
|
||||
"//submodules/StickerResources",
|
||||
"//submodules/TelegramUI/Components/Stories/ForwardInfoPanelComponent"
|
||||
"//submodules/TelegramUI/Components/Stories/ForwardInfoPanelComponent",
|
||||
"//submodules/TelegramUI/Components/SliderContextItem",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
||||
@ -31,6 +31,7 @@ public final class StoryContentItem: Equatable {
|
||||
|
||||
public final class SharedState {
|
||||
public var replyDrafts: [StoryId: NSAttributedString] = [:]
|
||||
public var baseRate: Double = 1.0
|
||||
|
||||
public init() {
|
||||
}
|
||||
@ -55,6 +56,9 @@ public final class StoryContentItem: Equatable {
|
||||
open func enterAmbientMode(ambient: Bool) {
|
||||
}
|
||||
|
||||
open func setBaseRate(_ baseRate: Double) {
|
||||
}
|
||||
|
||||
open var videoPlaybackPosition: Double? {
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -33,11 +33,12 @@ final class StoryItemContentComponent: Component {
|
||||
let availableReactions: StoryAvailableReactions?
|
||||
let entityFiles: [MediaId: TelegramMediaFile]
|
||||
let audioMode: StoryContentItem.AudioMode
|
||||
let baseRate: Double
|
||||
let isVideoBuffering: Bool
|
||||
let isCurrent: Bool
|
||||
let activateReaction: (UIView, MessageReaction.Reaction) -> Void
|
||||
|
||||
init(context: AccountContext, strings: PresentationStrings, peer: EnginePeer, item: EngineStoryItem, availableReactions: StoryAvailableReactions?, entityFiles: [MediaId: TelegramMediaFile], audioMode: StoryContentItem.AudioMode, isVideoBuffering: Bool, isCurrent: Bool, activateReaction: @escaping (UIView, MessageReaction.Reaction) -> Void) {
|
||||
init(context: AccountContext, strings: PresentationStrings, peer: EnginePeer, item: EngineStoryItem, availableReactions: StoryAvailableReactions?, entityFiles: [MediaId: TelegramMediaFile], audioMode: StoryContentItem.AudioMode, baseRate: Double, isVideoBuffering: Bool, isCurrent: Bool, activateReaction: @escaping (UIView, MessageReaction.Reaction) -> Void) {
|
||||
self.context = context
|
||||
self.strings = strings
|
||||
self.peer = peer
|
||||
@ -45,6 +46,7 @@ final class StoryItemContentComponent: Component {
|
||||
self.entityFiles = entityFiles
|
||||
self.availableReactions = availableReactions
|
||||
self.audioMode = audioMode
|
||||
self.baseRate = baseRate
|
||||
self.isVideoBuffering = isVideoBuffering
|
||||
self.isCurrent = isCurrent
|
||||
self.activateReaction = activateReaction
|
||||
@ -69,6 +71,9 @@ final class StoryItemContentComponent: Component {
|
||||
if lhs.entityFiles.keys != rhs.entityFiles.keys {
|
||||
return false
|
||||
}
|
||||
if lhs.baseRate != rhs.baseRate {
|
||||
return false
|
||||
}
|
||||
if lhs.isVideoBuffering != rhs.isVideoBuffering {
|
||||
return false
|
||||
}
|
||||
@ -221,6 +226,7 @@ final class StoryItemContentComponent: Component {
|
||||
priority: .gallery
|
||||
)
|
||||
videoNode.isHidden = true
|
||||
videoNode.setBaseRate(component.baseRate)
|
||||
|
||||
self.videoNode = videoNode
|
||||
self.insertSubview(videoNode.view, aboveSubview: self.imageView)
|
||||
@ -325,6 +331,12 @@ final class StoryItemContentComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
override func setBaseRate(_ baseRate: Double) {
|
||||
if let videoNode = self.videoNode {
|
||||
videoNode.setBaseRate(baseRate)
|
||||
}
|
||||
}
|
||||
|
||||
private func updateProgressMode(update: Bool) {
|
||||
if let videoNode = self.videoNode {
|
||||
let canPlay = self.progressMode != .pause && self.contentLoaded && self.hierarchyTrackingLayer.isInHierarchy
|
||||
|
||||
@ -42,6 +42,7 @@ import TranslateUI
|
||||
import TelegramUIPreferences
|
||||
import StoryFooterPanelComponent
|
||||
import TelegramNotices
|
||||
import SliderContextItem
|
||||
|
||||
public final class StoryAvailableReactions: Equatable {
|
||||
let reactionItems: [ReactionItem]
|
||||
@ -6021,6 +6022,73 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
return (tip, tipSignal)
|
||||
}
|
||||
|
||||
private func contextMenuSpeedItems(value: ValuePromise<Double>) -> Signal<[ContextMenuItem], NoError> {
|
||||
guard let component = self.component else {
|
||||
return .single([])
|
||||
}
|
||||
|
||||
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme)
|
||||
|
||||
let baseRate = component.storyItemSharedState.baseRate
|
||||
let valuePromise = ValuePromise<Double?>(nil)
|
||||
|
||||
var items: [ContextMenuItem] = []
|
||||
|
||||
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Common_Back, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.actionSheet.primaryTextColor)
|
||||
}, iconPosition: .left, action: { c, _ in
|
||||
c.popItems()
|
||||
})))
|
||||
|
||||
items.append(.custom(SliderContextItem(minValue: 0.2, maxValue: 2.5, value: baseRate, valueChanged: { [weak self] newValue, done in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
func normalizeValue(_ value: CGFloat) -> CGFloat {
|
||||
return round(value * 10.0) / 10.0
|
||||
}
|
||||
|
||||
let rate = normalizeValue(newValue)
|
||||
if let visibleItem = self.visibleItems[component.slice.item.storyItem.id], let view = visibleItem.view.view as? StoryItemContentComponent.View {
|
||||
view.setBaseRate(rate)
|
||||
}
|
||||
|
||||
component.storyItemSharedState.baseRate = rate
|
||||
valuePromise.set(rate)
|
||||
|
||||
if done {
|
||||
value.set(rate)
|
||||
}
|
||||
}), true))
|
||||
|
||||
items.append(.separator)
|
||||
|
||||
for (text, _, rate) in speedList(strings: presentationData.strings) {
|
||||
let isSelected = abs(baseRate - rate) < 0.01
|
||||
items.append(.action(ContextMenuActionItem(text: text, icon: { _ in return nil }, iconSource: ContextMenuActionItemIconSource(size: CGSize(width: 24.0, height: 24.0), signal: valuePromise.get()
|
||||
|> map { value in
|
||||
if isSelected && value == nil {
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: .white)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}), action: { [weak self] _, f in
|
||||
f(.default)
|
||||
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
|
||||
if let visibleItem = self.visibleItems[component.slice.item.storyItem.id], let view = visibleItem.view.view as? StoryItemContentComponent.View {
|
||||
view.setBaseRate(rate)
|
||||
}
|
||||
component.storyItemSharedState.baseRate = rate
|
||||
})))
|
||||
}
|
||||
|
||||
return .single(items)
|
||||
}
|
||||
|
||||
private func performMyMoreAction(sourceView: UIView, gesture: ContextGesture?) {
|
||||
guard let component = self.component, let controller = component.controller() else {
|
||||
return
|
||||
@ -6028,9 +6096,48 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
|
||||
self.dismissAllTooltips()
|
||||
|
||||
let baseRatePromise = ValuePromise<Double>(component.storyItemSharedState.baseRate)
|
||||
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme)
|
||||
|
||||
|
||||
let contextItems = baseRatePromise.get()
|
||||
|> mapToSignal { [weak self, weak component] baseRate -> Signal<ContextController.Items , NoError> in
|
||||
guard let self, let component else {
|
||||
return .complete()
|
||||
}
|
||||
|
||||
var items: [ContextMenuItem] = []
|
||||
|
||||
if case .file = component.slice.item.storyItem.media {
|
||||
var speedValue: String = presentationData.strings.PlaybackSpeed_Normal
|
||||
var speedIconText: String = "1x"
|
||||
var didSetSpeedValue = false
|
||||
for (text, iconText, speed) in speedList(strings: presentationData.strings) {
|
||||
if abs(speed - baseRate) < 0.01 {
|
||||
speedValue = text
|
||||
speedIconText = iconText
|
||||
didSetSpeedValue = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !didSetSpeedValue && baseRate != 1.0 {
|
||||
speedValue = String(format: "%.1fx", baseRate)
|
||||
speedIconText = speedValue
|
||||
}
|
||||
|
||||
items.append(.action(ContextMenuActionItem(text: presentationData.strings.PlaybackSpeed_Title, textLayout: .secondLineWithValue(speedValue), icon: { theme in
|
||||
return optionsRateImage(rate: speedIconText, isLarge: false, color: theme.contextMenu.primaryColor)
|
||||
}, action: { [weak self] c, _ in
|
||||
guard let self else {
|
||||
c.dismiss(completion: nil)
|
||||
return
|
||||
}
|
||||
|
||||
c.pushItems(items: self.contextMenuSpeedItems(value: baseRatePromise) |> map { ContextController.Items(content: .list($0)) })
|
||||
})))
|
||||
items.append(.separator)
|
||||
}
|
||||
|
||||
let additionalCount = component.slice.item.storyItem.privacy?.additionallyIncludePeers.count ?? 0
|
||||
let privacyText: String
|
||||
switch component.slice.item.storyItem.privacy?.base {
|
||||
@ -6181,9 +6288,10 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
|
||||
let (tip, tipSignal) = self.getLinkedStickerPacks()
|
||||
|
||||
let contextItems = ContextController.Items(content: .list(items), tip: tip, tipSignal: tipSignal)
|
||||
return .single(ContextController.Items(id: 0, content: .list(items), tip: tip, tipSignal: tipSignal))
|
||||
})
|
||||
|
||||
let contextController = ContextController(presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceView: sourceView, position: .bottom)), items: .single(contextItems), gesture: gesture)
|
||||
let contextController = ContextController(presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceView: sourceView, position: .bottom)), items: contextItems, gesture: gesture)
|
||||
contextController.dismissed = { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
@ -6206,9 +6314,46 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
|
||||
self.dismissAllTooltips()
|
||||
|
||||
let baseRatePromise = ValuePromise<Double>(component.storyItemSharedState.baseRate)
|
||||
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme)
|
||||
|
||||
let contextItems = baseRatePromise.get()
|
||||
|> mapToSignal { [weak self, weak component] baseRate -> Signal<ContextController.Items , NoError> in
|
||||
guard let self, let component else {
|
||||
return .complete()
|
||||
}
|
||||
var items: [ContextMenuItem] = []
|
||||
|
||||
if case .file = component.slice.item.storyItem.media {
|
||||
var speedValue: String = presentationData.strings.PlaybackSpeed_Normal
|
||||
var speedIconText: String = "1x"
|
||||
var didSetSpeedValue = false
|
||||
for (text, iconText, speed) in speedList(strings: presentationData.strings) {
|
||||
if abs(speed - baseRate) < 0.01 {
|
||||
speedValue = text
|
||||
speedIconText = iconText
|
||||
didSetSpeedValue = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !didSetSpeedValue && baseRate != 1.0 {
|
||||
speedValue = String(format: "%.1fx", baseRate)
|
||||
speedIconText = speedValue
|
||||
}
|
||||
|
||||
items.append(.action(ContextMenuActionItem(text: presentationData.strings.PlaybackSpeed_Title, textLayout: .secondLineWithValue(speedValue), icon: { theme in
|
||||
return optionsRateImage(rate: speedIconText, isLarge: false, color: theme.contextMenu.primaryColor)
|
||||
}, action: { [weak self] c, _ in
|
||||
guard let self else {
|
||||
c.dismiss(completion: nil)
|
||||
return
|
||||
}
|
||||
|
||||
c.pushItems(items: self.contextMenuSpeedItems(value: baseRatePromise) |> map { ContextController.Items(content: .list($0)) })
|
||||
})))
|
||||
items.append(.separator)
|
||||
}
|
||||
|
||||
if (component.slice.item.storyItem.isMy && channel.hasPermission(.postStories)) || channel.hasPermission(.editStories) {
|
||||
items.append(.action(ContextMenuActionItem(text: component.strings.Story_Context_Edit, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.contextMenu.primaryColor)
|
||||
@ -6427,10 +6572,10 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
}
|
||||
|
||||
let (tip, tipSignal) = self.getLinkedStickerPacks()
|
||||
return .single(ContextController.Items(id: 0, content: .list(items), tip: tip, tipSignal: tipSignal))
|
||||
})
|
||||
|
||||
let contextItems = ContextController.Items(content: .list(items), tip: tip, tipSignal: tipSignal)
|
||||
|
||||
let contextController = ContextController(presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceView: sourceView, position: .bottom)), items: .single(contextItems), gesture: gesture)
|
||||
let contextController = ContextController(presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceView: sourceView, position: .bottom)), items: contextItems, gesture: gesture)
|
||||
contextController.dismissed = { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
@ -6448,6 +6593,8 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
return
|
||||
}
|
||||
|
||||
let baseRatePromise = ValuePromise<Double>(component.storyItemSharedState.baseRate)
|
||||
|
||||
let translationSettings = component.context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.translationSettings])
|
||||
|> map { sharedData -> TranslationSettings in
|
||||
let translationSettings: TranslationSettings
|
||||
@ -6468,9 +6615,10 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
TelegramEngine.EngineData.Item.Peer.IsContact(id: component.slice.peer.id),
|
||||
TelegramEngine.EngineData.Item.Peer.Peer(id: component.context.account.peerId)
|
||||
),
|
||||
translationSettings
|
||||
translationSettings,
|
||||
baseRatePromise.get()
|
||||
)
|
||||
|> take(1)).startStandalone(next: { [weak self] result, translationSettings in
|
||||
|> take(1)).startStandalone(next: { [weak self] result, translationSettings, baseRate in
|
||||
guard let self, let component = self.component, let controller = component.controller() else {
|
||||
return
|
||||
}
|
||||
@ -6486,6 +6634,36 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme)
|
||||
var items: [ContextMenuItem] = []
|
||||
|
||||
if case .file = component.slice.item.storyItem.media {
|
||||
var speedValue: String = presentationData.strings.PlaybackSpeed_Normal
|
||||
var speedIconText: String = "1x"
|
||||
var didSetSpeedValue = false
|
||||
for (text, iconText, speed) in speedList(strings: presentationData.strings) {
|
||||
if abs(speed - baseRate) < 0.01 {
|
||||
speedValue = text
|
||||
speedIconText = iconText
|
||||
didSetSpeedValue = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !didSetSpeedValue && baseRate != 1.0 {
|
||||
speedValue = String(format: "%.1fx", baseRate)
|
||||
speedIconText = speedValue
|
||||
}
|
||||
|
||||
items.append(.action(ContextMenuActionItem(text: presentationData.strings.PlaybackSpeed_Title, textLayout: .secondLineWithValue(speedValue), icon: { theme in
|
||||
return optionsRateImage(rate: speedIconText, isLarge: false, color: theme.contextMenu.primaryColor)
|
||||
}, action: { [weak self] c, _ in
|
||||
guard let self else {
|
||||
c.dismiss(completion: nil)
|
||||
return
|
||||
}
|
||||
|
||||
c.pushItems(items: self.contextMenuSpeedItems(value: baseRatePromise) |> map { ContextController.Items(content: .list($0)) })
|
||||
})))
|
||||
items.append(.separator)
|
||||
}
|
||||
|
||||
let isMuted = resolvedAreStoriesMuted(globalSettings: globalSettings._asGlobalNotificationSettings(), peer: component.slice.peer._asPeer(), peerSettings: settings._asNotificationSettings(), topSearchPeers: topSearchPeers)
|
||||
|
||||
if !component.slice.peer.isService && isContact {
|
||||
@ -6939,3 +7117,49 @@ private func generateParabollicMotionKeyframes(from sourcePoint: CGPoint, to tar
|
||||
|
||||
return keyframes
|
||||
}
|
||||
|
||||
private func speedList(strings: PresentationStrings) -> [(String, String, Double)] {
|
||||
return [
|
||||
("0.5x", "0.5x", 0.5),
|
||||
(strings.PlaybackSpeed_Normal, "1x", 1.0),
|
||||
("1.5x", "1.5x", 1.5),
|
||||
("2x", "2x", 2.0)
|
||||
]
|
||||
}
|
||||
|
||||
private func optionsRateImage(rate: String, isLarge: Bool, color: UIColor = .white) -> UIImage? {
|
||||
return generateImage(isLarge ? CGSize(width: 30.0, height: 30.0) : CGSize(width: 24.0, height: 24.0), rotatedContext: { size, context in
|
||||
UIGraphicsPushContext(context)
|
||||
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
if let image = generateTintedImage(image: UIImage(bundleImageName: isLarge ? "Chat/Context Menu/Playspeed30" : "Chat/Context Menu/Playspeed24"), color: .white) {
|
||||
image.draw(at: CGPoint(x: 0.0, y: 0.0))
|
||||
}
|
||||
|
||||
let string = NSMutableAttributedString(string: rate, font: Font.with(size: isLarge ? 11.0 : 10.0, design: .round, weight: .semibold), textColor: color)
|
||||
|
||||
var offset = CGPoint(x: 1.0, y: 0.0)
|
||||
if rate.count >= 3 {
|
||||
if rate == "0.5x" {
|
||||
string.addAttribute(.kern, value: -0.8 as NSNumber, range: NSRange(string.string.startIndex ..< string.string.endIndex, in: string.string))
|
||||
offset.x += -0.5
|
||||
} else {
|
||||
string.addAttribute(.kern, value: -0.5 as NSNumber, range: NSRange(string.string.startIndex ..< string.string.endIndex, in: string.string))
|
||||
offset.x += -0.3
|
||||
}
|
||||
} else {
|
||||
offset.x += -0.3
|
||||
}
|
||||
|
||||
if !isLarge {
|
||||
offset.x *= 0.5
|
||||
offset.y *= 0.5
|
||||
}
|
||||
|
||||
let boundingRect = string.boundingRect(with: size, options: [], context: nil)
|
||||
string.draw(at: CGPoint(x: offset.x + floor((size.width - boundingRect.width) / 2.0), y: offset.y + floor((size.height - boundingRect.height) / 2.0)))
|
||||
|
||||
UIGraphicsPopContext()
|
||||
})
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user