mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-15 21:45:19 +00:00
Improve media playback rate controls
This commit is contained in:
parent
a235246c49
commit
8f181490e2
@ -8911,10 +8911,11 @@ Sorry for the inconvenience.";
|
||||
|
||||
"ChatList.EmptyChatListWithArchive" = "All of your chats are archived.";
|
||||
|
||||
"VoiceOver.Media.PlaybackRate05X" = "0.5X";
|
||||
"VoiceOver.Media.PlaybackRate125X" = "1.25X";
|
||||
"VoiceOver.Media.PlaybackRate15X" = "1.5X";
|
||||
"VoiceOver.Media.PlaybackRate175X" = "1.75X";
|
||||
"VoiceOver.Media.PlaybackRate2X" = "2X";
|
||||
|
||||
"Conversation.AudioRateTooltip15X" = "Audio will play at 1.5X speed.";
|
||||
"Conversation.AudioRateOptionsTooltip" = "Long tap for more speed values.";
|
||||
|
||||
"ImportStickerPack.EmojiCount_1" = "%@ Emoji";
|
||||
"ImportStickerPack.EmojiCount_any" = "%@ Emojis";
|
||||
|
||||
"ImportStickerPack.ImportingEmojis" = "Importing Emojis";
|
||||
"ImportStickerPack.CreateNewEmojiPack" = "Create a New Emoji Pack";
|
||||
|
@ -45,6 +45,7 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/TextNodeWithEntities:TextNodeWithEntities",
|
||||
"//submodules/TelegramUI/Components/AnimationCache:AnimationCache",
|
||||
"//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer",
|
||||
"//submodules/TelegramUI/Components/SliderContextItem:SliderContextItem",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -22,6 +22,7 @@ import TelegramUIPreferences
|
||||
import OpenInExternalAppUI
|
||||
import AVKit
|
||||
import TextFormat
|
||||
import SliderContextItem
|
||||
|
||||
public enum UniversalVideoGalleryItemContentInfo {
|
||||
case message(Message)
|
||||
@ -1267,17 +1268,11 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||
strongSelf.moreBarButtonRateTimestamp = CFAbsoluteTimeGetCurrent()
|
||||
|
||||
if abs(effectiveBaseRate - 1.0) > 0.01 {
|
||||
let rateString: String
|
||||
if abs(effectiveBaseRate - 0.5) < 0.01 {
|
||||
rateString = "0.5x"
|
||||
} else if abs(effectiveBaseRate - 1.5) < 0.01 {
|
||||
rateString = "1.5x"
|
||||
} else if abs(effectiveBaseRate - 2.0) < 0.01 {
|
||||
rateString = "2x"
|
||||
} else {
|
||||
rateString = "x"
|
||||
var stringValue = String(format: "%.1fx", effectiveBaseRate)
|
||||
if stringValue.hasSuffix(".0x") {
|
||||
stringValue = stringValue.replacingOccurrences(of: ".0x", with: "x")
|
||||
}
|
||||
strongSelf.moreBarButton.setContent(.image(optionsRateImage(rate: rateString, isLarge: true)), animated: animated)
|
||||
strongSelf.moreBarButton.setContent(.image(optionsRateImage(rate: stringValue, isLarge: true)), animated: animated)
|
||||
} else {
|
||||
strongSelf.moreBarButton.setContent(.more(optionsCircleImage(dark: false)), animated: animated)
|
||||
}
|
||||
@ -2428,15 +2423,19 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||
}
|
||||
|
||||
private func openMoreMenu(sourceNode: ASDisplayNode, gesture: ContextGesture?) {
|
||||
let items: Signal<[ContextMenuItem], NoError> = self.contextMenuMainItems()
|
||||
guard let controller = self.baseNavigationController()?.topViewController as? ViewController else {
|
||||
return
|
||||
}
|
||||
|
||||
var dismissImpl: (() -> Void)?
|
||||
let items: Signal<[ContextMenuItem], NoError> = self.contextMenuMainItems(dismiss: {
|
||||
dismissImpl?()
|
||||
})
|
||||
let contextController = ContextController(account: self.context.account, presentationData: self.presentationData.withUpdated(theme: defaultDarkColorPresentationTheme), source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceNode: self.moreBarButton.referenceNode)), items: items |> map { ContextController.Items(content: .list($0)) }, gesture: gesture)
|
||||
self.isShowingContextMenuPromise.set(true)
|
||||
controller.presentInGlobalOverlay(contextController)
|
||||
|
||||
dismissImpl = { [weak contextController] in
|
||||
contextController?.dismiss()
|
||||
}
|
||||
contextController.dismissed = { [weak self] in
|
||||
Queue.mainQueue().after(0.1, {
|
||||
self?.isShowingContextMenuPromise.set(false)
|
||||
@ -2455,7 +2454,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||
return speedList
|
||||
}
|
||||
|
||||
private func contextMenuMainItems() -> Signal<[ContextMenuItem], NoError> {
|
||||
private func contextMenuMainItems(dismiss: @escaping () -> Void) -> Signal<[ContextMenuItem], NoError> {
|
||||
guard let videoNode = self.videoNode else {
|
||||
return .single([])
|
||||
}
|
||||
@ -2470,6 +2469,29 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||
|
||||
var items: [ContextMenuItem] = []
|
||||
|
||||
var speedValue: String = strongSelf.presentationData.strings.PlaybackSpeed_Normal
|
||||
var speedIconText: String = "1x"
|
||||
for (text, iconText, speed) in strongSelf.speedList(strings: strongSelf.presentationData.strings) {
|
||||
if abs(speed - status.baseRate) < 0.01 {
|
||||
speedValue = text
|
||||
speedIconText = iconText
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.PlaybackSpeed_Title, textLayout: .secondLineWithValue(speedValue), icon: { theme in
|
||||
return optionsRateImage(rate: speedIconText, isLarge: false, color: theme.contextMenu.primaryColor)
|
||||
}, action: { c, _ in
|
||||
guard let strongSelf = self else {
|
||||
c.dismiss(completion: nil)
|
||||
return
|
||||
}
|
||||
|
||||
c.setItems(strongSelf.contextMenuSpeedItems(dismiss: dismiss) |> map { ContextController.Items(content: .list($0)) }, minHeight: nil)
|
||||
})))
|
||||
|
||||
items.append(.separator)
|
||||
|
||||
if let (message, _, _) = strongSelf.contentInfo() {
|
||||
let context = strongSelf.context
|
||||
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.SharedMedia_ViewInChat, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/GoToMessage"), color: theme.contextMenu.primaryColor)}, action: { [weak self] _, f in
|
||||
@ -2493,27 +2515,6 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||
})))
|
||||
}
|
||||
|
||||
var speedValue: String = strongSelf.presentationData.strings.PlaybackSpeed_Normal
|
||||
var speedIconText: String = "1x"
|
||||
for (text, iconText, speed) in strongSelf.speedList(strings: strongSelf.presentationData.strings) {
|
||||
if abs(speed - status.baseRate) < 0.01 {
|
||||
speedValue = text
|
||||
speedIconText = iconText
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.PlaybackSpeed_Title, textLayout: .secondLineWithValue(speedValue), icon: { theme in
|
||||
return optionsRateImage(rate: speedIconText, isLarge: false, color: theme.contextMenu.primaryColor)
|
||||
}, action: { c, _ in
|
||||
guard let strongSelf = self else {
|
||||
c.dismiss(completion: nil)
|
||||
return
|
||||
}
|
||||
|
||||
c.setItems(strongSelf.contextMenuSpeedItems() |> map { ContextController.Items(content: .list($0)) }, minHeight: nil)
|
||||
})))
|
||||
|
||||
// if #available(iOS 11.0, *) {
|
||||
// items.append(.action(ContextMenuActionItem(text: "AirPlay", textColor: .primary, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/AirPlay"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in
|
||||
// f(.default)
|
||||
@ -2593,7 +2594,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||
}
|
||||
}
|
||||
|
||||
private func contextMenuSpeedItems() -> Signal<[ContextMenuItem], NoError> {
|
||||
private func contextMenuSpeedItems(dismiss: @escaping () -> Void) -> Signal<[ContextMenuItem], NoError> {
|
||||
guard let videoNode = self.videoNode else {
|
||||
return .single([])
|
||||
}
|
||||
@ -2607,7 +2608,29 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||
}
|
||||
|
||||
var items: [ContextMenuItem] = []
|
||||
|
||||
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Common_Back, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.actionSheet.primaryTextColor)
|
||||
}, iconPosition: .left, action: { c, _ in
|
||||
guard let strongSelf = self else {
|
||||
c.dismiss(completion: nil)
|
||||
return
|
||||
}
|
||||
c.setItems(strongSelf.contextMenuMainItems(dismiss: dismiss) |> map { ContextController.Items(content: .list($0)) }, minHeight: nil)
|
||||
})))
|
||||
|
||||
items.append(.custom(SliderContextItem(minValue: 0.05, maxValue: 2.5, value: status.baseRate, valueChanged: { [weak self] newValue, finished in
|
||||
guard let strongSelf = self, let videoNode = strongSelf.videoNode else {
|
||||
return
|
||||
}
|
||||
videoNode.setBaseRate(newValue)
|
||||
if finished {
|
||||
dismiss()
|
||||
}
|
||||
}), true))
|
||||
|
||||
items.append(.separator)
|
||||
|
||||
for (text, _, rate) in strongSelf.speedList(strings: strongSelf.presentationData.strings) {
|
||||
let isSelected = abs(status.baseRate - rate) < 0.01
|
||||
items.append(.action(ContextMenuActionItem(text: text, icon: { theme in
|
||||
@ -2631,17 +2654,6 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||
})))
|
||||
}
|
||||
|
||||
items.append(.separator)
|
||||
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Common_Back, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.actionSheet.primaryTextColor)
|
||||
}, iconPosition: .left, action: { c, _ in
|
||||
guard let strongSelf = self else {
|
||||
c.dismiss(completion: nil)
|
||||
return
|
||||
}
|
||||
c.setItems(strongSelf.contextMenuMainItems() |> map { ContextController.Items(content: .list($0)) }, minHeight: nil)
|
||||
})))
|
||||
|
||||
return items
|
||||
}
|
||||
}
|
||||
|
@ -394,19 +394,39 @@ final class ImportStickerPackControllerNode: ViewControllerTracingNode, UIScroll
|
||||
forceTitleUpdate = true
|
||||
}
|
||||
|
||||
if let _ = self.stickerPack, self.currentItems.isEmpty || self.currentItems.count != self.pendingItems.count || self.pendingItems != self.currentItems || forceTitleUpdate {
|
||||
let itemsPerRow: Int
|
||||
if let stickerPack = self.stickerPack, case .emoji = stickerPack.type {
|
||||
itemsPerRow = 8
|
||||
} else {
|
||||
itemsPerRow = 4
|
||||
}
|
||||
if let stickerPack = self.stickerPack, self.currentItems.isEmpty || self.currentItems.count != self.pendingItems.count || self.pendingItems != self.currentItems || forceTitleUpdate {
|
||||
let previousItems = self.currentItems
|
||||
self.currentItems = self.pendingItems
|
||||
|
||||
let titleFont = Font.medium(20.0)
|
||||
let title: String
|
||||
if let _ = self.progress {
|
||||
title = self.presentationData.strings.ImportStickerPack_ImportingStickers
|
||||
if case .emoji = stickerPack.type {
|
||||
title = self.presentationData.strings.ImportStickerPack_ImportingEmojis
|
||||
} else {
|
||||
title = self.presentationData.strings.ImportStickerPack_ImportingStickers
|
||||
}
|
||||
} else {
|
||||
title = self.presentationData.strings.ImportStickerPack_StickerCount(Int32(self.currentItems.count))
|
||||
if case .emoji = stickerPack.type {
|
||||
title = self.presentationData.strings.ImportStickerPack_EmojiCount(Int32(self.currentItems.count))
|
||||
} else {
|
||||
title = self.presentationData.strings.ImportStickerPack_StickerCount(Int32(self.currentItems.count))
|
||||
}
|
||||
}
|
||||
self.contentTitleNode.attributedText = stringWithAppliedEntities(title, entities: [], baseColor: self.presentationData.theme.actionSheet.primaryTextColor, linkColor: self.presentationData.theme.actionSheet.controlAccentColor, baseFont: titleFont, linkFont: titleFont, boldFont: titleFont, italicFont: titleFont, boldItalicFont: titleFont, fixedFont: titleFont, blockQuoteFont: titleFont, message: nil)
|
||||
|
||||
if case .emoji = stickerPack.type {
|
||||
self.createActionButtonNode.setTitle(self.presentationData.strings.ImportStickerPack_CreateNewEmojiPack, with: Font.regular(20.0), with: self.presentationData.theme.actionSheet.controlAccentColor, for: .normal)
|
||||
} else {
|
||||
self.createActionButtonNode.setTitle(self.presentationData.strings.ImportStickerPack_CreateNewStickerSet, with: Font.regular(20.0), with: self.presentationData.theme.actionSheet.controlAccentColor, for: .normal)
|
||||
}
|
||||
|
||||
if !forceTitleUpdate {
|
||||
transaction = StickerPackPreviewGridTransaction(previousList: previousItems, list: self.currentItems, account: self.context.account, interaction: self.interaction, theme: self.presentationData.theme)
|
||||
}
|
||||
@ -422,7 +442,7 @@ final class ImportStickerPackControllerNode: ViewControllerTracingNode, UIScroll
|
||||
transition.updateFrame(node: self.contentTitleNode, frame: titleFrame)
|
||||
transition.updateFrame(node: self.contentSeparatorNode, frame: CGRect(origin: CGPoint(x: contentContainerFrame.minX, y: self.contentBackgroundNode.frame.minY + titleAreaHeight), size: CGSize(width: contentContainerFrame.size.width, height: UIScreenPixel)))
|
||||
|
||||
let itemsPerRow = 4
|
||||
|
||||
let itemWidth = floor(contentFrame.size.width / CGFloat(itemsPerRow))
|
||||
let rowCount = itemCount / itemsPerRow + (itemCount % itemsPerRow != 0 ? 1 : 0)
|
||||
|
||||
|
@ -18,7 +18,7 @@ public final class MediaPlaybackStoredState: Codable {
|
||||
let container = try decoder.container(keyedBy: StringCodingKey.self)
|
||||
|
||||
self.timestamp = try container.decode(Double.self, forKey: "timestamp")
|
||||
self.playbackRate = AudioPlaybackRate(rawValue: try container.decode(Int32.self, forKey: "playbackRate")) ?? .x1
|
||||
self.playbackRate = AudioPlaybackRate(rawValue: try container.decode(Int32.self, forKey: "playbackRate"))
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
|
@ -145,6 +145,9 @@ private final class SemanticStatusNodeIconContext: SemanticStatusNodeStateContex
|
||||
if let iconImage = self.iconImage {
|
||||
context.saveGState()
|
||||
let iconRect = CGRect(origin: CGPoint(), size: iconImage.size)
|
||||
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
|
||||
context.scaleBy(x: 1.0, y: -1.0)
|
||||
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
|
||||
context.clip(to: iconRect, mask: iconImage.cgImage!)
|
||||
context.fill(iconRect)
|
||||
context.restoreGState()
|
||||
@ -180,6 +183,9 @@ private final class SemanticStatusNodeIconContext: SemanticStatusNodeStateContex
|
||||
if let iconImage = self.iconImage {
|
||||
context.saveGState()
|
||||
let iconRect = CGRect(origin: CGPoint(), size: iconImage.size)
|
||||
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
|
||||
context.scaleBy(x: 1.0, y: -1.0)
|
||||
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
|
||||
context.clip(to: iconRect, mask: iconImage.cgImage!)
|
||||
context.fill(iconRect)
|
||||
context.restoreGState()
|
||||
|
@ -24,6 +24,9 @@ swift_library(
|
||||
"//submodules/Markdown:Markdown",
|
||||
"//submodules/TelegramCallsUI:TelegramCallsUI",
|
||||
"//submodules/ManagedAnimationNode:ManagedAnimationNode",
|
||||
"//submodules/TelegramNotices:TelegramNotices",
|
||||
"//submodules/TooltipUI:TooltipUI",
|
||||
"//submodules/TelegramUI/Components/SliderContextItem:SliderContextItem",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -11,6 +11,9 @@ import AccountContext
|
||||
import TelegramStringFormatting
|
||||
import ManagedAnimationNode
|
||||
import ContextUI
|
||||
import TelegramNotices
|
||||
import TooltipUI
|
||||
import SliderContextItem
|
||||
|
||||
private let titleFont = Font.regular(12.0)
|
||||
private let subtitleFont = Font.regular(10.0)
|
||||
@ -186,28 +189,9 @@ public final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, UIScrollVi
|
||||
}
|
||||
self.rateButton.accessibilityLabel = self.strings.VoiceOver_Media_PlaybackRate
|
||||
self.rateButton.accessibilityHint = self.strings.VoiceOver_Media_PlaybackRateChange
|
||||
switch playbackBaseRate {
|
||||
case .x0_5:
|
||||
self.rateButton.setContent(.image(optionsRateImage(rate: "0.5X", color: self.theme.rootController.navigationBar.accentTextColor)))
|
||||
self.rateButton.accessibilityValue = self.strings.VoiceOver_Media_PlaybackRate05X
|
||||
case .x1:
|
||||
self.rateButton.setContent(.image(optionsRateImage(rate: "1X", color: self.theme.rootController.navigationBar.controlColor)))
|
||||
self.rateButton.accessibilityValue = self.strings.VoiceOver_Media_PlaybackRateNormal
|
||||
case .x1_25:
|
||||
self.rateButton.setContent(.image(optionsRateImage(rate: "1.25X", color: self.theme.rootController.navigationBar.accentTextColor)))
|
||||
self.rateButton.accessibilityValue = self.strings.VoiceOver_Media_PlaybackRate125X
|
||||
case .x1_5:
|
||||
self.rateButton.setContent(.image(optionsRateImage(rate: "1.5X", color: self.theme.rootController.navigationBar.accentTextColor)))
|
||||
self.rateButton.accessibilityValue = self.strings.VoiceOver_Media_PlaybackRate15X
|
||||
case .x1_75:
|
||||
self.rateButton.setContent(.image(optionsRateImage(rate: "1.75X", color: self.theme.rootController.navigationBar.accentTextColor)))
|
||||
self.rateButton.accessibilityValue = self.strings.VoiceOver_Media_PlaybackRate175X
|
||||
case .x2:
|
||||
self.rateButton.setContent(.image(optionsRateImage(rate: "2X", color: self.theme.rootController.navigationBar.accentTextColor)))
|
||||
self.rateButton.accessibilityValue = self.strings.VoiceOver_Media_PlaybackRate2X
|
||||
default:
|
||||
break
|
||||
}
|
||||
self.rateButton.accessibilityValue = playbackBaseRate.stringValue
|
||||
|
||||
self.rateButton.setContent(.image(optionsRateImage(rate: playbackBaseRate.stringValue.uppercased(), color: self.theme.rootController.navigationBar.controlColor)))
|
||||
}
|
||||
}
|
||||
|
||||
@ -380,22 +364,7 @@ public final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, UIScrollVi
|
||||
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 {
|
||||
case .x0_5:
|
||||
self.rateButton.setContent(.image(optionsRateImage(rate: "0.5X", color: self.theme.rootController.navigationBar.accentTextColor)))
|
||||
case .x1:
|
||||
self.rateButton.setContent(.image(optionsRateImage(rate: "1X", color: self.theme.rootController.navigationBar.controlColor)))
|
||||
case .x1_25:
|
||||
self.rateButton.setContent(.image(optionsRateImage(rate: "1.25X", color: self.theme.rootController.navigationBar.controlColor)))
|
||||
case .x1_5:
|
||||
self.rateButton.setContent(.image(optionsRateImage(rate: "1.5X", color: self.theme.rootController.navigationBar.accentTextColor)))
|
||||
case .x1_75:
|
||||
self.rateButton.setContent(.image(optionsRateImage(rate: "1.75X", color: self.theme.rootController.navigationBar.controlColor)))
|
||||
case .x2:
|
||||
self.rateButton.setContent(.image(optionsRateImage(rate: "2X", color: self.theme.rootController.navigationBar.accentTextColor)))
|
||||
default:
|
||||
break
|
||||
}
|
||||
self.rateButton.setContent(.image(optionsRateImage(rate: playbackBaseRate.stringValue.uppercased(), color: self.theme.rootController.navigationBar.controlColor)))
|
||||
}
|
||||
if let (size, leftInset, rightInset) = self.validLayout {
|
||||
self.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset, transition: .immediate)
|
||||
@ -532,23 +501,41 @@ public final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, UIScrollVi
|
||||
nextRate = .x2
|
||||
}
|
||||
self.setRate?(nextRate, false)
|
||||
|
||||
let frame = self.rateButton.view.convert(self.rateButton.bounds, to: nil)
|
||||
|
||||
let _ = (ApplicationSpecificNotice.incrementAudioRateOptionsTip(accountManager: self.context.sharedContext.accountManager)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] value in
|
||||
if let strongSelf = self, let controller = strongSelf.getController?(), value == 4 {
|
||||
controller.present(TooltipScreen(account: strongSelf.context.account, text: strongSelf.strings.Conversation_AudioRateOptionsTooltip, style: .default, icon: nil, location: .point(frame.offsetBy(dx: 0.0, dy: 4.0), .bottom), displayDuration: .custom(3.0), inset: 3.0, shouldDismissOnTouch: { _ in
|
||||
return .dismiss(consume: false)
|
||||
}), in: .window(.root))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private func speedList(strings: PresentationStrings) -> [(String, String, AudioPlaybackRate)] {
|
||||
let speedList: [(String, String, AudioPlaybackRate)] = [
|
||||
("0.5x", "0.5x", .x0_5),
|
||||
(strings.PlaybackSpeed_Normal, "1x", .x1),
|
||||
("1.25x", "1.25x", .x1_25),
|
||||
("1.5x", "1.5x", .x1_5),
|
||||
("1.75x", "1.75x", .x1_75),
|
||||
("2x", "2x", .x2)
|
||||
]
|
||||
return speedList
|
||||
}
|
||||
|
||||
private func contextMenuSpeedItems() -> Signal<[ContextMenuItem], NoError> {
|
||||
private func contextMenuSpeedItems(dismiss: @escaping () -> Void) -> Signal<[ContextMenuItem], NoError> {
|
||||
var items: [ContextMenuItem] = []
|
||||
|
||||
items.append(.custom(SliderContextItem(minValue: 0.05, maxValue: 2.5, value: self.playbackBaseRate?.doubleValue ?? 1.0, valueChanged: { [weak self] newValue, finished in
|
||||
self?.setRate?(AudioPlaybackRate(newValue), true)
|
||||
if finished {
|
||||
dismiss()
|
||||
}
|
||||
}), true))
|
||||
|
||||
items.append(.separator)
|
||||
|
||||
for (text, _, rate) in self.speedList(strings: self.strings) {
|
||||
let isSelected = self.playbackBaseRate == rate
|
||||
items.append(.action(ContextMenuActionItem(text: text, icon: { theme in
|
||||
@ -571,9 +558,14 @@ public final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, UIScrollVi
|
||||
guard let controller = self.getController?() else {
|
||||
return
|
||||
}
|
||||
let items: Signal<[ContextMenuItem], NoError> = self.contextMenuSpeedItems()
|
||||
var dismissImpl: (() -> Void)?
|
||||
let items: Signal<[ContextMenuItem], NoError> = self.contextMenuSpeedItems(dismiss: {
|
||||
dismissImpl?()
|
||||
})
|
||||
let contextController = ContextController(account: self.context.account, presentationData: self.context.sharedContext.currentPresentationData.with { $0 }, source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceNode: self.rateButton.referenceNode, shouldBeDismissed: self.dismissedPromise.get())), items: items |> map { ContextController.Items(content: .list($0)) }, gesture: gesture)
|
||||
|
||||
dismissImpl = { [weak contextController] in
|
||||
contextController?.dismiss()
|
||||
}
|
||||
self.presentInGlobalOverlay?(contextController)
|
||||
}
|
||||
|
||||
@ -639,45 +631,38 @@ private final class PlayPauseIconNode: ManagedAnimationNode {
|
||||
}
|
||||
|
||||
private func optionsRateImage(rate: String, color: UIColor = .white) -> UIImage? {
|
||||
return generateImage(CGSize(width: 36.0, height: 16.0), rotatedContext: { size, context in
|
||||
let isLarge = "".isEmpty
|
||||
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))
|
||||
|
||||
let lineWidth = 1.0 + UIScreenPixel
|
||||
context.setLineWidth(lineWidth)
|
||||
context.setStrokeColor(color.cgColor)
|
||||
|
||||
if let image = generateTintedImage(image: UIImage(bundleImageName: isLarge ? "Chat/Context Menu/Playspeed30" : "Chat/Context Menu/Playspeed24"), color: color) {
|
||||
image.draw(at: CGPoint(x: 0.0, y: 0.0))
|
||||
}
|
||||
|
||||
let string = NSMutableAttributedString(string: rate, font: Font.with(size: 11.0, design: .round, weight: .bold), textColor: color)
|
||||
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)
|
||||
var width: CGFloat
|
||||
if rate.count >= 5 {
|
||||
string.addAttribute(.kern, value: -0.8 as NSNumber, range: NSRange(string.string.startIndex ..< string.string.endIndex, in: string.string))
|
||||
offset.x += -0.5
|
||||
width = 34.0
|
||||
} else if rate.count >= 3 {
|
||||
if rate == "0.5X" {
|
||||
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
|
||||
}
|
||||
width = 29.0
|
||||
} else {
|
||||
string.addAttribute(.kern, value: -0.5 as NSNumber, range: NSRange(string.string.startIndex ..< string.string.endIndex, in: string.string))
|
||||
width = 19.0
|
||||
offset.x += -0.3
|
||||
}
|
||||
|
||||
let path = UIBezierPath(roundedRect: CGRect(x: floorToScreenPixels((size.width - width) / 2.0), y: 0.0, width: width, height: 16.0).insetBy(dx: lineWidth / 2.0, dy: lineWidth / 2.0), byRoundingCorners: .allCorners, cornerRadii: CGSize(width: 2.0, height: 2.0))
|
||||
context.addPath(path.cgPath)
|
||||
context.strokePath()
|
||||
|
||||
|
||||
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 + UIScreenPixel + floor((size.height - boundingRect.height) / 2.0)))
|
||||
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()
|
||||
})
|
||||
|
22
submodules/TelegramUI/Components/SliderContextItem/BUILD
Normal file
22
submodules/TelegramUI/Components/SliderContextItem/BUILD
Normal file
@ -0,0 +1,22 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "SliderContextItem",
|
||||
module_name = "SliderContextItem",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
|
||||
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
|
||||
"//submodules/Display:Display",
|
||||
"//submodules/ContextUI:ContextUI",
|
||||
"//submodules/TelegramPresentationData:TelegramPresentationData",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
@ -0,0 +1,184 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import ContextUI
|
||||
import TelegramPresentationData
|
||||
|
||||
public final class SliderContextItem: ContextMenuCustomItem {
|
||||
private let minValue: CGFloat
|
||||
private let maxValue: CGFloat
|
||||
private let value: CGFloat
|
||||
private let valueChanged: (CGFloat, Bool) -> Void
|
||||
|
||||
public init(minValue: CGFloat, maxValue: CGFloat, value: CGFloat, valueChanged: @escaping (CGFloat, Bool) -> Void) {
|
||||
self.minValue = minValue
|
||||
self.maxValue = maxValue
|
||||
self.value = value
|
||||
self.valueChanged = valueChanged
|
||||
}
|
||||
|
||||
public func node(presentationData: PresentationData, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) -> ContextMenuCustomNode {
|
||||
return SliderContextItemNode(presentationData: presentationData, getController: getController, minValue: self.minValue, maxValue: self.maxValue, value: self.value, valueChanged: self.valueChanged)
|
||||
}
|
||||
}
|
||||
|
||||
private let textFont = Font.regular(17.0)
|
||||
|
||||
private final class SliderContextItemNode: ASDisplayNode, ContextMenuCustomNode {
|
||||
private var presentationData: PresentationData
|
||||
|
||||
private let backgroundTextNode: ImmediateTextNode
|
||||
|
||||
private let foregroundNode: ASDisplayNode
|
||||
private let foregroundTextNode: ImmediateTextNode
|
||||
|
||||
let minValue: CGFloat
|
||||
let maxValue: CGFloat
|
||||
var value: CGFloat = 1.0 {
|
||||
didSet {
|
||||
self.updateValue()
|
||||
}
|
||||
}
|
||||
|
||||
private let valueChanged: (CGFloat, Bool) -> Void
|
||||
|
||||
private let hapticFeedback = HapticFeedback()
|
||||
|
||||
init(presentationData: PresentationData, getController: @escaping () -> ContextControllerProtocol?, minValue: CGFloat, maxValue: CGFloat, value: CGFloat, valueChanged: @escaping (CGFloat, Bool) -> Void) {
|
||||
self.presentationData = presentationData
|
||||
self.minValue = minValue
|
||||
self.maxValue = maxValue
|
||||
self.value = value
|
||||
self.valueChanged = valueChanged
|
||||
|
||||
self.backgroundTextNode = ImmediateTextNode()
|
||||
self.backgroundTextNode.isAccessibilityElement = false
|
||||
self.backgroundTextNode.isUserInteractionEnabled = false
|
||||
self.backgroundTextNode.displaysAsynchronously = false
|
||||
self.backgroundTextNode.textAlignment = .left
|
||||
|
||||
self.foregroundNode = ASDisplayNode()
|
||||
self.foregroundNode.clipsToBounds = true
|
||||
self.foregroundNode.isAccessibilityElement = false
|
||||
self.foregroundNode.backgroundColor = UIColor(rgb: 0xffffff)
|
||||
self.foregroundNode.isUserInteractionEnabled = false
|
||||
|
||||
self.foregroundTextNode = ImmediateTextNode()
|
||||
self.foregroundTextNode.isAccessibilityElement = false
|
||||
self.foregroundTextNode.isUserInteractionEnabled = false
|
||||
self.foregroundTextNode.displaysAsynchronously = false
|
||||
self.foregroundTextNode.textAlignment = .left
|
||||
|
||||
super.init()
|
||||
|
||||
self.isUserInteractionEnabled = true
|
||||
|
||||
self.addSubnode(self.backgroundTextNode)
|
||||
self.addSubnode(self.foregroundNode)
|
||||
self.foregroundNode.addSubnode(self.foregroundTextNode)
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:)))
|
||||
self.view.addGestureRecognizer(panGestureRecognizer)
|
||||
|
||||
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))
|
||||
self.view.addGestureRecognizer(tapGestureRecognizer)
|
||||
}
|
||||
|
||||
func updateTheme(presentationData: PresentationData) {
|
||||
self.presentationData = presentationData
|
||||
self.updateValue()
|
||||
}
|
||||
|
||||
private func updateValue(transition: ContainedViewLayoutTransition = .immediate) {
|
||||
let width = self.frame.width
|
||||
|
||||
let range = self.maxValue - self.minValue
|
||||
let value = (self.value - self.minValue) / range
|
||||
transition.updateFrameAdditive(node: self.foregroundNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: value * width, height: self.frame.height)))
|
||||
|
||||
var stringValue = String(format: "%.1fx", self.value)
|
||||
if stringValue.hasSuffix(".0x") {
|
||||
stringValue = stringValue.replacingOccurrences(of: ".0x", with: "x")
|
||||
}
|
||||
self.backgroundTextNode.attributedText = NSAttributedString(string: stringValue, font: textFont, textColor: UIColor(rgb: 0xffffff))
|
||||
self.foregroundTextNode.attributedText = NSAttributedString(string: stringValue, font: textFont, textColor: UIColor(rgb: 0x000000))
|
||||
|
||||
let _ = self.backgroundTextNode.updateLayout(CGSize(width: 70.0, height: .greatestFiniteMagnitude))
|
||||
let _ = self.foregroundTextNode.updateLayout(CGSize(width: 70.0, height: .greatestFiniteMagnitude))
|
||||
}
|
||||
|
||||
func updateLayout(constrainedWidth: CGFloat, constrainedHeight: CGFloat) -> (CGSize, (CGSize, ContainedViewLayoutTransition) -> Void) {
|
||||
let valueWidth: CGFloat = 70.0
|
||||
let height: CGFloat = 45.0
|
||||
|
||||
var textSize = self.backgroundTextNode.updateLayout(CGSize(width: valueWidth, height: .greatestFiniteMagnitude))
|
||||
textSize.width = valueWidth
|
||||
|
||||
return (CGSize(width: height * 3.0, height: height), { size, transition in
|
||||
let leftInset: CGFloat = 17.0
|
||||
|
||||
let textFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((size.height - textSize.height) / 2.0)), size: textSize)
|
||||
transition.updateFrameAdditive(node: self.backgroundTextNode, frame: textFrame)
|
||||
transition.updateFrameAdditive(node: self.foregroundTextNode, frame: textFrame)
|
||||
|
||||
self.updateValue(transition: transition)
|
||||
})
|
||||
}
|
||||
|
||||
@objc private func panGesture(_ gestureRecognizer: UIPanGestureRecognizer) {
|
||||
let range = self.maxValue - self.minValue
|
||||
switch gestureRecognizer.state {
|
||||
case .began:
|
||||
break
|
||||
case .changed:
|
||||
let previousValue = self.value
|
||||
|
||||
let translation: CGFloat = gestureRecognizer.translation(in: gestureRecognizer.view).x
|
||||
let delta = translation / self.bounds.width * range
|
||||
self.value = max(self.minValue, min(self.maxValue, self.value + delta))
|
||||
gestureRecognizer.setTranslation(CGPoint(), in: gestureRecognizer.view)
|
||||
|
||||
if self.value == 2.0 && previousValue != 2.0 {
|
||||
self.hapticFeedback.impact(.soft)
|
||||
} else if self.value == 1.0 && previousValue != 1.0 {
|
||||
self.hapticFeedback.impact(.soft)
|
||||
} else if self.value == 2.5 && previousValue != 2.5 {
|
||||
self.hapticFeedback.impact(.soft)
|
||||
} else if self.value == 0.05 && previousValue != 0.05 {
|
||||
self.hapticFeedback.impact(.soft)
|
||||
}
|
||||
if abs(previousValue - self.value) >= 0.001 {
|
||||
self.valueChanged(self.value, false)
|
||||
}
|
||||
case .ended:
|
||||
let translation: CGFloat = gestureRecognizer.translation(in: gestureRecognizer.view).x
|
||||
let delta = translation / self.bounds.width * range
|
||||
self.value = max(self.minValue, min(self.maxValue, self.value + delta))
|
||||
self.valueChanged(self.value, true)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func tapGesture(_ gestureRecognizer: UITapGestureRecognizer) {
|
||||
let range = self.maxValue - self.minValue
|
||||
let location = gestureRecognizer.location(in: gestureRecognizer.view)
|
||||
self.value = max(self.minValue, min(self.maxValue, location.x / range))
|
||||
self.valueChanged(self.value, true)
|
||||
}
|
||||
|
||||
func canBeHighlighted() -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func updateIsHighlighted(isHighlighted: Bool) {
|
||||
}
|
||||
|
||||
func performAction() {
|
||||
}
|
||||
}
|
@ -422,11 +422,7 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
|
||||
let baseRate: AudioPlaybackRate
|
||||
if value.status.baseRate.isEqual(to: 2.0) {
|
||||
baseRate = .x2
|
||||
} else if value.status.baseRate.isEqual(to: 1.75) {
|
||||
baseRate = .x1_75
|
||||
} else if value.status.baseRate.isEqual(to: 1.5) {
|
||||
baseRate = .x1_25
|
||||
} else if value.status.baseRate.isEqual(to: 1.25) {
|
||||
baseRate = .x1_5
|
||||
} else if value.status.baseRate.isEqual(to: 0.5) {
|
||||
baseRate = .x0_5
|
||||
@ -794,12 +790,8 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
|
||||
switch baseRate {
|
||||
case .x2:
|
||||
self.rateButton.setImage(optionsRateImage(rate: "2X", color: self.presentationData.theme.list.itemAccentColor), for: [])
|
||||
case .x1_75:
|
||||
self.rateButton.setImage(optionsRateImage(rate: "1.75X", color: self.presentationData.theme.list.itemAccentColor), for: [])
|
||||
case .x1_5:
|
||||
self.rateButton.setImage(optionsRateImage(rate: "1.5X", color: self.presentationData.theme.list.itemAccentColor), for: [])
|
||||
case .x1_25:
|
||||
self.rateButton.setImage(optionsRateImage(rate: "1.25X", color: self.presentationData.theme.list.itemAccentColor), for: [])
|
||||
case .x0_5:
|
||||
self.rateButton.setImage(optionsRateImage(rate: "0.5X", color: self.presentationData.theme.list.itemAccentColor), for: [])
|
||||
default:
|
||||
|
@ -15,28 +15,73 @@ public enum MusicPlaybackSettingsLooping: Int32 {
|
||||
case all = 2
|
||||
}
|
||||
|
||||
public enum AudioPlaybackRate: Int32 {
|
||||
case x0_5 = 500
|
||||
case x1 = 1000
|
||||
case x1_25 = 1250
|
||||
case x1_5 = 1500
|
||||
case x1_75 = 1750
|
||||
case x2 = 2000
|
||||
case x4 = 4000
|
||||
case x8 = 8000
|
||||
case x16 = 16000
|
||||
public enum AudioPlaybackRate: Equatable {
|
||||
case x0_5
|
||||
case x1
|
||||
case x1_5
|
||||
case x2
|
||||
case x4
|
||||
case x8
|
||||
case x16
|
||||
case custom(Int32)
|
||||
|
||||
public var doubleValue: Double {
|
||||
return Double(self.rawValue) / 1000.0
|
||||
}
|
||||
|
||||
public var rawValue: Int32 {
|
||||
switch self {
|
||||
case .x0_5:
|
||||
return 500
|
||||
case .x1:
|
||||
return 1000
|
||||
case .x1_5:
|
||||
return 1500
|
||||
case .x2:
|
||||
return 2000
|
||||
case .x4:
|
||||
return 4000
|
||||
case .x8:
|
||||
return 8000
|
||||
case .x16:
|
||||
return 16000
|
||||
case let .custom(value):
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
public init(_ value: Double) {
|
||||
if let resolved = AudioPlaybackRate(rawValue: Int32(value * 1000.0)) {
|
||||
self = resolved
|
||||
} else {
|
||||
self.init(rawValue: Int32(value * 1000.0))
|
||||
}
|
||||
|
||||
public init(rawValue: Int32) {
|
||||
switch rawValue {
|
||||
case 500:
|
||||
self = .x0_5
|
||||
case 1000:
|
||||
self = .x1
|
||||
case 1500:
|
||||
self = .x1_5
|
||||
case 2000:
|
||||
self = .x2
|
||||
case 4000:
|
||||
self = .x4
|
||||
case 8000:
|
||||
self = .x8
|
||||
case 16000:
|
||||
self = .x16
|
||||
default:
|
||||
self = .custom(rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
public var stringValue: String {
|
||||
var stringValue = String(format: "%.1fx", self.doubleValue)
|
||||
if stringValue.hasSuffix(".0x") {
|
||||
stringValue = stringValue.replacingOccurrences(of: ".0x", with: "x")
|
||||
}
|
||||
return stringValue
|
||||
}
|
||||
}
|
||||
|
||||
public struct MusicPlaybackSettings: Codable, Equatable {
|
||||
@ -59,7 +104,7 @@ public struct MusicPlaybackSettings: Codable, Equatable {
|
||||
|
||||
self.order = MusicPlaybackSettingsOrder(rawValue: try container.decode(Int32.self, forKey: "order")) ?? .regular
|
||||
self.looping = MusicPlaybackSettingsLooping(rawValue: try container.decode(Int32.self, forKey: "looping")) ?? .none
|
||||
self.voicePlaybackRate = AudioPlaybackRate(rawValue: try container.decodeIfPresent(Int32.self, forKey: "voicePlaybackRate") ?? AudioPlaybackRate.x1.rawValue) ?? .x1
|
||||
self.voicePlaybackRate = AudioPlaybackRate(rawValue: try container.decodeIfPresent(Int32.self, forKey: "voicePlaybackRate") ?? AudioPlaybackRate.x1.rawValue)
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
|
Loading…
x
Reference in New Issue
Block a user