Merge branch 'master' into experiments/string-generation

This commit is contained in:
Ali 2021-07-21 18:49:41 +02:00
commit a836fe0442
292 changed files with 28648 additions and 15813 deletions

View File

@ -1693,6 +1693,8 @@ plist_fragment(
<key>UIAppFonts</key>
<array>
<string>SFCompactRounded-Semibold.otf</string>
<string>AremacFS-Regular.otf</string>
<string>AremacFS-Semibold.otf</string>
</array>
<key>UIBackgroundModes</key>
<array>

View File

@ -6597,3 +6597,8 @@ Sorry for the inconvenience.";
"TwoFactorRemember.Done.Title" = "Perfect!";
"TwoFactorRemember.Done.Text" = "You still remember your password.";
"TwoFactorRemember.Done.Action" = "Back to Settings";
"VoiceChat.VideoPreviewFrontCamera" = "Front Camera";
"VoiceChat.VideoPreviewBackCamera" = "Back Camera";
"VoiceChat.VideoPreviewContinue" = "Continue";
"VoiceChat.VideoPreviewShareScreenInfo" = "Everything on your screen, including notifications, will be shared.";

View File

@ -18,10 +18,10 @@ public final class GalleryControllerActionInteraction {
public let openHashtag: (String?, String) -> Void
public let openBotCommand: (String) -> Void
public let addContact: (String) -> Void
public let storeMediaPlaybackState: (MessageId, Double?) -> Void
public let storeMediaPlaybackState: (MessageId, Double?, Double) -> Void
public let editMedia: (MessageId, [UIView], @escaping () -> Void) -> Void
public init(openUrl: @escaping (String, Bool) -> Void, openUrlIn: @escaping (String) -> Void, openPeerMention: @escaping (String) -> Void, openPeer: @escaping (PeerId) -> Void, openHashtag: @escaping (String?, String) -> Void, openBotCommand: @escaping (String) -> Void, addContact: @escaping (String) -> Void, storeMediaPlaybackState: @escaping (MessageId, Double?) -> Void, editMedia: @escaping (MessageId, [UIView], @escaping () -> Void) -> Void) {
public init(openUrl: @escaping (String, Bool) -> Void, openUrlIn: @escaping (String) -> Void, openPeerMention: @escaping (String) -> Void, openPeer: @escaping (PeerId) -> Void, openHashtag: @escaping (String?, String) -> Void, openBotCommand: @escaping (String) -> Void, addContact: @escaping (String) -> Void, storeMediaPlaybackState: @escaping (MessageId, Double?, Double) -> Void, editMedia: @escaping (MessageId, [UIView], @escaping () -> Void) -> Void) {
self.openUrl = openUrl
self.openUrlIn = openUrlIn
self.openPeerMention = openPeerMention

View File

@ -347,7 +347,7 @@ func chatListNodeEntriesForView(_ view: ChatListView, state: ChatListNodeState,
}
}
result.append(.PeerEntry(index: ChatListIndex.absoluteUpperBound.predecessor, presentationData: state.presentationData, messages: [], readState: nil, isRemovedFromTotalUnreadCount: false, embeddedInterfaceState: nil, peer: RenderedPeer(peerId: savedMessagesPeer.id, peers: SimpleDictionary([savedMessagesPeer.id: savedMessagesPeer])), presence: nil, summaryInfo: ChatListMessageTagSummaryInfo(), editing: state.editing, hasActiveRevealControls: false, selected: false, inputActivities: nil, promoInfo: nil, hasFailedMessages: false, isContact: false))
result.append(.PeerEntry(index: ChatListIndex.absoluteUpperBound.predecessor, presentationData: state.presentationData, messages: [], readState: nil, isRemovedFromTotalUnreadCount: false, embeddedInterfaceState: nil, peer: RenderedPeer(peerId: savedMessagesPeer.id, peers: SimpleDictionary([savedMessagesPeer.id: savedMessagesPeer])), presence: nil, summaryInfo: ChatListMessageTagSummaryInfo(), editing: state.editing, hasActiveRevealControls: false, selected: state.selectedPeerIds.contains(savedMessagesPeer.id), inputActivities: nil, promoInfo: nil, hasFailedMessages: false, isContact: false))
} else {
if !filteredAdditionalItemEntries.isEmpty {
for item in filteredAdditionalItemEntries.reversed() {

View File

@ -239,7 +239,7 @@ open class NavigationBar: ASDisplayNode {
var presentationData: NavigationBarPresentationData
private var validLayout: (size: CGSize, defaultHeight: CGFloat, additionalTopHeight: CGFloat, additionalContentHeight: CGFloat, additionalBackgroundHeight: CGFloat, leftInset: CGFloat, rightInset: CGFloat, appearsHidden: Bool)?
private var validLayout: (size: CGSize, defaultHeight: CGFloat, additionalTopHeight: CGFloat, additionalContentHeight: CGFloat, additionalBackgroundHeight: CGFloat, leftInset: CGFloat, rightInset: CGFloat, appearsHidden: Bool, isLandscape: Bool)?
private var requestedLayout: Bool = false
var requestContainerLayout: (ContainedViewLayoutTransition) -> Void = { _ in }
@ -940,16 +940,16 @@ open class NavigationBar: ASDisplayNode {
if let validLayout = self.validLayout, self.requestedLayout {
self.requestedLayout = false
self.updateLayout(size: validLayout.size, defaultHeight: validLayout.defaultHeight, additionalTopHeight: validLayout.additionalTopHeight, additionalContentHeight: validLayout.additionalContentHeight, additionalBackgroundHeight: validLayout.additionalBackgroundHeight, leftInset: validLayout.leftInset, rightInset: validLayout.rightInset, appearsHidden: validLayout.appearsHidden, transition: .immediate)
self.updateLayout(size: validLayout.size, defaultHeight: validLayout.defaultHeight, additionalTopHeight: validLayout.additionalTopHeight, additionalContentHeight: validLayout.additionalContentHeight, additionalBackgroundHeight: validLayout.additionalBackgroundHeight, leftInset: validLayout.leftInset, rightInset: validLayout.rightInset, appearsHidden: validLayout.appearsHidden, isLandscape: validLayout.isLandscape, transition: .immediate)
}
}
func updateLayout(size: CGSize, defaultHeight: CGFloat, additionalTopHeight: CGFloat, additionalContentHeight: CGFloat, additionalBackgroundHeight: CGFloat, leftInset: CGFloat, rightInset: CGFloat, appearsHidden: Bool, transition: ContainedViewLayoutTransition) {
func updateLayout(size: CGSize, defaultHeight: CGFloat, additionalTopHeight: CGFloat, additionalContentHeight: CGFloat, additionalBackgroundHeight: CGFloat, leftInset: CGFloat, rightInset: CGFloat, appearsHidden: Bool, isLandscape: Bool, transition: ContainedViewLayoutTransition) {
if self.layoutSuspended {
return
}
self.validLayout = (size, defaultHeight, additionalTopHeight, additionalContentHeight, additionalBackgroundHeight, leftInset, rightInset, appearsHidden)
self.validLayout = (size, defaultHeight, additionalTopHeight, additionalContentHeight, additionalBackgroundHeight, leftInset, rightInset, appearsHidden, isLandscape)
let backgroundFrame = CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height + additionalBackgroundHeight))
if self.backgroundNode.frame != backgroundFrame {
@ -995,7 +995,7 @@ open class NavigationBar: ASDisplayNode {
var leftTitleInset: CGFloat = leftInset + 1.0
var rightTitleInset: CGFloat = rightInset + 1.0
if self.backButtonNode.supernode != nil {
let backButtonSize = self.backButtonNode.updateLayout(constrainedSize: CGSize(width: size.width, height: nominalHeight))
let backButtonSize = self.backButtonNode.updateLayout(constrainedSize: CGSize(width: size.width, height: nominalHeight), isLandscape: isLandscape)
leftTitleInset += backButtonSize.width + backButtonInset + 1.0
let topHitTestSlop = (nominalHeight - backButtonSize.height) * 0.5
@ -1047,7 +1047,7 @@ open class NavigationBar: ASDisplayNode {
self.badgeNode.alpha = 1.0
}
} else if self.leftButtonNode.supernode != nil {
let leftButtonSize = self.leftButtonNode.updateLayout(constrainedSize: CGSize(width: size.width, height: nominalHeight))
let leftButtonSize = self.leftButtonNode.updateLayout(constrainedSize: CGSize(width: size.width, height: nominalHeight), isLandscape: isLandscape)
leftTitleInset += leftButtonSize.width + leftButtonInset + 1.0
self.leftButtonNode.alpha = 1.0
@ -1059,7 +1059,7 @@ open class NavigationBar: ASDisplayNode {
transition.updateFrame(node: self.badgeNode, frame: CGRect(origin: backButtonArrowFrame.origin.offsetBy(dx: 7.0, dy: -9.0), size: badgeSize))
if self.rightButtonNode.supernode != nil {
let rightButtonSize = self.rightButtonNode.updateLayout(constrainedSize: (CGSize(width: size.width, height: nominalHeight)))
let rightButtonSize = self.rightButtonNode.updateLayout(constrainedSize: (CGSize(width: size.width, height: nominalHeight)), isLandscape: isLandscape)
rightTitleInset += rightButtonSize.width + leftButtonInset + 1.0
self.rightButtonNode.alpha = 1.0
transition.updateFrame(node: self.rightButtonNode, frame: CGRect(origin: CGPoint(x: size.width - leftButtonInset - rightButtonSize.width, y: contentVerticalOrigin + floor((nominalHeight - rightButtonSize.height) / 2.0)), size: rightButtonSize))
@ -1073,7 +1073,7 @@ open class NavigationBar: ASDisplayNode {
break
case .bottom:
if let transitionBackButtonNode = self.transitionBackButtonNode {
let transitionBackButtonSize = transitionBackButtonNode.updateLayout(constrainedSize: CGSize(width: size.width, height: nominalHeight))
let transitionBackButtonSize = transitionBackButtonNode.updateLayout(constrainedSize: CGSize(width: size.width, height: nominalHeight), isLandscape: isLandscape)
let initialX: CGFloat = backButtonInset + size.width * 0.3
let finalX: CGFloat = floor((size.width - transitionBackButtonSize.width) / 2.0)
@ -1204,7 +1204,7 @@ open class NavigationBar: ASDisplayNode {
node.updateManualText(self.backButtonNode.manualText)
node.color = accentColor
if let validLayout = self.validLayout {
let _ = node.updateLayout(constrainedSize: CGSize(width: validLayout.size.width, height: validLayout.defaultHeight))
let _ = node.updateLayout(constrainedSize: CGSize(width: validLayout.size.width, height: validLayout.defaultHeight), isLandscape: validLayout.isLandscape)
node.frame = self.backButtonNode.frame
}
return node
@ -1227,7 +1227,7 @@ open class NavigationBar: ASDisplayNode {
node.updateItems(items)
node.color = accentColor
if let validLayout = self.validLayout {
let _ = node.updateLayout(constrainedSize: CGSize(width: validLayout.size.width, height: validLayout.defaultHeight))
let _ = node.updateLayout(constrainedSize: CGSize(width: validLayout.size.width, height: validLayout.defaultHeight), isLandscape: validLayout.isLandscape)
node.frame = self.backButtonNode.frame
}
return node

View File

@ -255,8 +255,6 @@ private final class NavigationButtonItemNode: ImmediateTextNode {
public override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesMoved(touches, with: event)
//self.updateHighlightedState(self.touchInsideApparentBounds(touches.first!), animated: true)
}
public override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
@ -275,6 +273,15 @@ private final class NavigationButtonItemNode: ImmediateTextNode {
}
}
public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if let node = self.node as? HighlightableButtonNode {
let result = node.view.hitTest(self.view.convert(point, to: node.view), with: event)
return result
} else {
return super.hitTest(point, with: event)
}
}
public override func touchesCancelled(_ touches: Set<UITouch>?, with event: UIEvent?) {
super.touchesCancelled(touches, with: event)
@ -453,7 +460,7 @@ public final class NavigationButtonNode: ASDisplayNode {
}
}
public func updateLayout(constrainedSize: CGSize) -> CGSize {
public func updateLayout(constrainedSize: CGSize, isLandscape: Bool) -> CGSize {
var nodeOrigin = CGPoint()
var totalSize = CGSize()
for node in self.nodes {
@ -468,6 +475,9 @@ public final class NavigationButtonNode: ASDisplayNode {
totalSize.height = max(totalSize.height, nodeSize.height)
node.frame = CGRect(origin: CGPoint(x: nodeOrigin.x, y: floor((totalSize.height - nodeSize.height) / 2.0)), size: nodeSize)
nodeOrigin.x += node.bounds.width
if isLandscape {
nodeOrigin.x += 16.0
}
}
return totalSize
}

View File

@ -382,6 +382,8 @@ public enum TabBarItemContextActionType {
self.navigationBarOrigin = navigationBarFrame.origin.y
let isLandscape = layout.size.width > layout.size.height
if let navigationBar = self.navigationBar {
if let contentNode = navigationBar.contentNode, case .expansion = contentNode.mode, !self.displayNavigationBar {
navigationBarFrame.origin.y -= navigationLayout.defaultContentHeight
@ -392,7 +394,7 @@ public enum TabBarItemContextActionType {
navigationBarFrame.size.height += NavigationBar.defaultSecondaryContentHeight
//navigationBarFrame.origin.y += NavigationBar.defaultSecondaryContentHeight
}
navigationBar.updateLayout(size: navigationBarFrame.size, defaultHeight: navigationLayout.defaultContentHeight, additionalTopHeight: statusBarHeight, additionalContentHeight: self.additionalNavigationBarHeight, additionalBackgroundHeight: additionalBackgroundHeight, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, appearsHidden: !self.displayNavigationBar, transition: transition)
navigationBar.updateLayout(size: navigationBarFrame.size, defaultHeight: navigationLayout.defaultContentHeight, additionalTopHeight: statusBarHeight, additionalContentHeight: self.additionalNavigationBarHeight, additionalBackgroundHeight: additionalBackgroundHeight, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, appearsHidden: !self.displayNavigationBar, isLandscape: isLandscape, transition: transition)
if !transition.isAnimated {
navigationBar.layer.cancelAnimationsRecursive(key: "bounds")
navigationBar.layer.cancelAnimationsRecursive(key: "position")

View File

@ -6,11 +6,16 @@
#import "libswresample/swresample.h"
@interface FFMpegSWResample () {
int _sourceChannelCount;
int _sourceSampleRate;
FFMpegAVSampleFormat _sourceSampleFormat;
int _destinationChannelCount;
int _destinationSampleRate;
FFMpegAVSampleFormat _destinationSampleFormat;
int _currentSourceChannelCount;
SwrContext *_context;
NSUInteger _ratio;
NSInteger _destinationChannelCount;
enum FFMpegAVSampleFormat _destinationSampleFormat;
void *_buffer;
int _bufferSize;
}
@ -22,36 +27,57 @@
- (instancetype)initWithSourceChannelCount:(NSInteger)sourceChannelCount sourceSampleRate:(NSInteger)sourceSampleRate sourceSampleFormat:(enum FFMpegAVSampleFormat)sourceSampleFormat destinationChannelCount:(NSInteger)destinationChannelCount destinationSampleRate:(NSInteger)destinationSampleRate destinationSampleFormat:(enum FFMpegAVSampleFormat)destinationSampleFormat {
self = [super init];
if (self != nil) {
_sourceChannelCount = sourceChannelCount;
_destinationChannelCount = destinationChannelCount;
_sourceSampleRate = (int)sourceSampleRate;
_sourceSampleFormat = sourceSampleFormat;
_destinationChannelCount = (int)destinationChannelCount;
_destinationSampleRate = (int)destinationSampleRate;
_destinationSampleFormat = destinationSampleFormat;
_context = swr_alloc_set_opts(NULL,
av_get_default_channel_layout((int)destinationChannelCount),
(enum AVSampleFormat)destinationSampleFormat,
(int)destinationSampleRate,
av_get_default_channel_layout((int)sourceChannelCount),
(enum AVSampleFormat)sourceSampleFormat,
(int)sourceSampleRate,
0,
NULL);
_ratio = MAX(1, destinationSampleRate / MAX(sourceSampleRate, 1)) * MAX(1, destinationChannelCount / sourceChannelCount) * 2;
swr_init(_context);
_currentSourceChannelCount = -1;
}
return self;
}
- (void)dealloc {
swr_free(&_context);
if (_context) {
swr_free(&_context);
}
if (_buffer) {
free(_buffer);
}
}
- (void)resetContextForChannelCount:(int)channelCount {
if (_context) {
swr_free(&_context);
_context = NULL;
}
_context = swr_alloc_set_opts(NULL,
av_get_default_channel_layout((int)_destinationChannelCount),
(enum AVSampleFormat)_destinationSampleFormat,
(int)_destinationSampleRate,
av_get_default_channel_layout(channelCount),
(enum AVSampleFormat)_sourceSampleFormat,
(int)_sourceSampleRate,
0,
NULL);
_currentSourceChannelCount = channelCount;
_ratio = MAX(1, _destinationSampleRate / MAX(_sourceSampleRate, 1)) * MAX(1, _destinationChannelCount / channelCount) * 2;
if (_context) {
swr_init(_context);
}
}
- (NSData * _Nullable)resample:(FFMpegAVFrame *)frame {
AVFrame *frameImpl = (AVFrame *)[frame impl];
int numChannels = frameImpl->channels;
if (numChannels != _sourceChannelCount) {
if (numChannels != _currentSourceChannelCount) {
[self resetContextForChannelCount:numChannels];
}
if (!_context) {
return nil;
}

View File

@ -230,20 +230,20 @@ public func chatMessageGalleryControllerData(context: AccountContext, chatLocati
let gallery = SecretMediaPreviewController(context: context, messageId: message.id)
return .secretGallery(gallery)
} else {
let startTimecode: Signal<Double?, NoError>
let startState: Signal<(timecode: Double?, rate: Double), NoError>
if let timecode = timecode {
startTimecode = .single(timecode)
startState = .single((timecode: timecode, rate: 1.0))
} else {
startTimecode = mediaPlaybackStoredState(postbox: context.account.postbox, messageId: message.id)
startState = mediaPlaybackStoredState(postbox: context.account.postbox, messageId: message.id)
|> map { state in
return state?.timestamp
return (state?.timestamp, state?.playbackRate.doubleValue ?? 1.0)
}
}
return .gallery(startTimecode
return .gallery(startState
|> deliverOnMainQueue
|> map { timecode in
let gallery = GalleryController(context: context, source: source ?? (standalone ? .standaloneMessage(message) : .peerMessagesAtId(messageId: message.id, chatLocation: chatLocation ?? .peer(message.id.peerId), chatLocationContextHolder: chatLocationContextHolder ?? Atomic<ChatLocationContextHolder?>(value: nil))), invertItemOrder: reverseMessageGalleryOrder, streamSingleVideo: stream, fromPlayingVideo: autoplayingVideo, landscape: landscape, timecode: timecode, synchronousLoad: synchronousLoad, replaceRootController: { [weak navigationController] controller, ready in
|> map { startState in
let gallery = GalleryController(context: context, source: source ?? (standalone ? .standaloneMessage(message) : .peerMessagesAtId(messageId: message.id, chatLocation: chatLocation ?? .peer(message.id.peerId), chatLocationContextHolder: chatLocationContextHolder ?? Atomic<ChatLocationContextHolder?>(value: nil))), invertItemOrder: reverseMessageGalleryOrder, streamSingleVideo: stream, fromPlayingVideo: autoplayingVideo, landscape: landscape, timecode: startState.timecode, playbackRate: startState.rate, synchronousLoad: synchronousLoad, replaceRootController: { [weak navigationController] controller, ready in
navigationController?.replaceTopController(controller, animated: false, ready: ready)
}, baseNavigationController: navigationController, actionInteraction: actionInteraction)
gallery.temporaryDoNotWaitForReady = autoplayingVideo

View File

@ -28,7 +28,10 @@ swift_library(
"//submodules/OverlayStatusController:OverlayStatusController",
"//submodules/PresentationDataUtils:PresentationDataUtils",
"//submodules/UrlEscaping:UrlEscaping",
"//submodules/ManagedAnimationNode:ManagedAnimationNode"
"//submodules/ManagedAnimationNode:ManagedAnimationNode",
"//submodules/ContextUI:ContextUI",
"//submodules/SaveToCameraRoll:SaveToCameraRoll",
"//submodules/TelegramUIPreferences:TelegramUIPreferences",
],
visibility = [
"//visibility:public",

View File

@ -31,6 +31,9 @@ private let forwardImage = generateTintedImage(image: UIImage(bundleImageName: "
private let cloudFetchIcon = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/FileCloudFetch"), color: UIColor.white)
private let fullscreenOnImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Expand"), color: .white)
private let fullscreenOffImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Collapse"), color: .white)
private let captionMaskImage = generateImage(CGSize(width: 1.0, height: 17.0), opaque: false, rotatedContext: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
context.clear(bounds)
@ -119,6 +122,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
private let contentNode: ASDisplayNode
private let deleteButton: UIButton
private let fullscreenButton: UIButton
private let actionButton: UIButton
private let editButton: UIButton
private let maskNode: ASDisplayNode
@ -152,6 +156,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
var seekBackward: ((Double) -> Void)?
var seekForward: ((Double) -> Void)?
var setPlayRate: ((Double) -> Void)?
var toggleFullscreen: (() -> Void)?
var fetchControl: (() -> Void)?
var interacting: ((Bool) -> Void)?
@ -161,7 +166,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
private var seekRate: Double = 1.0
var performAction: ((GalleryControllerInteractionTapAction) -> Void)?
var openActionOptions: ((GalleryControllerInteractionTapAction) -> Void)?
var openActionOptions: ((GalleryControllerInteractionTapAction, Message) -> Void)?
var content: ChatItemGalleryFooterContent = .info {
didSet {
@ -286,6 +291,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
self.contentNode = ASDisplayNode()
self.deleteButton = UIButton()
self.fullscreenButton = UIButton()
self.actionButton = UIButton()
self.editButton = UIButton()
@ -357,12 +363,13 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
}
}
self.textNode.longTapAttributeAction = { [weak self] attributes, index in
if let strongSelf = self, let action = strongSelf.actionForAttributes(attributes, index) {
strongSelf.openActionOptions?(action)
if let strongSelf = self, let action = strongSelf.actionForAttributes(attributes, index), let message = strongSelf.currentMessage {
strongSelf.openActionOptions?(action, message)
}
}
self.contentNode.view.addSubview(self.deleteButton)
self.contentNode.view.addSubview(self.fullscreenButton)
self.contentNode.view.addSubview(self.actionButton)
self.contentNode.view.addSubview(self.editButton)
self.contentNode.addSubnode(self.scrollWrapperNode)
@ -381,6 +388,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
self.contentNode.addSubnode(self.statusButtonNode)
self.deleteButton.addTarget(self, action: #selector(self.deleteButtonPressed), for: [.touchUpInside])
self.fullscreenButton.addTarget(self, action: #selector(self.fullscreenButtonPressed), for: [.touchUpInside])
self.actionButton.addTarget(self, action: #selector(self.actionButtonPressed), for: [.touchUpInside])
self.editButton.addTarget(self, action: #selector(self.editButtonPressed), for: [.touchUpInside])
@ -559,6 +567,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
if origin == nil {
self.editButton.isHidden = true
self.deleteButton.isHidden = true
self.fullscreenButton.isHidden = true
self.editButton.isHidden = true
}
}
@ -569,11 +578,21 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
let canDelete: Bool
var canShare = !message.containsSecretMedia
var canFullscreen = false
var canEdit = false
for media in message.media {
if media is TelegramMediaImage {
canEdit = true
break
} else if let media = media as? TelegramMediaFile, !media.isAnimated {
for attribute in media.attributes {
switch attribute {
case .Video:
canFullscreen = true
default:
break
}
}
}
}
@ -637,7 +656,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
messageText = galleryCaptionStringWithAppliedEntities(message.text, entities: entities)
}
if self.currentMessageText != messageText || canDelete != !self.deleteButton.isHidden || canShare != !self.actionButton.isHidden || canEdit != !self.editButton.isHidden || self.currentAuthorNameText != authorNameText || self.currentDateText != dateText {
if self.currentMessageText != messageText || canDelete != !self.deleteButton.isHidden || canFullscreen != !self.fullscreenButton.isHidden || canShare != !self.actionButton.isHidden || canEdit != !self.editButton.isHidden || self.currentAuthorNameText != authorNameText || self.currentDateText != dateText {
self.currentMessageText = messageText
if messageText.length == 0 {
@ -655,7 +674,14 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
}
self.dateNode.attributedText = NSAttributedString(string: dateText, font: dateFont, textColor: .white)
self.deleteButton.isHidden = !canDelete
if canFullscreen {
self.fullscreenButton.isHidden = false
self.deleteButton.isHidden = true
} else {
self.deleteButton.isHidden = !canDelete
self.fullscreenButton.isHidden = true
}
self.actionButton.isHidden = !canShare
self.editButton.isHidden = !canEdit
@ -683,6 +709,9 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
panelHeight += contentInset
let isLandscape = size.width > size.height
self.fullscreenButton.setImage(isLandscape ? fullscreenOffImage : fullscreenOnImage, for: [.normal])
let displayCaption: Bool
if case .compact = metrics.widthClass {
displayCaption = !self.textNode.isHidden && !isLandscape
@ -776,10 +805,11 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
let deleteFrame = CGRect(origin: CGPoint(x: width - 44.0 - rightInset, y: panelHeight - bottomInset - 44.0), size: CGSize(width: 44.0, height: 44.0))
var editFrame = CGRect(origin: CGPoint(x: width - 44.0 - 50.0 - rightInset, y: panelHeight - bottomInset - 44.0), size: CGSize(width: 44.0, height: 44.0))
if self.deleteButton.isHidden {
if self.deleteButton.isHidden && self.fullscreenButton.isHidden {
editFrame = deleteFrame
}
self.deleteButton.frame = deleteFrame
self.fullscreenButton.frame = deleteFrame
self.editButton.frame = editFrame
if let image = self.backwardButton.backgroundIconNode.image {
@ -789,7 +819,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
self.forwardButton.frame = CGRect(origin: CGPoint(x: floor((width - image.size.width) / 2.0) + 66.0, y: panelHeight - bottomInset - 44.0 + 7.0), size: image.size)
}
self.playbackControlButton.frame = CGRect(origin: CGPoint(x: floor((width - 44.0) / 2.0), y: panelHeight - bottomInset - 44.0), size: CGSize(width: 44.0, height: 44.0))
self.playbackControlButton.frame = CGRect(origin: CGPoint(x: floor((width - 44.0) / 2.0), y: panelHeight - bottomInset - 44.0 - 2.0), size: CGSize(width: 44.0, height: 44.0))
self.playPauseIconNode.frame = self.playbackControlButton.bounds.offsetBy(dx: 2.0, dy: 2.0)
let statusSize = CGSize(width: 28.0, height: 28.0)
@ -855,6 +885,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
self.dateNode.alpha = 1.0
self.authorNameNode.alpha = 1.0
self.deleteButton.alpha = 1.0
self.fullscreenButton.alpha = 1.0
self.actionButton.alpha = 1.0
self.editButton.alpha = 1.0
self.backwardButton.alpha = 1.0
@ -878,6 +909,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
self.dateNode.alpha = 0.0
self.authorNameNode.alpha = 0.0
self.deleteButton.alpha = 0.0
self.fullscreenButton.alpha = 0.0
self.actionButton.alpha = 0.0
self.editButton.alpha = 0.0
self.backwardButton.alpha = 0.0
@ -889,6 +921,10 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
})
}
@objc func fullscreenButtonPressed() {
self.toggleFullscreen?()
}
@objc func deleteButtonPressed() {
if let currentMessage = self.currentMessage {
let _ = (self.context.account.postbox.transaction { transaction -> [Message] in

View File

@ -145,7 +145,7 @@ 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, displayInfoOnTop: Bool = false, 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? {
public func galleryItemForEntry(context: AccountContext, presentationData: PresentationData, entry: MessageHistoryEntry, isCentral: Bool = false, streamVideos: Bool, loopVideos: Bool = false, hideControls: Bool = false, fromPlayingVideo: Bool = false, isSecret: Bool = false, landscape: Bool = false, timecode: Double? = nil, playbackRate: Double = 1.0, displayInfoOnTop: Bool = false, configuration: GalleryConfiguration? = nil, tempFilePath: String? = nil, playbackCompleted: @escaping () -> Void = {}, performAction: @escaping (GalleryControllerInteractionTapAction) -> Void = { _ in }, openActionOptions: @escaping (GalleryControllerInteractionTapAction, Message) -> Void = { _, _ in }, storeMediaPlaybackState: @escaping (MessageId, Double?, Double) -> Void = { _, _, _ in }, present: @escaping (ViewController, Any?) -> Void) -> GalleryItem? {
let message = entry.message
let location = entry.location
if let (media, mediaImage) = mediaForMessage(message: message) {
@ -178,7 +178,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, displayInfoOnTop: displayInfoOnTop, hideControls: hideControls, fromPlayingVideo: fromPlayingVideo, landscape: landscape, timecode: timecode, configuration: configuration, playbackCompleted: playbackCompleted, performAction: performAction, openActionOptions: openActionOptions, storeMediaPlaybackState: storeMediaPlaybackState, present: present)
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, displayInfoOnTop: displayInfoOnTop, hideControls: hideControls, fromPlayingVideo: fromPlayingVideo, isSecret: isSecret, landscape: landscape, timecode: timecode, playbackRate: playbackRate, 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)
@ -188,7 +188,7 @@ public func galleryItemForEntry(context: AccountContext, presentationData: Prese
if let dimensions = file.dimensions {
pixelsCount = Int(dimensions.width) * Int(dimensions.height)
}
if (file.size == nil || file.size! < 4 * 1024 * 1024) && pixelsCount < 4096 * 4096 {
if pixelsCount < 10000 * 10000 {
return ChatImageGalleryItem(context: context, presentationData: presentationData, message: message, location: location, displayInfoOnTop: displayInfoOnTop, performAction: performAction, openActionOptions: openActionOptions, present: present)
} else {
return ChatDocumentGalleryItem(context: context, presentationData: presentationData, message: message, location: location)
@ -219,7 +219,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: ""), displayInfoOnTop: displayInfoOnTop, fromPlayingVideo: fromPlayingVideo, landscape: landscape, timecode: timecode, configuration: configuration, performAction: performAction, openActionOptions: openActionOptions, storeMediaPlaybackState: storeMediaPlaybackState, present: present)
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: ""), displayInfoOnTop: displayInfoOnTop, fromPlayingVideo: fromPlayingVideo, isSecret: isSecret, landscape: landscape, timecode: timecode, playbackRate: playbackRate, configuration: configuration, performAction: performAction, openActionOptions: openActionOptions, storeMediaPlaybackState: storeMediaPlaybackState, present: present)
} else {
return nil
}
@ -349,6 +349,7 @@ public class GalleryController: ViewController, StandalonePresentableController
private let fromPlayingVideo: Bool
private let landscape: Bool
private let timecode: Double?
private let playbackRate: Double
private let accountInUseDisposable = MetaDisposable()
private let disposable = MetaDisposable()
@ -378,7 +379,7 @@ public class GalleryController: ViewController, StandalonePresentableController
private let actionInteraction: GalleryControllerActionInteraction?
private var performAction: (GalleryControllerInteractionTapAction) -> Void
private var openActionOptions: (GalleryControllerInteractionTapAction) -> Void
private var openActionOptions: (GalleryControllerInteractionTapAction, Message) -> Void
private let updateVisibleDisposable = MetaDisposable()
@ -388,7 +389,7 @@ public class GalleryController: ViewController, StandalonePresentableController
private var initialOrientation: UIInterfaceOrientation?
public init(context: AccountContext, source: GalleryControllerItemSource, invertItemOrder: Bool = false, streamSingleVideo: Bool = false, fromPlayingVideo: Bool = false, landscape: Bool = false, timecode: Double? = nil, synchronousLoad: Bool = false, replaceRootController: @escaping (ViewController, Promise<Bool>?) -> Void, baseNavigationController: NavigationController?, actionInteraction: GalleryControllerActionInteraction? = nil) {
public init(context: AccountContext, source: GalleryControllerItemSource, invertItemOrder: Bool = false, streamSingleVideo: Bool = false, fromPlayingVideo: Bool = false, landscape: Bool = false, timecode: Double? = nil, playbackRate: Double = 1.0, synchronousLoad: Bool = false, replaceRootController: @escaping (ViewController, Promise<Bool>?) -> Void, baseNavigationController: NavigationController?, actionInteraction: GalleryControllerActionInteraction? = nil) {
self.context = context
self.source = source
self.invertItemOrder = invertItemOrder
@ -399,6 +400,7 @@ public class GalleryController: ViewController, StandalonePresentableController
self.fromPlayingVideo = fromPlayingVideo
self.landscape = landscape
self.timecode = timecode
self.playbackRate = playbackRate
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
@ -407,9 +409,9 @@ public class GalleryController: ViewController, StandalonePresentableController
performActionImpl?(action)
}
var openActionOptionsImpl: ((GalleryControllerInteractionTapAction) -> Void)?
self.openActionOptions = { action in
openActionOptionsImpl?(action)
var openActionOptionsImpl: ((GalleryControllerInteractionTapAction, Message) -> Void)?
self.openActionOptions = { action, message in
openActionOptionsImpl?(action, message)
}
super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: GalleryController.darkNavigationTheme, strings: NavigationBarStrings(presentationStrings: self.presentationData.strings)))
@ -537,7 +539,7 @@ 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, displayInfoOnTop: displayInfoOnTop, configuration: configuration, performAction: strongSelf.performAction, openActionOptions: strongSelf.openActionOptions, storeMediaPlaybackState: strongSelf.actionInteraction?.storeMediaPlaybackState ?? { _, _ in }, present: { [weak self] c, a 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, playbackRate: isCentral ? playbackRate : 1.0, displayInfoOnTop: displayInfoOnTop, 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)
}
@ -667,7 +669,7 @@ public class GalleryController: ViewController, StandalonePresentableController
}
}
openActionOptionsImpl = { [weak self] action in
openActionOptionsImpl = { [weak self] action, message in
if let strongSelf = self {
var presentationData = strongSelf.presentationData
if !presentationData.theme.overallDarkAppearance {
@ -847,6 +849,13 @@ public class GalleryController: ViewController, StandalonePresentableController
])
strongSelf.present(actionSheet, in: .window(.root))
case let .timecode(timecode, text):
let isCopyLink: Bool
if message.id.namespace == Namespaces.Message.Cloud, let _ = message.peers[message.id.peerId] as? TelegramChannel, !(message.media.first is TelegramMediaAction) {
isCopyLink = true
} else {
isCopyLink = false
}
let actionSheet = ActionSheetController(presentationData: presentationData)
actionSheet.setItemGroups([ActionSheetItemGroup(items: [
ActionSheetTextItem(title: text),
@ -857,12 +866,50 @@ public class GalleryController: ViewController, StandalonePresentableController
strongSelf.galleryNode.pager.centralItemNode()?.processAction(.timecode(timecode))
}
}),
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet, weak self] in
ActionSheetButtonItem(title: isCopyLink ? strongSelf.presentationData.strings.Conversation_ContextMenuCopyLink : strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet, weak self] in
actionSheet?.dismissAnimated()
UIPasteboard.general.string = text
if isCopyLink, let channel = message.peers[message.id.peerId] as? TelegramChannel {
var threadMessageId: MessageId?
// if case let .replyThread(replyThreadMessage) = chatPresentationInterfaceState.chatLocation {
// threadMessageId = replyThreadMessage.messageId
// }
let _ = (context.engine.messages.exportMessageLink(peerId: message.id.peerId, messageId: message.id, isThread: threadMessageId != nil)
|> map { result -> String? in
return result
}
|> deliverOnMainQueue).start(next: { link in
if let link = link {
UIPasteboard.general.string = link + "?t=\(Int32(timecode))"
let content: UndoOverlayContent = .copy(text: presentationData.strings.Conversation_TextCopied)
self?.present(UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root))
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
var warnAboutPrivate = false
if channel.addressName == nil {
warnAboutPrivate = true
}
Queue.mainQueue().after(0.2, {
let content: UndoOverlayContent
if warnAboutPrivate {
content = .linkCopied(text: presentationData.strings.Conversation_PrivateMessageLinkCopiedLong)
} else {
content = .linkCopied(text: presentationData.strings.Conversation_LinkCopied)
}
self?.present(UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root))
})
} else {
UIPasteboard.general.string = text
let content: UndoOverlayContent = .copy(text: presentationData.strings.Conversation_TextCopied)
self?.present(UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current)
}
})
} else {
UIPasteboard.general.string = text
let content: UndoOverlayContent = .copy(text: presentationData.strings.Conversation_TextCopied)
self?.present(UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root))
}
})
]), ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
@ -1040,6 +1087,9 @@ public class GalleryController: ViewController, StandalonePresentableController
self.galleryNode.baseNavigationController = { [weak baseNavigationController] in
return baseNavigationController
}
self.galleryNode.galleryController = { [weak self] in
return self
}
var displayInfoOnTop = false
if case .custom = source {
@ -1053,7 +1103,7 @@ 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, displayInfoOnTop: displayInfoOnTop, configuration: self.configuration, performAction: self.performAction, openActionOptions: self.openActionOptions, storeMediaPlaybackState: self.actionInteraction?.storeMediaPlaybackState ?? { _, _ in }, present: { [weak self] c, a 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, playbackRate: isCentral ? self.playbackRate : 1.0, displayInfoOnTop: displayInfoOnTop, 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)
}
@ -1133,7 +1183,7 @@ 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, displayInfoOnTop: displayInfoOnTop, configuration: strongSelf.configuration, performAction: strongSelf.performAction, openActionOptions: strongSelf.openActionOptions, storeMediaPlaybackState: strongSelf.actionInteraction?.storeMediaPlaybackState ?? { _, _ in }, present: { [weak self] c, a 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, displayInfoOnTop: displayInfoOnTop, 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)
}
@ -1185,7 +1235,7 @@ 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, displayInfoOnTop: displayInfoOnTop, configuration: strongSelf.configuration, performAction: strongSelf.performAction, openActionOptions: strongSelf.openActionOptions, storeMediaPlaybackState: strongSelf.actionInteraction?.storeMediaPlaybackState ?? { _, _ in }, present: { [weak self] c, a 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, displayInfoOnTop: displayInfoOnTop, 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)
}

View File

@ -22,6 +22,7 @@ open class GalleryControllerNode: ASDisplayNode, UIScrollViewDelegate, UIGesture
public var beginCustomDismiss: () -> Void = { }
public var completeCustomDismiss: () -> Void = { }
public var baseNavigationController: () -> NavigationController? = { return nil }
public var galleryController: () -> ViewController? = { return nil }
private var presentationState = GalleryControllerPresentationState()
@ -120,6 +121,9 @@ open class GalleryControllerNode: ASDisplayNode, UIScrollViewDelegate, UIGesture
self.pager.baseNavigationController = { [weak self] in
return self?.baseNavigationController()
}
self.pager.galleryController = { [weak self] in
return self?.galleryController()
}
self.addSubnode(self.backgroundNode)

View File

@ -27,6 +27,7 @@ open class GalleryItemNode: ASDisplayNode {
public var beginCustomDismiss: () -> Void = { }
public var completeCustomDismiss: () -> Void = { }
public var baseNavigationController: () -> NavigationController? = { return nil }
public var galleryController: () -> ViewController? = { return nil }
public var alternativeDismiss: () -> Bool = { return false }
override public init() {

View File

@ -112,6 +112,7 @@ public final class GalleryPagerNode: ASDisplayNode, UIScrollViewDelegate, UIGest
public var beginCustomDismiss: () -> Void = { }
public var completeCustomDismiss: () -> Void = { }
public var baseNavigationController: () -> NavigationController? = { return nil }
public var galleryController: () -> ViewController? = { return nil }
public init(pageGap: CGFloat, disableTapNavigation: Bool) {
self.pageGap = pageGap
@ -480,6 +481,7 @@ public final class GalleryPagerNode: ASDisplayNode, UIScrollViewDelegate, UIGest
node.beginCustomDismiss = self.beginCustomDismiss
node.completeCustomDismiss = self.completeCustomDismiss
node.baseNavigationController = self.baseNavigationController
node.galleryController = self.galleryController
node.index = index
return node
}

View File

@ -106,10 +106,10 @@ class ChatImageGalleryItem: GalleryItem {
let location: MessageHistoryEntryLocation?
let displayInfoOnTop: Bool
let performAction: (GalleryControllerInteractionTapAction) -> Void
let openActionOptions: (GalleryControllerInteractionTapAction) -> Void
let openActionOptions: (GalleryControllerInteractionTapAction, Message) -> Void
let present: (ViewController, Any?) -> Void
init(context: AccountContext, presentationData: PresentationData, message: Message, location: MessageHistoryEntryLocation?, displayInfoOnTop: Bool, performAction: @escaping (GalleryControllerInteractionTapAction) -> Void, openActionOptions: @escaping (GalleryControllerInteractionTapAction) -> Void, present: @escaping (ViewController, Any?) -> Void) {
init(context: AccountContext, presentationData: PresentationData, message: Message, location: MessageHistoryEntryLocation?, displayInfoOnTop: Bool, performAction: @escaping (GalleryControllerInteractionTapAction) -> Void, openActionOptions: @escaping (GalleryControllerInteractionTapAction, Message) -> Void, present: @escaping (ViewController, Any?) -> Void) {
self.context = context
self.presentationData = presentationData
self.message = message
@ -206,7 +206,7 @@ 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, present: @escaping (ViewController, Any?) -> Void) {
init(context: AccountContext, presentationData: PresentationData, performAction: @escaping (GalleryControllerInteractionTapAction) -> Void, openActionOptions: @escaping (GalleryControllerInteractionTapAction, Message) -> Void, present: @escaping (ViewController, Any?) -> Void) {
self.context = context
self.imageNode = TransformImageNode()

View File

@ -15,6 +15,11 @@ import PresentationDataUtils
import OverlayStatusController
import StickerPackPreviewUI
import AppBundle
import AnimationUI
import ContextUI
import SaveToCameraRoll
import UndoUI
import TelegramUIPreferences
public enum UniversalVideoGalleryItemContentInfo {
case message(Message)
@ -37,16 +42,18 @@ public class UniversalVideoGalleryItem: GalleryItem {
let displayInfoOnTop: Bool
let hideControls: Bool
let fromPlayingVideo: Bool
let isSecret: Bool
let landscape: Bool
let timecode: Double?
let playbackRate: Double
let configuration: GalleryConfiguration?
let playbackCompleted: () -> Void
let performAction: (GalleryControllerInteractionTapAction) -> Void
let openActionOptions: (GalleryControllerInteractionTapAction) -> Void
let storeMediaPlaybackState: (MessageId, Double?) -> Void
let openActionOptions: (GalleryControllerInteractionTapAction, Message) -> Void
let storeMediaPlaybackState: (MessageId, Double?, 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, displayInfoOnTop: Bool = false, 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) {
public init(context: AccountContext, presentationData: PresentationData, content: UniversalVideoContent, originData: GalleryItemOriginData?, indexData: GalleryItemIndexData?, contentInfo: UniversalVideoGalleryItemContentInfo?, caption: NSAttributedString, credit: NSAttributedString? = nil, displayInfoOnTop: Bool = false, hideControls: Bool = false, fromPlayingVideo: Bool = false, isSecret: Bool = false, landscape: Bool = false, timecode: Double? = nil, playbackRate: Double = 1.0, configuration: GalleryConfiguration? = nil, playbackCompleted: @escaping () -> Void = {}, performAction: @escaping (GalleryControllerInteractionTapAction) -> Void, openActionOptions: @escaping (GalleryControllerInteractionTapAction, Message) -> Void, storeMediaPlaybackState: @escaping (MessageId, Double?, Double) -> Void, present: @escaping (ViewController, Any?) -> Void) {
self.context = context
self.presentationData = presentationData
self.content = content
@ -58,8 +65,10 @@ public class UniversalVideoGalleryItem: GalleryItem {
self.displayInfoOnTop = displayInfoOnTop
self.hideControls = hideControls
self.fromPlayingVideo = fromPlayingVideo
self.isSecret = isSecret
self.landscape = landscape
self.timecode = timecode
self.playbackRate = playbackRate
self.configuration = configuration
self.playbackCompleted = playbackCompleted
self.performAction = performAction
@ -129,6 +138,8 @@ public class UniversalVideoGalleryItem: GalleryItem {
private let pictureInPictureImage = UIImage(bundleImageName: "Media Gallery/PictureInPictureIcon")?.precomposed()
private let pictureInPictureButtonImage = generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/PictureInPictureButton"), color: .white)
private let moreButtonImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/More"), color: .white)
private let placeholderFont = Font.regular(16.0)
private final class UniversalVideoGalleryItemPictureInPictureNode: ASDisplayNode {
@ -251,6 +262,196 @@ private struct FetchControls {
let cancel: () -> Void
}
func optionsBackgroundImage(dark: Bool) -> UIImage? {
return generateImage(CGSize(width: 28.0, height: 28.0), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(UIColor(rgb: dark ? 0x1c1c1e : 0x2c2c2e).cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
})?.stretchableImage(withLeftCapWidth: 14, topCapHeight: 14)
}
private func optionsCircleImage(dark: Bool) -> UIImage? {
return generateImage(CGSize(width: 22.0, height: 22.0), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setStrokeColor(UIColor.white.cgColor)
let lineWidth: CGFloat = 1.3
context.setLineWidth(lineWidth)
context.strokeEllipse(in: CGRect(origin: CGPoint(), size: size).insetBy(dx: lineWidth, dy: lineWidth))
})
}
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()
})
}
private final class MoreHeaderButton: HighlightableButtonNode {
enum Content {
case image(UIImage?)
case more(UIImage?)
}
let referenceNode: ContextReferenceContentNode
let containerNode: ContextControllerSourceNode
private let iconNode: ASImageNode
private var animationNode: AnimationNode?
var contextAction: ((ASDisplayNode, ContextGesture?) -> Void)?
private let wide: Bool
init(wide: Bool = false) {
self.wide = wide
self.referenceNode = ContextReferenceContentNode()
self.containerNode = ContextControllerSourceNode()
self.containerNode.animateScale = false
self.iconNode = ASImageNode()
self.iconNode.displaysAsynchronously = false
self.iconNode.displayWithoutProcessing = true
self.iconNode.contentMode = .scaleToFill
super.init()
self.containerNode.addSubnode(self.referenceNode)
self.referenceNode.addSubnode(self.iconNode)
self.addSubnode(self.containerNode)
self.containerNode.shouldBegin = { [weak self] location in
guard let strongSelf = self, let _ = strongSelf.contextAction else {
return false
}
return true
}
self.containerNode.activated = { [weak self] gesture, _ in
guard let strongSelf = self else {
return
}
strongSelf.contextAction?(strongSelf.containerNode, gesture)
}
self.containerNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: wide ? 32.0 : 22.0, height: 22.0))
self.referenceNode.frame = self.containerNode.bounds
self.iconNode.image = optionsCircleImage(dark: false)
if let image = self.iconNode.image {
self.iconNode.frame = CGRect(origin: CGPoint(x: floor((self.containerNode.bounds.width - image.size.width) / 2.0), y: floor((self.containerNode.bounds.height - image.size.height) / 2.0)), size: image.size)
}
}
private var content: Content?
func setContent(_ content: Content, animated: Bool = false) {
if case .more = content, self.animationNode == nil {
let iconColor = UIColor(rgb: 0xffffff)
let animationNode = AnimationNode(animation: "anim_profilemore", colors: ["Point 2.Group 1.Fill 1": iconColor,
"Point 3.Group 1.Fill 1": iconColor,
"Point 1.Group 1.Fill 1": iconColor], scale: 1.0)
animationNode.frame = self.containerNode.bounds
self.addSubnode(animationNode)
self.animationNode = animationNode
}
if animated {
if let snapshotView = self.referenceNode.view.snapshotContentTree() {
snapshotView.frame = self.referenceNode.frame
self.view.addSubview(snapshotView)
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak snapshotView] _ in
snapshotView?.removeFromSuperview()
})
snapshotView.layer.animateScale(from: 1.0, to: 0.1, duration: 0.3, removeOnCompletion: false)
self.iconNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
self.iconNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.3)
self.animationNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
self.animationNode?.layer.animateScale(from: 0.1, to: 1.0, duration: 0.3)
}
switch content {
case let .image(image):
if let image = image {
self.iconNode.frame = CGRect(origin: CGPoint(x: floor((self.containerNode.bounds.width - image.size.width) / 2.0), y: floor((self.containerNode.bounds.height - image.size.height) / 2.0)), size: image.size)
}
self.iconNode.image = image
self.iconNode.isHidden = false
self.animationNode?.isHidden = true
case let .more(image):
if let image = image {
self.iconNode.frame = CGRect(origin: CGPoint(x: floor((self.containerNode.bounds.width - image.size.width) / 2.0), y: floor((self.containerNode.bounds.height - image.size.height) / 2.0)), size: image.size)
}
self.iconNode.image = image
self.iconNode.isHidden = false
self.animationNode?.isHidden = false
}
} else {
self.content = content
switch content {
case let .image(image):
self.iconNode.image = image
self.iconNode.isHidden = false
self.animationNode?.isHidden = true
case let .more(image):
self.iconNode.image = image
self.iconNode.isHidden = false
self.animationNode?.isHidden = false
}
}
}
override func didLoad() {
super.didLoad()
self.view.isOpaque = false
}
override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
return CGSize(width: wide ? 32.0 : 22.0, height: 22.0)
}
func onLayout() {
}
func play() {
self.animationNode?.playOnce()
}
}
final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
private let context: AccountContext
private let presentationData: PresentationData
@ -265,6 +466,10 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
private let footerContentNode: ChatItemGalleryFooterContentNode
private let overlayContentNode: UniversalVideoGalleryItemOverlayNode
private let moreBarButton: MoreHeaderButton
private var moreBarButtonRate: Double = 1.0
private var moreBarButtonRateTimestamp: Double?
private var videoNode: UniversalVideoNode?
private var videoNodeUserInteractionEnabled: Bool = false
private var videoFramePreview: FramePreview?
@ -292,6 +497,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
private var item: UniversalVideoGalleryItem?
private let statusDisposable = MetaDisposable()
private let moreButtonStateDisposable = MetaDisposable()
private let mediaPlaybackStateDisposable = MetaDisposable()
private let fetchDisposable = MetaDisposable()
@ -305,13 +511,14 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
private let isPlayingPromise = ValuePromise<Bool>(false, ignoreRepeated: true)
private let isInteractingPromise = ValuePromise<Bool>(false, ignoreRepeated: true)
private let controlsVisiblePromise = ValuePromise<Bool>(true, ignoreRepeated: true)
private let isShowingContextMenuPromise = ValuePromise<Bool>(false, ignoreRepeated: true)
private var hideControlsDisposable: Disposable?
var playbackCompleted: (() -> Void)?
private var customUnembedWhenPortrait: ((OverlayMediaItemNode) -> Bool)?
init(context: AccountContext, presentationData: PresentationData, performAction: @escaping (GalleryControllerInteractionTapAction) -> Void, openActionOptions: @escaping (GalleryControllerInteractionTapAction) -> Void, present: @escaping (ViewController, Any?) -> Void) {
init(context: AccountContext, presentationData: PresentationData, performAction: @escaping (GalleryControllerInteractionTapAction) -> Void, openActionOptions: @escaping (GalleryControllerInteractionTapAction, Message) -> Void, present: @escaping (ViewController, Any?) -> Void) {
self.context = context
self.presentationData = presentationData
self.scrubberView = ChatVideoGalleryItemScrubberView()
@ -329,8 +536,14 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
self._title.set(.single(""))
self.moreBarButton = MoreHeaderButton()
self.moreBarButton.isUserInteractionEnabled = true
self.moreBarButton.setContent(.more(optionsCircleImage(dark: false)))
super.init()
self.moreBarButton.addTarget(self, action: #selector(self.moreButtonPressed), forControlEvents: .touchUpInside)
self.footerContentNode.interacting = { [weak self] value in
self?.isInteractingPromise.set(value)
}
@ -427,6 +640,19 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
}
}
self.footerContentNode.toggleFullscreen = { [weak self] in
guard let strongSelf = self else {
return
}
var toLandscape = false
let size = strongSelf.bounds.size
if size.width < size.height {
toLandscape = true
}
strongSelf.updateControlsVisibility(!toLandscape)
strongSelf.updateOrientation(toLandscape ? .landscapeRight : .portrait)
}
self.scrubbingFrameDisposable = (self.scrubbingFrame.get()
|> deliverOnMainQueue).start(next: { [weak self] result in
guard let strongSelf = self else {
@ -452,11 +678,18 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
return true
}
self.moreBarButton.contextAction = { [weak self] sourceNode, gesture in
self?.openMoreMenu(sourceNode: sourceNode, gesture: gesture)
}
self.titleContentView = GalleryTitleView(frame: CGRect())
self._titleView.set(.single(self.titleContentView))
let shouldHideControlsSignal: Signal<Void, NoError> = combineLatest(self.isPlayingPromise.get(), self.isInteractingPromise.get(), self.controlsVisiblePromise.get())
|> mapToSignal { isPlaying, isIntracting, controlsVisible -> Signal<Void, NoError> in
let shouldHideControlsSignal: Signal<Void, NoError> = combineLatest(self.isPlayingPromise.get(), self.isInteractingPromise.get(), self.controlsVisiblePromise.get(), self.isShowingContextMenuPromise.get())
|> mapToSignal { isPlaying, isIntracting, controlsVisible, isShowingContextMenu -> Signal<Void, NoError> in
if isShowingContextMenu {
return .complete()
}
if isPlaying && !isIntracting && controlsVisible {
return .single(Void())
|> delay(4.0, queue: Queue.mainQueue())
@ -475,6 +708,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
deinit {
self.statusDisposable.dispose()
self.moreButtonStateDisposable.dispose()
self.mediaPlaybackStateDisposable.dispose()
self.scrubbingFrameDisposable?.dispose()
self.hideControlsDisposable?.dispose()
@ -644,7 +878,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
if status.timestamp > 5.0 && status.timestamp < status.duration - 5.0 {
timestamp = status.timestamp
}
item.storeMediaPlaybackState(message.id, timestamp)
item.storeMediaPlaybackState(message.id, timestamp, status.baseRate)
}
}))
}
@ -685,6 +919,56 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
}
}
self.moreButtonStateDisposable.set(combineLatest(queue: .mainQueue(),
videoNode.status,
self.isShowingContextMenuPromise.get()
).start(next: { [weak self] status, isShowingContextMenu in
guard let strongSelf = self else {
return
}
guard let status = status else {
return
}
let effectiveBaseRate: Double
if isShowingContextMenu {
effectiveBaseRate = 1.0
} else {
effectiveBaseRate = status.baseRate
}
if abs(effectiveBaseRate - strongSelf.moreBarButtonRate) > 0.01 {
strongSelf.moreBarButtonRate = effectiveBaseRate
let animated: Bool
if let moreBarButtonRateTimestamp = strongSelf.moreBarButtonRateTimestamp {
animated = CFAbsoluteTimeGetCurrent() > (moreBarButtonRateTimestamp + 0.2)
} else {
animated = false
}
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"
}
strongSelf.moreBarButton.setContent(.image(optionsRateImage(rate: rateString, isLarge: true)), animated: animated)
} else {
strongSelf.moreBarButton.setContent(.more(optionsCircleImage(dark: false)), animated: animated)
}
} else {
if strongSelf.moreBarButtonRateTimestamp == nil {
strongSelf.moreBarButtonRateTimestamp = CFAbsoluteTimeGetCurrent()
}
}
}))
self.statusDisposable.set((combineLatest(queue: .mainQueue(), videoNode.status, mediaFileStatus)
|> deliverOnMainQueue).start(next: { [weak self] value, fetchStatus in
if let strongSelf = self {
@ -823,6 +1107,27 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
} else {
self.hasPictureInPicture = false
}
if let contentInfo = item.contentInfo, case let .message(message) = contentInfo {
var file: TelegramMediaFile?
var isWebpage = false
for m in message.media {
if let m = m as? TelegramMediaFile, m.isVideo {
file = m
break
} else if let m = m as? TelegramMediaWebpage, case let .Loaded(content) = m.content, let f = content.file, f.isVideo {
file = f
isWebpage = true
break
}
}
if !isWebpage, let file = file, !file.isAnimated {
let moreMenuItem = UIBarButtonItem(customDisplayNode: self.moreBarButton)!
barButtonItems.append(moreMenuItem)
}
}
self._rightBarButtonItems.set(.single(barButtonItems))
videoNode.playbackCompleted = { [weak self, weak videoNode] in
@ -999,18 +1304,21 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
var isAnimated = false
var seek = MediaPlayerSeek.start
var playbackRate: Double = 1.0
if let item = self.item {
if let content = item.content as? NativeVideoContent {
isAnimated = content.fileReference.media.isAnimated
if let time = item.timecode {
seek = .timecode(time)
}
playbackRate = item.playbackRate
} else if let _ = item.content as? WebEmbedVideoContent {
if let time = item.timecode {
seek = .timecode(time)
}
}
}
videoNode.setBaseRate(playbackRate)
if isAnimated {
videoNode.seek(0.0)
videoNode.play()
@ -1024,7 +1332,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
private var actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd {
if let item = self.item {
if let content = item.content as? NativeVideoContent, content.duration <= 30 {
if !item.isSecret, let content = item.content as? NativeVideoContent, content.duration <= 30 {
return .loop
}
}
@ -1614,6 +1922,207 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
}
}
private func contentInfo() -> (message: Message, file: TelegramMediaFile, isWebpage: Bool)? {
guard let item = self.item else {
return nil
}
if let contentInfo = item.contentInfo, case let .message(message) = contentInfo {
var file: TelegramMediaFile?
var isWebpage = false
for m in message.media {
if let m = m as? TelegramMediaFile, m.isVideo {
file = m
break
} else if let m = m as? TelegramMediaWebpage, case let .Loaded(content) = m.content, let f = content.file, f.isVideo {
file = f
isWebpage = true
break
}
}
if let file = file {
return (message, file, isWebpage)
}
}
return nil
}
private func canDelete() -> Bool {
guard let (message, _, _) = self.contentInfo() else {
return false
}
var canDelete = false
if let peer = message.peers[message.id.peerId] {
if peer is TelegramUser || peer is TelegramSecretChat {
canDelete = true
} else if let _ = peer as? TelegramGroup {
canDelete = true
} else if let channel = peer as? TelegramChannel {
if message.flags.contains(.Incoming) {
canDelete = channel.hasPermission(.deleteAllMessages)
} else {
canDelete = true
}
} else {
canDelete = false
}
} else {
canDelete = false
}
return canDelete
}
@objc private func moreButtonPressed() {
self.moreBarButton.play()
self.moreBarButton.contextAction?(self.moreBarButton.containerNode, nil)
}
private func openMoreMenu(sourceNode: ASDisplayNode, gesture: ContextGesture?) {
let items: Signal<[ContextMenuItem], NoError> = self.contextMenuMainItems()
guard let controller = self.baseNavigationController()?.topViewController as? ViewController else {
return
}
let contextController = ContextController(account: self.context.account, presentationData: self.presentationData.withUpdated(theme: defaultDarkColorPresentationTheme), source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceNode: self.moreBarButton.referenceNode)), items: items, reactionItems: [], gesture: gesture)
self.isShowingContextMenuPromise.set(true)
controller.presentInGlobalOverlay(contextController)
contextController.dismissed = { [weak self] in
Queue.mainQueue().after(0.1, {
self?.isShowingContextMenuPromise.set(false)
})
}
}
private func speedList() -> [(String, String, Double)] {
let speedList: [(String, String, Double)] = [
("0.5x", "0.5x", 0.5),
("Normal", "1x", 1.0),
("1.5x", "1.5x", 1.5),
("2x", "2x", 2.0)
]
return speedList
}
private func contextMenuMainItems() -> Signal<[ContextMenuItem], NoError> {
guard let videoNode = self.videoNode else {
return .single([])
}
return videoNode.status
|> take(1)
|> deliverOnMainQueue
|> map { [weak self] status -> [ContextMenuItem] in
guard let status = status, let strongSelf = self else {
return []
}
var items: [ContextMenuItem] = []
var speedValue: String = "Normal"
var speedIconText: String = "1x"
for (text, iconText, speed) in strongSelf.speedList() {
if abs(speed - status.baseRate) < 0.01 {
speedValue = text
speedIconText = iconText
break
}
}
items.append(.action(ContextMenuActionItem(text: "Playback Speed", 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())
})))
if let (message, file, isWebpage) = strongSelf.contentInfo(), !isWebpage {
items.append(.action(ContextMenuActionItem(text: "Save to Gallery", icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Download"), color: theme.actionSheet.primaryTextColor) }, action: { _, f in
f(.default)
if let strongSelf = self {
let _ = (SaveToCameraRoll.saveToCameraRoll(context: strongSelf.context, postbox: strongSelf.context.account.postbox, mediaReference: .message(message: MessageReference(message), media: file))
|> deliverOnMainQueue).start(completed: {
guard let strongSelf = self else {
return
}
guard let controller = strongSelf.galleryController() else {
return
}
//TODO:localize
controller.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .mediaSaved(text: "Video Saved"), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root))
})
}
})))
}
if strongSelf.canDelete() {
items.append(.action(ContextMenuActionItem(text: "Delete", textColor: .destructive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { _, f in
f(.default)
if let strongSelf = self {
strongSelf.footerContentNode.deleteButtonPressed()
}
})))
}
return items
}
}
private func contextMenuSpeedItems() -> Signal<[ContextMenuItem], NoError> {
guard let videoNode = self.videoNode else {
return .single([])
}
return videoNode.status
|> take(1)
|> deliverOnMainQueue
|> map { [weak self] status -> [ContextMenuItem] in
guard let status = status, let strongSelf = self else {
return []
}
var items: [ContextMenuItem] = []
for (text, _, rate) in strongSelf.speedList() {
let isSelected = abs(status.baseRate - rate) < 0.01
items.append(.action(ContextMenuActionItem(text: text, icon: { theme in
if isSelected {
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor)
} else {
return nil
}
}, action: { _, f in
f(.default)
guard let strongSelf = self, let videoNode = strongSelf.videoNode else {
return
}
videoNode.setBaseRate(rate)
})))
}
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)
}, action: { c, _ in
guard let strongSelf = self else {
c.dismiss(completion: nil)
return
}
c.setItems(strongSelf.contextMenuMainItems())
})))
return items
}
}
@objc func openStickersButtonPressed() {
if let content = self.item?.content as? NativeVideoContent {
let media = content.fileReference.abstract
@ -1665,6 +2174,20 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
}
override func footerContent() -> Signal<(GalleryFooterContentNode?, GalleryOverlayContentNode?), NoError> {
return .single((self.footerContentNode, self.overlayContentNode))
return .single((self.footerContentNode, nil))
}
}
private final class HeaderContextReferenceContentSource: ContextReferenceContentSource {
private let controller: ViewController
private let sourceNode: ContextReferenceContentNode
init(controller: ViewController, sourceNode: ContextReferenceContentNode) {
self.controller = controller
self.sourceNode = sourceNode
}
func transitionInfo() -> ContextControllerReferenceViewInfo? {
return ContextControllerReferenceViewInfo(referenceNode: self.sourceNode, contentAreaInScreenSpace: UIScreen.main.bounds)
}
}

View File

@ -438,7 +438,7 @@ public final class SecretMediaPreviewController: ViewController {
}
}
guard let item = galleryItemForEntry(context: self.context, presentationData: self.presentationData, entry: MessageHistoryEntry(message: message, isRead: false, location: nil, monthLocation: nil, attributes: MutableMessageHistoryEntryAttributes(authorIsContact: false)), streamVideos: false, hideControls: true, tempFilePath: tempFilePath, playbackCompleted: { [weak self] in
guard let item = galleryItemForEntry(context: self.context, presentationData: self.presentationData, entry: MessageHistoryEntry(message: message, isRead: false, location: nil, monthLocation: nil, attributes: MutableMessageHistoryEntryAttributes(authorIsContact: false)), streamVideos: false, hideControls: true, isSecret: true, tempFilePath: tempFilePath, playbackCompleted: { [weak self] in
self?.dismiss(forceAway: false)
}, present: { _, _ in }) else {
self._ready.set(.single(true))

View File

@ -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, nil), caption: caption, credit: credit, fromPlayingVideo: fromPlayingVideo, landscape: landscape, performAction: { _ in }, openActionOptions: { _ in }, storeMediaPlaybackState: { _, _ in }, present: { _, _ 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, nil), 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)
@ -135,12 +135,12 @@ public struct InstantPageGalleryEntry: Equatable {
present(gallery, InstantPageGalleryControllerPresentationArguments(transitionArguments: { entry -> GalleryTransitionArguments? in
return makeArguments()
}))
}), caption: NSAttributedString(string: ""), fromPlayingVideo: fromPlayingVideo, landscape: landscape, performAction: { _ in }, openActionOptions: { _ in }, storeMediaPlaybackState: { _, _ in }, present: { _, _ in })
}), caption: NSAttributedString(string: ""), fromPlayingVideo: fromPlayingVideo, landscape: landscape, performAction: { _ in }, openActionOptions: { _, _ in }, storeMediaPlaybackState: { _, _, _ in }, present: { _, _ in })
} else {
if let content = WebEmbedVideoContent(webPage: embedWebpage, webpageContent: webpageContent, openUrl: { url in
}) {
return UniversalVideoGalleryItem(context: context, presentationData: presentationData, content: content, originData: nil, indexData: nil, contentInfo: .webPage(webPage, embedWebpage, nil), caption: NSAttributedString(string: ""), fromPlayingVideo: fromPlayingVideo, landscape: landscape, performAction: { _ in }, openActionOptions: { _ in }, storeMediaPlaybackState: { _, _ in }, present: { _, _ in })
return UniversalVideoGalleryItem(context: context, presentationData: presentationData, content: content, originData: nil, indexData: nil, contentInfo: .webPage(webPage, embedWebpage, nil), caption: NSAttributedString(string: ""), fromPlayingVideo: fromPlayingVideo, landscape: landscape, performAction: { _ in }, openActionOptions: { _, _ in }, storeMediaPlaybackState: { _, _, _ in }, present: { _, _ in })
} else {
preconditionFailure()
}

View File

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

View File

@ -0,0 +1,9 @@
{
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"provides-namespace" : true
}
}

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
"author" : "xcode",
"version" : 1
}
}

View File

@ -80,7 +80,6 @@
#import <LegacyComponents/TGCameraMainView.h>
#import <LegacyComponents/TGCameraModeControl.h>
#import <LegacyComponents/TGCameraPreviewView.h>
#import <LegacyComponents/TGCameraSegmentsView.h>
#import <LegacyComponents/TGCameraShutterButton.h>
#import <LegacyComponents/TGCameraTimeCodeView.h>
#import <LegacyComponents/TGCameraZoomView.h>

View File

@ -26,7 +26,8 @@ typedef enum
PGCameraModeVideo,
PGCameraModeSquarePhoto,
PGCameraModeSquareVideo,
PGCameraModeSquareSwing
PGCameraModeSquareSwing,
PGCameraModePhotoScan
} PGCameraMode;
typedef enum
@ -132,4 +133,7 @@ typedef enum
+ (PGCameraAuthorizationStatus)cameraAuthorizationStatus;
+ (PGMicrophoneAuthorizationStatus)microphoneAuthorizationStatus;
+ (bool)isPhotoCameraMode:(PGCameraMode)mode;
+ (bool)isVideoCameraMode:(PGCameraMode)mode;
@end

View File

@ -3,6 +3,7 @@
#import <LegacyComponents/PGCamera.h>
@class PGCameraMovieWriter;
@class PGRectangleDetector;
@interface PGCameraCaptureSession : AVCaptureSession
@ -12,6 +13,7 @@
@property (nonatomic, readonly) AVCaptureAudioDataOutput *audioOutput;
@property (nonatomic, readonly) AVCaptureMetadataOutput *metadataOutput;
@property (nonatomic, readonly) PGCameraMovieWriter *movieWriter;
@property (nonatomic, readonly) PGRectangleDetector *rectangleDetector;
@property (nonatomic, assign) bool alwaysSetFlash;
@property (nonatomic, assign) PGCameraMode currentMode;

View File

@ -1,9 +1,12 @@
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
@class PGRectangle;
@interface PGCameraShotMetadata : NSObject
@property (nonatomic, assign) CGFloat deviceAngle;
@property (nonatomic, strong) PGRectangle *rectangle;
+ (CGFloat)relativeDeviceAngleFromAngle:(CGFloat)angle orientation:(UIInterfaceOrientation)orientation;

View File

@ -1,9 +1,16 @@
#import <LegacyComponents/TGMediaEditingContext.h>
@class TGPaintingData;
@class PGRectangle;
@interface PGPhotoEditorValues : NSObject <TGMediaEditAdjustments>
@property (nonatomic, readonly) PGRectangle *cropRectangle;
@property (nonatomic, readonly) CGSize cropSize;
@property (nonatomic, readonly) bool enhanceDocument;
+ (instancetype)editorValuesWithOriginalSize:(CGSize)originalSize cropRectangle:(PGRectangle *)cropRectangle cropOrientation:(UIImageOrientation)cropOrientation cropSize:(CGSize)cropSize enhanceDocument:(bool)enhanceDocument paintingData:(TGPaintingData *)paintingData;
+ (instancetype)editorValuesWithOriginalSize:(CGSize)originalSize cropRect:(CGRect)cropRect cropRotation:(CGFloat)cropRotation cropOrientation:(UIImageOrientation)cropOrientation cropLockedAspectRatio:(CGFloat)cropLockedAspectRatio cropMirrored:(bool)cropMirrored toolValues:(NSDictionary *)toolValues paintingData:(TGPaintingData *)paintingData sendAsGif:(bool)sendAsGif;
@end

View File

@ -3,15 +3,19 @@
#import <LegacyComponents/TGMediaSelectionContext.h>
@class PGCameraShotMetadata;
@class PGRectangle;
@interface TGCameraCapturedPhoto : NSObject <TGMediaEditableItem, TGMediaSelectableItem>
@property (nonatomic, readonly) NSURL *url;
@property (nonatomic, readonly) PGCameraShotMetadata *metadata;
@property (nonatomic, readonly) PGRectangle *rectangle;
- (instancetype)initWithImage:(UIImage *)image metadata:(PGCameraShotMetadata *)metadata;
- (instancetype)initWithExistingImage:(UIImage *)image;
- (instancetype)initWithImage:(UIImage *)image rectangle:(PGRectangle *)rectangle;
- (void)_cleanUp;
@end

View File

@ -2,7 +2,12 @@
@interface TGCameraFlipButton : TGModernButton
- (instancetype)initWithFrame:(CGRect)frame large:(bool)large;
- (void)setHidden:(bool)hidden animated:(bool)animated;
@end
@interface TGCameraCancelButton : TGModernButton
- (void)setHidden:(bool)hidden animated:(bool)animated;
@end

View File

@ -10,7 +10,9 @@
+ (UIColor *)panelBackgroundColor;
+ (UIColor *)transparentPanelBackgroundColor;
+ (UIColor *)transparentOverlayBackgroundColor;
+ (UIColor *)buttonColor;
+ (UIFont *)normalFontOfSize:(CGFloat)size;
+ (UIFont *)regularFontOfSize:(CGFloat)size;
+ (UIFont *)boldFontOfSize:(CGFloat)size;
@end

View File

@ -10,7 +10,7 @@
@class TGCameraFlipButton;
@class TGCameraTimeCodeView;
@class TGCameraZoomView;
@class TGCameraSegmentsView;
@class TGCameraToastView;
@class TGMediaPickerPhotoCounterButton;
@class TGMediaPickerPhotoStripView;
@class TGMediaPickerGallerySelectedItemsModel;
@ -26,6 +26,8 @@
TGCameraFlipButton *_flipButton;
TGCameraTimeCodeView *_timecodeView;
TGCameraToastView *_toastView;
TGMediaPickerPhotoCounterButton *_photoCounterButton;
TGMediaPickerPhotoStripView *_selectedPhotosView;
@ -51,8 +53,6 @@
@property (nonatomic, copy) void(^resultPressed)(NSInteger index);
@property (nonatomic, copy) void(^itemRemoved)(NSInteger index);
@property (nonatomic, copy) void (^deleteSegmentButtonPressed)(void);
@property (nonatomic, copy) NSTimeInterval(^requestedVideoRecordingDuration)(void);
@property (nonatomic, assign) CGRect previewViewFrame;
@ -64,6 +64,8 @@
- (void)updateForCameraModeChangeWithPreviousMode:(PGCameraMode)previousMode;
- (void)updateForCameraModeChangeAfterResize;
- (void)setToastMessage:(NSString *)message animated:(bool)animated;
- (void)setFlashMode:(PGCameraFlashMode)mode;
- (void)setFlashActive:(bool)active;
- (void)setFlashUnavailable:(bool)unavailable;

View File

@ -1,20 +0,0 @@
#import <UIKit/UIKit.h>
@interface TGCameraSegmentsView : UIView
@property (nonatomic, copy) void (^deletePressed)(void);
- (void)setSegments:(NSArray *)segments;
- (void)startCurrentSegment;
- (void)setCurrentSegment:(CGFloat)length;
- (void)commitCurrentSegmentWithCompletion:(void (^)(void))completion;
- (void)highlightLastSegment;
- (void)removeLastSegment;
- (void)setHidden:(bool)hidden animated:(bool)animated delay:(NSTimeInterval)delay;
- (void)setDeleteButtonHidden:(bool)hidden animated:(bool)animated;
@end

View File

@ -843,4 +843,14 @@ NSString *const PGCameraAdjustingFocusKey = @"adjustingFocus";
}
}
+ (bool)isPhotoCameraMode:(PGCameraMode)mode
{
return mode == PGCameraModePhoto || mode == PGCameraModeSquarePhoto || mode == PGCameraModePhotoScan;
}
+ (bool)isVideoCameraMode:(PGCameraMode)mode
{
return mode == PGCameraModeVideo || mode == PGCameraModeSquareVideo;
}
@end

View File

@ -1,5 +1,6 @@
#import "PGCameraCaptureSession.h"
#import "PGCameraMovieWriter.h"
#import "PGRectangleDetector.h"
#import <LegacyComponents/LegacyComponentsGlobals.h>
#import <LegacyComponents/TGPhotoEditorUtils.h>
@ -261,10 +262,15 @@ const NSInteger PGCameraFrameRate = 30;
{
case PGCameraModePhoto:
case PGCameraModeSquarePhoto:
case PGCameraModePhotoScan:
{
[self _removeAudioInputEndAudioSession:true];
self.sessionPreset = AVCaptureSessionPresetPhoto;
[self setFrameRate:0 forDevice:_videoDevice];
if (mode == PGCameraModePhotoScan) {
[self setCurrentCameraPosition:PGCameraPositionRear];
}
}
break;
@ -287,6 +293,14 @@ const NSInteger PGCameraFrameRate = 30;
[self _enableVideoStabilization];
[self commitConfiguration];
if (mode == PGCameraModePhotoScan) {
if (_rectangleDetector == nil) {
_rectangleDetector = [[PGRectangleDetector alloc] init];
}
} else {
_rectangleDetector = nil;
}
}
- (void)switchToBestVideoFormatForDevice:(AVCaptureDevice *)device
@ -630,9 +644,11 @@ const NSInteger PGCameraFrameRate = 30;
- (void)setCurrentCameraPosition:(PGCameraPosition)position
{
NSError *error;
AVCaptureDevice *deviceForTargetPosition = [PGCameraCaptureSession _deviceWithCameraPosition:position];
if ([_videoDevice isEqual:deviceForTargetPosition])
return;
NSError *error;
AVCaptureDeviceInput *newVideoInput = [[AVCaptureDeviceInput alloc] initWithDevice:deviceForTargetPosition error:&error];
if (newVideoInput != nil)
@ -924,6 +940,11 @@ static UIImageOrientation TGSnapshotOrientationForVideoOrientation(bool mirrored
if (_movieWriter.isRecording)
[_movieWriter _processSampleBuffer:sampleBuffer];
if (_rectangleDetector != nil) {
CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
[_rectangleDetector detectRectangle:imageBuffer];
}
if (!_captureNextFrame || captureOutput != _videoOutput)
return;

View File

@ -15,6 +15,20 @@
@synthesize sendAsGif = _sendAsGif;
@synthesize toolValues = _toolValues;
+ (instancetype)editorValuesWithOriginalSize:(CGSize)originalSize cropRectangle:(PGRectangle *)cropRectangle cropOrientation:(UIImageOrientation)cropOrientation cropSize:(CGSize)cropSize enhanceDocument:(bool)enhanceDocument paintingData:(TGPaintingData *)paintingData
{
PGPhotoEditorValues *values = [[PGPhotoEditorValues alloc] init];
values->_originalSize = originalSize;
values->_cropRect = CGRectMake(0.0, 0.0, cropSize.width, cropSize.height);
values->_cropSize = cropSize;
values->_cropRectangle = cropRectangle;
values->_cropOrientation = cropOrientation;
values->_enhanceDocument = enhanceDocument;
values->_paintingData = paintingData;
return values;
}
+ (instancetype)editorValuesWithOriginalSize:(CGSize)originalSize cropRect:(CGRect)cropRect cropRotation:(CGFloat)cropRotation cropOrientation:(UIImageOrientation)cropOrientation cropLockedAspectRatio:(CGFloat)cropLockedAspectRatio cropMirrored:(bool)cropMirrored toolValues:(NSDictionary *)toolValues paintingData:(TGPaintingData *)paintingData sendAsGif:(bool)sendAsGif
{
PGPhotoEditorValues *values = [[PGPhotoEditorValues alloc] init];
@ -27,7 +41,6 @@
values->_toolValues = toolValues;
values->_paintingData = paintingData;
values->_sendAsGif = sendAsGif;
return values;
}
@ -38,6 +51,9 @@
- (bool)cropAppliedForAvatar:(bool)forAvatar
{
if (_cropRectangle != nil)
return true;
CGRect defaultCropRect = CGRectMake(0, 0, _originalSize.width, _originalSize.height);
if (forAvatar)
{
@ -65,6 +81,9 @@
- (bool)toolsApplied
{
if (_enhanceDocument)
return true;
if (self.toolValues.count > 0)
return true;
@ -112,6 +131,9 @@
if (self.paintingData != values.paintingData && ![self.paintingData isEqual:values.paintingData])
return false;
if (self.enhanceDocument != values.enhanceDocument)
return false;
return true;
}

View File

@ -0,0 +1,24 @@
#import <UIKit/UIKit.h>
@interface PGRectangle : NSObject
@property (nonatomic, readonly) CGPoint topLeft;
@property (nonatomic, readonly) CGPoint topRight;
@property (nonatomic, readonly) CGPoint bottomLeft;
@property (nonatomic, readonly) CGPoint bottomRight;
- (PGRectangle *)transform:(CGAffineTransform)transform;
- (PGRectangle *)rotate90;
- (PGRectangle *)sort;
- (PGRectangle *)cartesian:(CGFloat)height;
@end
@interface PGRectangleDetector : NSObject
@property (nonatomic, copy) void(^update)(bool, PGRectangle *);
- (void)detectRectangle:(CVPixelBufferRef)pixelBuffer;
@end

View File

@ -0,0 +1,362 @@
#import "PGRectangleDetector.h"
#import "LegacyComponentsInternal.h"
#import <Vision/Vision.h>
#import <CoreImage/CoreImage.h>
#import <SSignalKit/SSignalKit.h>
@interface PGRectangle ()
- (instancetype)initWithRectangleFeature:(CIRectangleFeature *)rectangleFeature;
- (instancetype)initWithRectangleObservation:(VNRectangleObservation *)rectangleObservation API_AVAILABLE(ios(11.0));
- (CGFloat)size;
@end
@interface PGRectangleEntry : NSObject
@property (nonatomic, readonly) PGRectangle *rectangle;
@property (nonatomic, assign) NSInteger rate;
- (instancetype)initWithRectangle:(PGRectangle *)rectangle;
@end
@implementation PGRectangleEntry
- (instancetype)initWithRectangle:(PGRectangle *)rectangle
{
self = [super init];
if (self != nil)
{
_rectangle = rectangle;
_rate = 0;
}
return self;
}
@end
@implementation PGRectangle
- (instancetype)initWithRectangleFeature:(CIRectangleFeature *)rectangleFeature
{
self = [super init];
if (self != nil) {
_topLeft = rectangleFeature.topLeft;
_topRight = rectangleFeature.topRight;
_bottomLeft = rectangleFeature.bottomLeft;
_bottomRight = rectangleFeature.bottomRight;
}
return self;
}
- (instancetype)initWithRectangleObservation:(VNRectangleObservation *)rectangleObservation API_AVAILABLE(ios(11.0))
{
self = [super init];
if (self != nil) {
_topLeft = rectangleObservation.topLeft;
_topRight = rectangleObservation.topRight;
_bottomLeft = rectangleObservation.bottomLeft;
_bottomRight = rectangleObservation.bottomRight;
}
return self;
}
- (PGRectangle *)transform:(CGAffineTransform)transform
{
PGRectangle *rectangle = [[PGRectangle alloc] init];
rectangle->_topLeft = CGPointApplyAffineTransform(_topLeft, transform);
rectangle->_topRight = CGPointApplyAffineTransform(_topRight, transform);
rectangle->_bottomLeft = CGPointApplyAffineTransform(_bottomLeft, transform);
rectangle->_bottomRight = CGPointApplyAffineTransform(_bottomRight, transform);
return rectangle;
}
- (PGRectangle *)rotate90
{
PGRectangle *rectangle = [[PGRectangle alloc] init];
rectangle->_topLeft = CGPointMake(_topLeft.y, _topLeft.x);
rectangle->_topRight = CGPointMake(_topRight.y, _topRight.x);
rectangle->_bottomLeft = CGPointMake(_bottomLeft.y, _bottomLeft.x);
rectangle->_bottomRight = CGPointMake(_bottomRight.y, _bottomRight.x);
return rectangle;
}
- (PGRectangle *)sort
{
NSArray *points = @[ [NSValue valueWithCGPoint:_topLeft], [NSValue valueWithCGPoint:_topRight], [NSValue valueWithCGPoint:_bottomLeft], [NSValue valueWithCGPoint:_bottomRight] ];
NSArray *ySorted = [points sortedArrayUsingComparator:^NSComparisonResult(id firstObject, id secondObject) {
CGPoint firstPoint = [firstObject CGPointValue];
CGPoint secondPoint = [secondObject CGPointValue];
if (firstPoint.y < secondPoint.y) {
return NSOrderedAscending;
} else {
return NSOrderedDescending;
}
}];
NSArray *top = [ySorted subarrayWithRange:NSMakeRange(0, 2)];
NSArray *bottom = [ySorted subarrayWithRange:NSMakeRange(2, 2)];
NSArray *xSortedTop = [top sortedArrayUsingComparator:^NSComparisonResult(id firstObject, id secondObject) {
CGPoint firstPoint = [firstObject CGPointValue];
CGPoint secondPoint = [secondObject CGPointValue];
if (firstPoint.x < secondPoint.x) {
return NSOrderedAscending;
} else {
return NSOrderedDescending;
}
}];
NSArray *xSortedBottom = [bottom sortedArrayUsingComparator:^NSComparisonResult(id firstObject, id secondObject) {
CGPoint firstPoint = [firstObject CGPointValue];
CGPoint secondPoint = [secondObject CGPointValue];
if (firstPoint.x < secondPoint.x) {
return NSOrderedAscending;
} else {
return NSOrderedDescending;
}
}];
PGRectangle *rectangle = [[PGRectangle alloc] init];
rectangle->_topLeft = [xSortedTop[0] CGPointValue];
rectangle->_topRight = [xSortedTop[1] CGPointValue];
rectangle->_bottomLeft = [xSortedBottom[0] CGPointValue];
rectangle->_bottomRight = [xSortedBottom[1] CGPointValue];
return rectangle;
}
- (PGRectangle *)cartesian:(CGFloat)height
{
PGRectangle *rectangle = [[PGRectangle alloc] init];
rectangle->_topLeft = CGPointMake(_topLeft.x, height - _topLeft.y);
rectangle->_topRight = CGPointMake(_topRight.x, height - _topRight.y);
rectangle->_bottomLeft = CGPointMake(_bottomLeft.x, height - _bottomLeft.y);
rectangle->_bottomRight = CGPointMake(_bottomRight.x, height - _bottomRight.y);
return rectangle;
}
- (PGRectangle *)normalize:(CGSize)size
{
return [self transform:CGAffineTransformMakeScale(1.0 / size.width, 1.0 / size.height)];
}
+ (CGFloat)distance:(CGPoint)a to:(CGPoint)b
{
return hypot(a.x - b.x, a.y - b.y);
}
- (CGFloat)size
{
CGFloat sum = 0.0f;
sum += [PGRectangle distance:self.topLeft to:self.topRight];
sum += [PGRectangle distance:self.topRight to:self.bottomRight];
sum += [PGRectangle distance:self.bottomRight to:self.bottomLeft];
sum += [PGRectangle distance:self.bottomLeft to:self.topLeft];
return sum;
}
+ (CGRect)pointSquare:(CGPoint)point size:(CGFloat)size
{
return CGRectMake(point.x - size / 2.0, point.y - size / 2.0, size, size);
}
- (bool)matches:(PGRectangle *)other threshold:(CGFloat)threshold
{
if (!CGRectContainsPoint([PGRectangle pointSquare:self.topLeft size:threshold], other.topLeft))
return false;
if (!CGRectContainsPoint([PGRectangle pointSquare:self.topRight size:threshold], other.topRight))
return false;
if (!CGRectContainsPoint([PGRectangle pointSquare:self.bottomLeft size:threshold], other.bottomLeft))
return false;
if (!CGRectContainsPoint([PGRectangle pointSquare:self.bottomRight size:threshold], other.bottomRight))
return false;
return true;
}
@end
@implementation PGRectangleDetector
{
SQueue *_queue;
CIDetector *_detector;
bool _disabled;
CGSize _imageSize;
NSInteger _notFoundCount;
NSMutableArray *_rectangles;
PGRectangle *_detectedRectangle;
NSInteger _autoscanCount;
}
- (instancetype)init
{
self = [super init];
if (self != nil) {
_queue = [[SQueue alloc] init];
_rectangles = [[NSMutableArray alloc] init];
}
return self;
}
- (void)updateEntries
{
for (PGRectangleEntry *entry in _rectangles) {
entry.rate = 1;
}
for (NSInteger i = 0; i < _rectangles.count; i++) {
for (NSInteger j = 0; i < _rectangles.count; i++) {
if (j > i && [[_rectangles[i] rectangle] matches:_rectangles[j] threshold:40.0]) {
((PGRectangleEntry *)_rectangles[i]).rate += 1;
((PGRectangleEntry *)_rectangles[j]).rate += 1;
}
}
}
}
- (void)addRectangle:(PGRectangle *)rectangle
{
if (_disabled)
return;
PGRectangleEntry *entry = [[PGRectangleEntry alloc] initWithRectangle:rectangle];
[_rectangles addObject:entry];
if (_rectangles.count < 3)
return;
if (_rectangles.count > 8)
[_rectangles removeObjectAtIndex:0];
[self updateEntries];
__block PGRectangleEntry *best = nil;
[_rectangles enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(PGRectangleEntry *rectangle, NSUInteger idx, BOOL * stop) {
if (best == nil) {
best = rectangle;
return;
}
if (rectangle.rate > best.rate) {
best = rectangle;
} else if (rectangle.rate == best.rate) {
if (_detectedRectangle != nil) {
if ([rectangle.rectangle matches:_detectedRectangle threshold:40.0]) {
best = rectangle;
}
}
}
}];
if (_detectedRectangle != nil && [best.rectangle matches:_detectedRectangle threshold:24.0f]) {
_autoscanCount += 1;
_detectedRectangle = best.rectangle;
if (_autoscanCount > 20) {
_autoscanCount = 0;
self.update(true, [_detectedRectangle normalize:_imageSize]);
_detectedRectangle = nil;
_disabled = true;
TGDispatchAfter(2.0, _queue._dispatch_queue, ^{
_disabled = false;
});
}
} else {
_autoscanCount = 0;
_detectedRectangle = best.rectangle;
self.update(false, [_detectedRectangle normalize:_imageSize]);
}
}
- (void)processRectangle:(PGRectangle *)rectangle imageSize:(CGSize)imageSize
{
_imageSize = imageSize;
if (rectangle != nil) {
_notFoundCount = 0;
[self addRectangle:rectangle];
} else {
_notFoundCount += 1;
if (_notFoundCount > 3) {
_autoscanCount = 0;
_detectedRectangle = nil;
self.update(false, nil);
}
}
}
- (void)detectRectangle:(CVPixelBufferRef)pixelBuffer
{
CGSize size = CGSizeMake(CVPixelBufferGetWidth(pixelBuffer), CVPixelBufferGetHeight(pixelBuffer));
if (@available(iOS 11.0, *)) {
CVPixelBufferRetain(pixelBuffer);
NSError *error;
VNImageRequestHandler *handler = [[VNImageRequestHandler alloc] initWithCVPixelBuffer:pixelBuffer options:@{}];
VNDetectRectanglesRequest *request = [[VNDetectRectanglesRequest alloc] initWithCompletionHandler:^(VNRequest * _Nonnull request, NSError * _Nullable error) {
CVPixelBufferRelease(pixelBuffer);
[_queue dispatch:^{
if (error == nil && request.results.count > 0) {
PGRectangle *largestRectangle = nil;
for (VNRectangleObservation *result in request.results) {
if (![result isKindOfClass:[VNRectangleObservation class]])
continue;
PGRectangle *rectangle = [[PGRectangle alloc] initWithRectangleObservation:result];
if (largestRectangle == nil || largestRectangle.size < rectangle.size) {
largestRectangle = rectangle;
}
}
[self processRectangle:[largestRectangle transform:CGAffineTransformMakeScale(size.width, size.height)] imageSize:size];
} else {
[self processRectangle:nil imageSize:size];
}
}];
}];
request.minimumConfidence = 0.85f;
request.maximumObservations = 15;
request.minimumAspectRatio = 0.33;
request.minimumSize = 0.4;
[handler performRequests:@[request] error:&error];
} else {
CVPixelBufferRetain(pixelBuffer);
[_queue dispatch:^{
if (_detector == nil) {
_detector = [CIDetector detectorOfType:CIDetectorTypeRectangle context:[CIContext contextWithOptions:nil] options:@{ CIDetectorAccuracy: CIDetectorAccuracyHigh }];
}
CIImage *image = [[CIImage alloc] initWithCVPixelBuffer:pixelBuffer];
NSArray *results = [_detector featuresInImage:image];
CVPixelBufferRelease(pixelBuffer);
PGRectangle *largestRectangle = nil;
for (CIRectangleFeature *result in results) {
if (![result isKindOfClass:[CIRectangleFeature class]])
continue;
PGRectangle *rectangle = [[PGRectangle alloc] initWithRectangleFeature:result];
if (largestRectangle == nil || largestRectangle.size < rectangle.size) {
largestRectangle = rectangle;
}
}
[self processRectangle:largestRectangle imageSize:size];
}];
}
}
@end

View File

@ -32,6 +32,23 @@
return self;
}
- (instancetype)initWithImage:(UIImage *)image rectangle:(PGRectangle *)rectangle
{
self = [super init];
if (self != nil)
{
_identifier = [NSString stringWithFormat:@"%ld", lrand48()];
_dimensions = CGSizeMake(image.size.width * image.scale, image.size.height * image.scale);
PGCameraShotMetadata *metadata = [[PGCameraShotMetadata alloc] init];
metadata.rectangle = rectangle;
_metadata = metadata;
_thumbnail = [[SVariable alloc] init];
[self _saveToDisk:image];
}
return self;
}
- (instancetype)initWithExistingImage:(UIImage *)image
{
self = [super init];
@ -110,6 +127,11 @@
return [NSTemporaryDirectory() stringByAppendingPathComponent:[[NSString alloc] initWithFormat:@"camphoto_%@.jpg", _identifier]];
}
- (PGRectangle *)rectangle
{
return _metadata.rectangle;
}
- (NSURL *)url
{
return [NSURL fileURLWithPath:[self filePath]];

View File

@ -17,6 +17,7 @@
#import <LegacyComponents/TGCameraMainPhoneView.h>
#import <LegacyComponents/TGCameraMainTabletView.h>
#import "TGCameraFocusCrosshairsControl.h"
#import "TGCameraRectangleView.h"
#import <LegacyComponents/TGFullscreenContainerView.h>
#import <LegacyComponents/TGPhotoEditorController.h>
@ -50,6 +51,8 @@
#import "TGCameraCapturedVideo.h"
#import "PGPhotoEditor.h"
#import "PGRectangleDetector.h"
#import "TGWarpedView.h"
#import "TGAnimationUtils.h"
@ -106,9 +109,7 @@ static CGPoint TGCameraControllerClampPointToScreenSize(__unused id self, __unus
TGCameraMainView *_interfaceView;
UIView *_overlayView;
TGCameraFocusCrosshairsControl *_focusControl;
TGModernGalleryVideoView *_segmentPreviewView;
bool _previewingSegment;
TGCameraRectangleView *_rectangleView;
UISwipeGestureRecognizer *_photoSwipeGestureRecognizer;
UISwipeGestureRecognizer *_videoSwipeGestureRecognizer;
@ -281,6 +282,11 @@ static CGPoint TGCameraControllerClampPointToScreenSize(__unused id self, __unus
[_focusControl setInterfaceOrientation:interfaceOrientation animated:false];
[_overlayView addSubview:_focusControl];
_rectangleView = [[TGCameraRectangleView alloc] initWithFrame:_overlayView.bounds];
_rectangleView.previewView = _previewView;
_rectangleView.hidden = true;
[_overlayView addSubview:_rectangleView];
if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone)
{
_panGestureRecognizer = [[TGModernGalleryZoomableScrollViewSwipeGestureRecognizer alloc] initWithTarget:self action:@selector(handlePan:)];
@ -360,7 +366,7 @@ static CGPoint TGCameraControllerClampPointToScreenSize(__unused id self, __unus
if (strongSelf == nil)
return;
[strongSelf->_camera setCameraMode:mode];
[strongSelf _updateCameraMode:mode updateInterface:false];
};
_interfaceView.flashModeChanged = ^(PGCameraFlashMode mode)
@ -478,6 +484,25 @@ static CGPoint TGCameraControllerClampPointToScreenSize(__unused id self, __unus
[self _configureCamera];
}
- (void)_updateCameraMode:(PGCameraMode)mode updateInterface:(bool)updateInterface {
[_camera setCameraMode:mode];
if (updateInterface)
[_interfaceView setCameraMode:mode];
_focusControl.hidden = mode == PGCameraModePhotoScan;
_rectangleView.hidden = mode != PGCameraModePhotoScan;
if (mode == PGCameraModePhotoScan) {
[self _createContextsIfNeeded];
if (_items.count == 0) {
[_interfaceView setToastMessage:@"Position the document in view" animated:true];
} else {
}
}
}
- (void)_configureCamera
{
__weak TGCameraController *weakSelf = self;
@ -514,9 +539,11 @@ static CGPoint TGCameraControllerClampPointToScreenSize(__unused id self, __unus
strongSelf.view.userInteractionEnabled = false;
PGCameraMode currentMode = strongSelf->_camera.cameraMode;
bool generalModeNotChanged = (mode == PGCameraModePhoto && currentMode == PGCameraModeSquarePhoto) || (mode == PGCameraModeSquarePhoto && currentMode == PGCameraModePhoto) || (mode == PGCameraModeVideo && currentMode == PGCameraModeSquareVideo) || (mode == PGCameraModeSquareVideo && currentMode == PGCameraModeVideo);
if ((mode == PGCameraModeVideo || mode == PGCameraModeSquareVideo) && !generalModeNotChanged)
bool generalModeNotChanged = [PGCamera isPhotoCameraMode:mode] == [PGCamera isPhotoCameraMode:currentMode];
if (strongSelf->_camera.captureSession.currentCameraPosition == PGCameraPositionFront && mode == PGCameraModePhotoScan) {
generalModeNotChanged = false;
}
if ([PGCamera isVideoCameraMode:mode] && !generalModeNotChanged)
{
[[LegacyComponentsGlobals provider] pauseMusicPlayback];
}
@ -570,6 +597,21 @@ static CGPoint TGCameraControllerClampPointToScreenSize(__unused id self, __unus
[[[LegacyComponentsGlobals provider] accessChecker] checkMicrophoneAuthorizationStatusForIntent:TGMicrophoneAccessIntentVideo alertDismissCompletion:nil];
strongSelf->_shownMicrophoneAlert = true;
}
if (strongSelf->_camera.cameraMode == PGCameraModePhotoScan) {
strongSelf->_camera.captureSession.rectangleDetector.update = ^(bool capture, PGRectangle *rectangle) {
__strong TGCameraController *strongSelf = weakSelf;
if (strongSelf == nil)
return;
TGDispatchOnMainThread(^{
[strongSelf->_rectangleView drawRectangle:rectangle];
if (capture) {
[strongSelf _makeScan:rectangle];
}
});
};
}
}
});
};
@ -874,9 +916,7 @@ static CGPoint TGCameraControllerClampPointToScreenSize(__unused id self, __unus
}];
};
_camera.autoStartVideoRecording = true;
[_camera setCameraMode:PGCameraModeVideo];
[_interfaceView setCameraMode:PGCameraModeVideo];
[self _updateCameraMode:PGCameraModeVideo updateInterface:true];
}
else if (_camera.cameraMode == PGCameraModeVideo)
{
@ -968,7 +1008,7 @@ static CGPoint TGCameraControllerClampPointToScreenSize(__unused id self, __unus
__weak TGCameraController *weakSelf = self;
PGCameraMode cameraMode = _camera.cameraMode;
if (cameraMode == PGCameraModePhoto || cameraMode == PGCameraModeSquarePhoto)
if (cameraMode == PGCameraModePhoto || cameraMode == PGCameraModeSquarePhoto || cameraMode == PGCameraModePhotoScan)
{
_camera.disabled = true;
@ -1060,6 +1100,140 @@ static CGPoint TGCameraControllerClampPointToScreenSize(__unused id self, __unus
}
}
- (void)_makeScan:(PGRectangle *)rectangle
{
if (_shutterIsBusy)
return;
_camera.disabled = true;
_shutterIsBusy = true;
__weak TGCameraController *weakSelf = self;
[_camera takePhotoWithCompletion:^(UIImage *result, PGCameraShotMetadata *metadata)
{
__strong TGCameraController *strongSelf = weakSelf;
if (strongSelf == nil)
return;
TGDispatchOnMainThread(^
{
[strongSelf->_interfaceView setToastMessage:nil animated:true];
strongSelf->_shutterIsBusy = false;
[strongSelf->_rectangleView drawRectangle:nil];
strongSelf->_rectangleView.enabled = false;
TGDispatchAfter(2.0, dispatch_get_main_queue(), ^{
strongSelf->_rectangleView.enabled = true;
});
TGCameraCapturedPhoto *capturedPhoto = [[TGCameraCapturedPhoto alloc] initWithImage:result rectangle:rectangle];
[strongSelf addResultItem:capturedPhoto];
PGRectangle *cropRectangle = [[rectangle rotate90] transform:CGAffineTransformMakeScale(result.size.width, result.size.height)];
PGRectangle *convertedRectangle = [cropRectangle sort];
convertedRectangle = [convertedRectangle cartesian:result.size.height];
CIImage *ciImage = [[CIImage alloc] initWithImage:result];
CIImage *croppedImage = [ciImage imageByApplyingFilter:@"CIPerspectiveCorrection" withInputParameters:@{
@"inputTopLeft": [CIVector vectorWithCGPoint:convertedRectangle.topLeft],
@"inputTopRight": [CIVector vectorWithCGPoint:convertedRectangle.topRight],
@"inputBottomLeft": [CIVector vectorWithCGPoint:convertedRectangle.bottomLeft],
@"inputBottomRight": [CIVector vectorWithCGPoint:convertedRectangle.bottomRight]
}];
CIImage *enhancedImage = [croppedImage imageByApplyingFilter:@"CIDocumentEnhancer" withInputParameters:@{}];
CIContext *context = [CIContext contextWithOptions:nil];
UIImage *editedImage = [UIImage imageWithCGImage:[context createCGImage:enhancedImage fromRect:enhancedImage.extent]];
UIImage *thumbnailImage = TGScaleImage(editedImage, TGScaleToFillSize(editedImage.size, TGPhotoThumbnailSizeForCurrentScreen()));
[strongSelf->_editingContext setImage:editedImage thumbnailImage:thumbnailImage forItem:capturedPhoto synchronous:true];
[strongSelf->_editingContext setAdjustments:[PGPhotoEditorValues editorValuesWithOriginalSize:result.size cropRectangle:cropRectangle cropOrientation:UIImageOrientationUp cropSize:editedImage.size enhanceDocument:true paintingData:nil] forItem:capturedPhoto];
[strongSelf _playScanAnimation:editedImage rectangle:rectangle completion:^{
[strongSelf->_selectedItemsModel addSelectedItem:capturedPhoto];
[strongSelf->_selectionContext setItem:capturedPhoto selected:true];
[strongSelf->_interfaceView setResults:[strongSelf->_items copy]];
TGDispatchAfter(0.5, dispatch_get_main_queue(), ^{
[strongSelf->_interfaceView setToastMessage:@"Ready for next scan" animated:true];
});
}];
strongSelf->_camera.disabled = false;
});
}];
}
- (void)_playScanAnimation:(UIImage *)image rectangle:(PGRectangle *)rectangle completion:(void(^)(void))completion
{
TGWarpedView *warpedView = [[TGWarpedView alloc] initWithImage:image];
warpedView.layer.anchorPoint = CGPointMake(0, 0);
warpedView.frame = _rectangleView.frame;
[_rectangleView.superview addSubview:warpedView];
CGAffineTransform transform = CGAffineTransformMakeScale(_previewView.frame.size.width, _previewView.frame.size.height);
PGRectangle *displayRectangle = [[[rectangle rotate90] transform:transform] sort];
[warpedView transformToFitQuadTopLeft:displayRectangle.topLeft topRight:displayRectangle.topRight bottomLeft:displayRectangle.bottomLeft bottomRight:displayRectangle.bottomRight];
CGFloat inset = 16.0f;
CGSize targetSize = TGScaleToFit(image.size, CGSizeMake(_previewView.frame.size.width - inset * 2.0, _previewView.frame.size.height - inset * 2.0));
CGRect targetRect = CGRectMake(floor((_previewView.frame.size.width - targetSize.width) / 2.0), floor((_previewView.frame.size.height - targetSize.height) / 2.0), targetSize.width, targetSize.height);
[UIView animateWithDuration:0.3 delay:0.0 options:(7 << 16) animations:^{
[warpedView transformToFitQuadTopLeft:CGPointMake(targetRect.origin.x, targetRect.origin.y) topRight:CGPointMake(targetRect.origin.x + targetRect.size.width, targetRect.origin.y) bottomLeft:CGPointMake(targetRect.origin.x, targetRect.origin.y + targetRect.size.height) bottomRight:CGPointMake(targetRect.origin.x + targetRect.size.width, targetRect.origin.y + targetRect.size.height)];
} completion:^(BOOL finished) {
UIImageView *outView = [[UIImageView alloc] initWithImage:image];
outView.frame = targetRect;
[warpedView.superview addSubview:outView];
[warpedView removeFromSuperview];
TGDispatchAfter(0.2, dispatch_get_main_queue(), ^{
CGPoint sourcePoint = outView.center;
CGPoint targetPoint = CGPointMake(_previewView.frame.size.width - 44.0, _previewView.frame.size.height - 44.0);
CGPoint midPoint = CGPointMake((sourcePoint.x + targetPoint.x) / 2.0, sourcePoint.y - 30.0);
CGFloat x1 = sourcePoint.x;
CGFloat y1 = sourcePoint.y;
CGFloat x2 = midPoint.x;
CGFloat y2 = midPoint.y;
CGFloat x3 = targetPoint.x;
CGFloat y3 = targetPoint.y;
CGFloat a = (x3 * (y2 - y1) + x2 * (y1 - y3) + x1 * (y3 - y2)) / ((x1 - x2) * (x1 - x3) * (x2 - x3));
CGFloat b = (x1 * x1 * (y2 - y3) + x3 * x3 * (y1 - y2) + x2 * x2 * (y3 - y1)) / ((x1 - x2) * (x1 - x3) * (x2 - x3));
CGFloat c = (x2 * x2 * (x3 * y1 - x1 * y3) + x2 * (x1 * x1 * y3 - x3 * x3 * y1) + x1 * x3 * (x3 - x1) * y2) / ((x1 - x2) * (x1 - x3) * (x2 - x3));
[UIView animateWithDuration:0.3 animations:^{
outView.transform = CGAffineTransformMakeScale(0.1, 0.1);
} completion:^(BOOL finished) {
[outView removeFromSuperview];
}];
TGDispatchAfter(0.28, dispatch_get_main_queue(), ^{
completion();
});
CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"position"];
NSMutableArray *values = [[NSMutableArray alloc] init];
NSMutableArray *keyTimes = [[NSMutableArray alloc] init];
for (NSInteger i = 0; i < 10; i++) {
CGFloat k = (CGFloat)i / (CGFloat)(10 - 1);
CGFloat x = sourcePoint.x * (1.0 - k) + targetPoint.x * k;
CGFloat y = a * x * x + b * x + c;
[values addObject:[NSValue valueWithCGPoint:CGPointMake(x, y)]];
[keyTimes addObject:@(k)];
}
animation.values = values;
animation.keyTimes = keyTimes;
animation.duration = 0.35;
animation.removedOnCompletion = false;
[outView.layer addAnimation:animation forKey:@"position"];
});
}];
}
- (void)cancelPressed
{
if (_items.count > 0)
@ -1182,27 +1356,34 @@ static CGPoint TGCameraControllerClampPointToScreenSize(__unused id self, __unus
return galleryItems;
}
- (void)_createContextsIfNeeded
{
TGMediaEditingContext *editingContext = _editingContext;
if (editingContext == nil)
{
editingContext = [[TGMediaEditingContext alloc] init];
if (self.forcedCaption != nil)
[editingContext setForcedCaption:self.forcedCaption entities:self.forcedEntities];
_editingContext = editingContext;
_interfaceView.editingContext = editingContext;
}
TGMediaSelectionContext *selectionContext = _selectionContext;
if (selectionContext == nil)
{
selectionContext = [[TGMediaSelectionContext alloc] initWithGroupingAllowed:self.allowGrouping selectionLimit:100];
if (self.allowGrouping)
selectionContext.grouping = true;
_selectionContext = selectionContext;
}
}
- (void)presentResultControllerForItem:(id<TGMediaEditableItem, TGMediaSelectableItem>)editableItemValue completion:(void (^)(void))completion
{
__block id<TGMediaEditableItem, TGMediaSelectableItem> editableItem = editableItemValue;
UIViewController *(^begin)(id<LegacyComponentsContext>) = ^(id<LegacyComponentsContext> windowContext) {
[self _createContextsIfNeeded];
TGMediaEditingContext *editingContext = _editingContext;
if (editingContext == nil)
{
editingContext = [[TGMediaEditingContext alloc] init];
if (self.forcedCaption != nil)
[editingContext setForcedCaption:self.forcedCaption entities:self.forcedEntities];
_editingContext = editingContext;
_interfaceView.editingContext = editingContext;
}
TGMediaSelectionContext *selectionContext = _selectionContext;
if (selectionContext == nil)
{
selectionContext = [[TGMediaSelectionContext alloc] initWithGroupingAllowed:self.allowGrouping selectionLimit:100];
if (self.allowGrouping)
selectionContext.grouping = true;
_selectionContext = selectionContext;
}
if (editableItem == nil)
editableItem = _items.lastObject;
@ -1989,6 +2170,7 @@ static CGPoint TGCameraControllerClampPointToScreenSize(__unused id self, __unus
self.view.userInteractionEnabled = false;
_focusControl.active = false;
_rectangleView.hidden = true;
[UIView animateWithDuration:0.3f animations:^
{
@ -2256,21 +2438,27 @@ static CGPoint TGCameraControllerClampPointToScreenSize(__unused id self, __unus
PGCameraMode newMode = PGCameraModeUndefined;
if (gestureRecognizer == _photoSwipeGestureRecognizer)
{
newMode = PGCameraModePhoto;
if (_camera.cameraMode == PGCameraModePhoto && _intent == TGCameraControllerGenericIntent)
newMode = PGCameraModePhotoScan;
else if (_camera.cameraMode != PGCameraModePhotoScan)
newMode = PGCameraModePhoto;
}
else if (gestureRecognizer == _videoSwipeGestureRecognizer)
{
if (_intent == TGCameraControllerAvatarIntent) {
newMode = PGCameraModeSquareVideo;
if (_camera.cameraMode == PGCameraModePhotoScan) {
if (_items.count == 0)
newMode = PGCameraModePhoto;
} else {
newMode = PGCameraModeVideo;
if (_intent == TGCameraControllerAvatarIntent) {
newMode = PGCameraModeSquareVideo;
} else {
newMode = PGCameraModeVideo;
}
}
}
if (newMode != PGCameraModeUndefined && _camera.cameraMode != newMode)
{
[_camera setCameraMode:newMode];
[_interfaceView setCameraMode:newMode];
if (newMode != PGCameraModeUndefined && _camera.cameraMode != newMode) {
[self _updateCameraMode:newMode updateInterface:true];
}
}
@ -2403,6 +2591,8 @@ static CGPoint TGCameraControllerClampPointToScreenSize(__unused id self, __unus
{
if (widescreenWidth == 896.0f)
return CGRectMake(0, 121, screenSize.width, screenSize.height - 121 - 223);
else if (widescreenWidth == 844.0f)
return CGRectMake(0, 77, screenSize.width, screenSize.height - 77 - 191);
else if (widescreenWidth == 812.0f)
return CGRectMake(0, 121, screenSize.width, screenSize.height - 121 - 191);
else if (widescreenWidth >= 736.0f - FLT_EPSILON)
@ -2459,11 +2649,22 @@ static CGPoint TGCameraControllerClampPointToScreenSize(__unused id self, __unus
if (selectedItems.count == 0 && currentItem != nil)
[selectedItems addObject:currentItem];
if (storeAssets)
{
bool isScan = false;
for (id<TGMediaEditableItem> item in selectedItems) {
if ([item isKindOfClass:[TGCameraCapturedPhoto class]] && ((TGCameraCapturedPhoto *)item).rectangle != nil) {
isScan = true;
break;
}
}
if (storeAssets && !isScan) {
NSMutableArray *fullSizeSignals = [[NSMutableArray alloc] init];
for (id<TGMediaEditableItem> item in selectedItems)
{
if ([item isKindOfClass:[TGCameraCapturedPhoto class]] && ((TGCameraCapturedPhoto *)item).rectangle != nil) {
isScan = true;
}
if ([editingContext timerForItem:item] == nil)
{
SSignal *saveMedia = [SSignal defer:^SSignal *
@ -2584,8 +2785,14 @@ static CGPoint TGCameraControllerClampPointToScreenSize(__unused id self, __unus
else if (groupedId != nil && !hasAnyTimers)
dict[@"groupedId"] = groupedId;
id generatedItem = descriptionGenerator(dict, caption, entities, nil);
return generatedItem;
if (isScan) {
if (caption != nil)
dict[@"caption"] = caption;
return dict;
} else {
id generatedItem = descriptionGenerator(dict, caption, entities, nil);
return generatedItem;
}
}];
SSignal *assetSignal = inlineSignal;
@ -2611,12 +2818,13 @@ static CGPoint TGCameraControllerClampPointToScreenSize(__unused id self, __unus
return [SSignal complete];
}] onCompletion:^
{
__strong TGMediaEditingContext *strongEditingContext = editingContext;
[strongEditingContext description];
}];
} else {
NSLog(@"Editing context is nil");
}
[signals addObject:[[imageSignal map:^NSDictionary *(UIImage *image)
[signals addObject:[[[imageSignal map:^NSDictionary *(UIImage *image)
{
NSMutableDictionary *dict = [[NSMutableDictionary alloc] init];
dict[@"type"] = @"editedPhoto";
@ -2663,11 +2871,19 @@ static CGPoint TGCameraControllerClampPointToScreenSize(__unused id self, __unus
else if (groupedId != nil && !hasAnyTimers)
dict[@"groupedId"] = groupedId;
id generatedItem = descriptionGenerator(dict, caption, entities, nil);
return generatedItem;
if (isScan) {
if (caption != nil)
dict[@"caption"] = caption;
return dict;
} else {
id generatedItem = descriptionGenerator(dict, caption, entities, nil);
return generatedItem;
}
}] catch:^SSignal *(__unused id error)
{
return inlineSignal;
}] onCompletion:^{
[editingContext description];
}]];
i++;
@ -2750,6 +2966,54 @@ static CGPoint TGCameraControllerClampPointToScreenSize(__unused id self, __unus
groupedId = @([TGCameraController generateGroupedId]);
}
}
if (isScan) {
SSignal *scanSignal = [[SSignal combineSignals:signals] map:^NSDictionary *(NSArray *results) {
NSMutableData *data = [[NSMutableData alloc] init];
UIImage *previewImage = nil;
UIGraphicsBeginPDFContextToData(data, CGRectZero, nil);
for (NSDictionary *dict in results) {
if ([dict[@"type"] isEqual:@"editedPhoto"]) {
UIImage *image = dict[@"image"];
if (previewImage == nil) {
previewImage = image;
}
if (image != nil) {
CGRect rect = CGRectMake(0, 0, image.size.width, image.size.height);
UIGraphicsBeginPDFPageWithInfo(rect, nil);
CGContextRef pdfContext = UIGraphicsGetCurrentContext();
CGContextTranslateCTM(pdfContext, 0, image.size.height);
CGContextScaleCTM(pdfContext, 1.0, -1.0);
NSData *jpegData = UIImageJPEGRepresentation(image, 0.65);
CGDataProviderRef dataProvider = CGDataProviderCreateWithCFData((__bridge CFDataRef)jpegData);
CGImageRef cgImage = CGImageCreateWithJPEGDataProvider(dataProvider, NULL, true, kCGRenderingIntentDefault);
CGContextDrawImage(pdfContext, rect, cgImage);
CGDataProviderRelease(dataProvider);
CGImageRelease(cgImage);
}
}
}
UIGraphicsEndPDFContext();
NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:[[NSString alloc] initWithFormat:@"scan_%x.pdf", (int)arc4random()]];
[data writeToFile:filePath atomically:true];
NSMutableDictionary *dict = [[NSMutableDictionary alloc] init];
dict[@"type"] = @"file";
dict[@"previewImage"] = previewImage;
dict[@"tempFileUrl"] = [NSURL fileURLWithPath:filePath];
dict[@"fileName"] = @"Document Scan.pdf";
dict[@"mimeType"] = @"application/pdf";
id generatedItem = descriptionGenerator(dict, dict[@"caption"], nil, nil);
return generatedItem;
}];
signals = [NSMutableArray arrayWithObject:scanSignal];
}
return signals;
}

View File

@ -29,79 +29,16 @@ const CGFloat TGCameraFlashControlHeight = 44.0f;
{
self.hitTestEdgeInsets = UIEdgeInsetsMake(-10, -10, -10, -10);
_flashIconView = [[UIButton alloc] initWithFrame:CGRectMake(0, 0, 34, 44)];
_flashIconView = [[UIButton alloc] initWithFrame:CGRectMake(0, 0, 44, 44)];
_flashIconView.adjustsImageWhenHighlighted = false;
_flashIconView.contentMode = UIViewContentModeCenter;
_flashIconView.exclusiveTouch = true;
_flashIconView.hitTestEdgeInsets = UIEdgeInsetsMake(0, -10, 0, -10);
_flashIconView.tag = -1;
[_flashIconView setImage:TGComponentsImageNamed(@"CameraFlashButton") forState:UIControlStateNormal];
[_flashIconView setImage:[UIImage imageNamed:@"Camera/FlashOff"] forState:UIControlStateNormal];
[_flashIconView addTarget:self action:@selector(buttonPressed:) forControlEvents:UIControlEventTouchUpInside];
[self addSubview:_flashIconView];
static UIImage *highlightedIconImage = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^
{
UIImage *image = TGComponentsImageNamed(@"CameraFlashButton");
UIGraphicsBeginImageContextWithOptions(image.size, false, 0.0f);
CGContextRef context = UIGraphicsGetCurrentContext();
[image drawInRect:CGRectMake(0, 0, image.size.width, image.size.height)];
CGContextSetBlendMode (context, kCGBlendModeSourceAtop);
CGContextSetFillColorWithColor(context, [TGCameraInterfaceAssets accentColor].CGColor);
CGContextFillRect(context, CGRectMake(0, 0, image.size.width, image.size.height));
highlightedIconImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
});
[_flashIconView setImage:highlightedIconImage forState:UIControlStateSelected];
[_flashIconView setImage:highlightedIconImage forState:UIControlStateHighlighted | UIControlStateSelected];
_autoButton = [[UIButton alloc] init];
_autoButton.backgroundColor = [UIColor clearColor];
_autoButton.contentHorizontalAlignment = UIControlContentHorizontalAlignmentLeft;
_autoButton.exclusiveTouch = true;
_autoButton.hitTestEdgeInsets = UIEdgeInsetsMake(-10, -15, -10, -15);
_autoButton.tag = PGCameraFlashModeAuto;
_autoButton.titleLabel.font = [TGCameraInterfaceAssets normalFontOfSize:13];
[_autoButton setAttributedTitle:[[NSAttributedString alloc] initWithString:TGLocalized(@"Camera.FlashAuto") attributes:@{ NSForegroundColorAttributeName: [TGCameraInterfaceAssets normalColor], NSKernAttributeName: @2 }] forState:UIControlStateNormal];
[_autoButton setAttributedTitle:[[NSAttributedString alloc] initWithString:TGLocalized(@"Camera.FlashAuto") attributes:@{ NSForegroundColorAttributeName: [TGCameraInterfaceAssets accentColor], NSKernAttributeName: @2 }] forState:UIControlStateSelected];
[_autoButton setAttributedTitle:[_autoButton attributedTitleForState:UIControlStateSelected] forState:UIControlStateHighlighted | UIControlStateSelected];
[_autoButton addTarget:self action:@selector(buttonPressed:) forControlEvents:UIControlEventTouchUpInside];
[_autoButton sizeToFit];
_autoButton.frame = (CGRect){ CGPointZero, [TGCameraFlashControl _sizeForModeButtonWithTitle:[_autoButton attributedTitleForState:UIControlStateNormal]] };
[self addSubview:_autoButton];
_onButton = [[UIButton alloc] init];
_onButton.backgroundColor = [UIColor clearColor];
_onButton.contentHorizontalAlignment = UIControlContentHorizontalAlignmentLeft;
_onButton.exclusiveTouch = true;
_onButton.hitTestEdgeInsets = UIEdgeInsetsMake(-10, -15, -10, -15);
_onButton.tag = PGCameraFlashModeOn;
_onButton.titleLabel.font = [TGCameraInterfaceAssets normalFontOfSize:13];
[_onButton setAttributedTitle:[[NSAttributedString alloc] initWithString:TGLocalized(@"Camera.FlashOn") attributes:@{ NSForegroundColorAttributeName: [TGCameraInterfaceAssets normalColor], NSKernAttributeName: @2 }] forState:UIControlStateNormal];
[_onButton setAttributedTitle:[[NSAttributedString alloc] initWithString:TGLocalized(@"Camera.FlashOn") attributes:@{ NSForegroundColorAttributeName: [TGCameraInterfaceAssets accentColor], NSKernAttributeName: @2 }] forState:UIControlStateSelected];
[_onButton setAttributedTitle:[_onButton attributedTitleForState:UIControlStateSelected] forState:UIControlStateHighlighted | UIControlStateSelected];
[_onButton addTarget:self action:@selector(buttonPressed:) forControlEvents:UIControlEventTouchUpInside];
[_onButton sizeToFit];
_onButton.frame = (CGRect){ CGPointZero, [TGCameraFlashControl _sizeForModeButtonWithTitle:[_onButton attributedTitleForState:UIControlStateNormal]] };
[self addSubview:_onButton];
_offButton = [[UIButton alloc] init];
_offButton.backgroundColor = [UIColor clearColor];
_offButton.contentHorizontalAlignment = UIControlContentHorizontalAlignmentLeft;
_offButton.exclusiveTouch = true;
_offButton.hitTestEdgeInsets = UIEdgeInsetsMake(-10, -15, -10, -15);
_offButton.tag = PGCameraFlashModeOff;
_offButton.titleLabel.font = [TGCameraInterfaceAssets normalFontOfSize:13];
[_offButton setAttributedTitle:[[NSAttributedString alloc] initWithString:TGLocalized(@"Camera.FlashOff") attributes:@{ NSForegroundColorAttributeName: [TGCameraInterfaceAssets normalColor], NSKernAttributeName: @2 }] forState:UIControlStateNormal];
[_offButton setAttributedTitle:[[NSAttributedString alloc] initWithString:TGLocalized(@"Camera.FlashOff") attributes:@{ NSForegroundColorAttributeName: [TGCameraInterfaceAssets accentColor], NSKernAttributeName: @2 }] forState:UIControlStateSelected];
[_offButton setAttributedTitle:[_offButton attributedTitleForState:UIControlStateSelected] forState:UIControlStateHighlighted | UIControlStateSelected];
[_offButton addTarget:self action:@selector(buttonPressed:) forControlEvents:UIControlEventTouchUpInside];
[_offButton sizeToFit];
_offButton.frame = (CGRect){ CGPointZero, [TGCameraFlashControl _sizeForModeButtonWithTitle:[_offButton attributedTitleForState:UIControlStateNormal]] };
[self addSubview:_offButton];
[UIView performWithoutAnimation:^
{
self.mode = PGCameraFlashModeOff;
@ -123,305 +60,27 @@ const CGFloat TGCameraFlashControlHeight = 44.0f;
- (void)buttonPressed:(UIButton *)sender
{
if (!_active)
{
[self setActive:true animated:true];
if (_mode == PGCameraFlashModeOff) {
self.mode = PGCameraFlashModeOn;
[_flashIconView setImage:[UIImage imageNamed:@"Camera/FlashOn"] forState:UIControlStateNormal];
} else {
self.mode = PGCameraFlashModeOff;
[_flashIconView setImage:[UIImage imageNamed:@"Camera/FlashOff"] forState:UIControlStateNormal];
}
else
{
if (sender != _flashIconView)
self.mode = (int)sender.tag;
else
self.mode = _mode;
if (self.modeChanged != nil)
self.modeChanged(self.mode);
}
if (self.modeChanged != nil)
self.modeChanged(self.mode);
}
- (void)setFlashUnavailable:(bool)unavailable
{
self.userInteractionEnabled = !unavailable;
[self setActive:false animated:false];
}
- (void)setActive:(bool)active animated:(bool)animated
{
_active = active;
if (animated)
{
self.userInteractionEnabled = false;
if (active)
{
UIView *animatedView = nil;
UIView *snapshotView = nil;
CGRect targetFrame = CGRectZero;
if (self.mode != PGCameraFlashModeAuto)
{
_autoButton.frame = [self _autoButtonFrameForInterfaceOrientation:_interfaceOrientation];
_autoButton.alpha = 0.0f;
_autoButton.hidden = false;
}
else
{
animatedView = _autoButton;
targetFrame = [self _autoButtonFrameForInterfaceOrientation:_interfaceOrientation];
snapshotView = [animatedView snapshotViewAfterScreenUpdates:false];
}
_autoButton.selected = (self.mode == PGCameraFlashModeAuto);
if (self.mode != PGCameraFlashModeOn)
{
_onButton.frame = [self _onButtonFrameForInterfaceOrientation:_interfaceOrientation];
_onButton.alpha = 0.0f;
_onButton.hidden = false;
}
else
{
animatedView = _onButton;
targetFrame = [self _onButtonFrameForInterfaceOrientation:_interfaceOrientation];
}
_onButton.selected = (self.mode == PGCameraFlashModeOn);
if (self.mode != PGCameraFlashModeOff)
{
_offButton.frame = [self _offButtonFrameForInterfaceOrientation:_interfaceOrientation];
_offButton.alpha = 0.0f;
_offButton.hidden = false;
}
else
{
animatedView = _offButton;
targetFrame = [self _offButtonFrameForInterfaceOrientation:_interfaceOrientation];
snapshotView = [animatedView snapshotViewAfterScreenUpdates:false];
}
_offButton.selected = (self.mode == PGCameraFlashModeOff);
if (snapshotView != nil)
{
snapshotView.frame = animatedView.frame;
[animatedView.superview insertSubview:snapshotView belowSubview:animatedView];
animatedView.alpha = 0.0f;
}
UIView *iconSnapshotView = nil;
if (_flashIconView.selected)
{
iconSnapshotView = [_flashIconView snapshotViewAfterScreenUpdates:false];
iconSnapshotView.frame = _flashIconView.frame;
[_flashIconView.superview insertSubview:iconSnapshotView belowSubview:_flashIconView];
_flashIconView.selected = false;
_flashIconView.alpha = 0.0f;
}
[UIView animateWithDuration:0.25f delay:0.0f options:UIViewAnimationOptionCurveEaseInOut animations:^
{
_flashIconView.alpha = 1.0f;
_flashIconView.frame = [self _flashIconFrameForActive:active interfaceOrientation:_interfaceOrientation];
iconSnapshotView.frame = _flashIconView.frame;
_autoButton.alpha = 1.0f;
_onButton.alpha = 1.0f;
_offButton.alpha = 1.0f;
animatedView.alpha = 1.0f;
animatedView.frame = targetFrame;
snapshotView.frame = targetFrame;
} completion:^(BOOL finished)
{
[snapshotView removeFromSuperview];
[iconSnapshotView removeFromSuperview];
if (finished)
self.userInteractionEnabled = true;
}];
}
else
{
UIView *animatedView = nil;
UIView *snapshotView = nil;
UIView *iconSnapshotView = nil;
switch (self.mode)
{
case PGCameraFlashModeAuto:
{
animatedView = _autoButton;
snapshotView = [animatedView snapshotViewAfterScreenUpdates:false];
_autoButton.selected = false;
}
break;
case PGCameraFlashModeOn:
{
animatedView = _onButton;
if (!_onButton.selected)
{
snapshotView = [animatedView snapshotViewAfterScreenUpdates:false];
_onButton.selected = true;
}
iconSnapshotView = [_flashIconView snapshotViewAfterScreenUpdates:false];
iconSnapshotView.frame = _flashIconView.frame;
[_flashIconView.superview insertSubview:iconSnapshotView belowSubview:_flashIconView];
_flashIconView.selected = true;
_flashIconView.alpha = 0.0f;
}
break;
case PGCameraFlashModeOff:
{
animatedView = _offButton;
snapshotView = [animatedView snapshotViewAfterScreenUpdates:false];
_offButton.selected = false;
}
break;
default:
break;
}
if (snapshotView != nil)
{
snapshotView.frame = animatedView.frame;
[animatedView.superview insertSubview:snapshotView belowSubview:animatedView];
animatedView.alpha = 0.0f;
}
[UIView animateWithDuration:0.25f delay:0.0f options:UIViewAnimationOptionCurveEaseInOut animations:^
{
_flashIconView.alpha = 1.0f;
_flashIconView.frame = [self _flashIconFrameForActive:active interfaceOrientation:_interfaceOrientation];
iconSnapshotView.frame = _flashIconView.frame;
if (self.mode != PGCameraFlashModeAuto)
_autoButton.alpha = 0.0f;
if (self.mode != PGCameraFlashModeOn)
_onButton.alpha = 0.0f;
if (self.mode != PGCameraFlashModeOff)
_offButton.alpha = 0.0f;
animatedView.alpha = 1.0f;
animatedView.frame = [self _selectedButtonFrameForSize:animatedView.frame.size interfaceOrientation:_interfaceOrientation];
snapshotView.frame = animatedView.frame;
} completion:^(BOOL finished)
{
[snapshotView removeFromSuperview];
[iconSnapshotView removeFromSuperview];
if (finished)
{
self.userInteractionEnabled = true;
if (self.mode != PGCameraFlashModeAuto)
_autoButton.hidden = true;
if (self.mode != PGCameraFlashModeOn)
_onButton.hidden = true;
if (self.mode != PGCameraFlashModeOff)
_offButton.hidden = true;
}
}];
}
}
else
{
_flashIconView.frame = [self _flashIconFrameForActive:active interfaceOrientation:_interfaceOrientation];
if (active)
{
_flashIconView.selected = false;
_autoButton.frame = [self _autoButtonFrameForInterfaceOrientation:_interfaceOrientation];
_autoButton.alpha = 1.0f;
_autoButton.hidden = false;
_autoButton.selected = (self.mode == PGCameraFlashModeAuto);
_onButton.frame = [self _onButtonFrameForInterfaceOrientation:_interfaceOrientation];
_onButton.alpha = 1.0f;
_onButton.hidden = false;
_onButton.selected = (self.mode == PGCameraFlashModeOn);
_offButton.frame = [self _offButtonFrameForInterfaceOrientation:_interfaceOrientation];
_offButton.alpha = 1.0f;
_offButton.hidden = false;
_offButton.selected = (self.mode == PGCameraFlashModeOff);
}
else
{
switch (self.mode)
{
case PGCameraFlashModeOff:
{
_flashIconView.selected = false;
_autoButton.alpha = 0.0f;
_autoButton.hidden = true;
_autoButton.selected = false;
_onButton.alpha = 0.0f;
_onButton.hidden = true;
_onButton.selected = false;
_offButton.frame = [self _selectedButtonFrameForSize:_offButton.frame.size interfaceOrientation:_interfaceOrientation];
_offButton.alpha = 1.0f;
_offButton.hidden = false;
_offButton.selected = false;
}
break;
case PGCameraFlashModeOn:
{
_flashIconView.selected = true;
_autoButton.alpha = 0.0f;
_autoButton.hidden = true;
_autoButton.selected = false;
_onButton.frame = [self _selectedButtonFrameForSize:_onButton.frame.size interfaceOrientation:_interfaceOrientation];
_onButton.alpha = 1.0f;
_onButton.hidden = false;
_onButton.selected = true;
_offButton.alpha = 0.0f;
_offButton.hidden = true;
_offButton.selected = false;
}
break;
case PGCameraFlashModeAuto:
{
_flashIconView.selected = false;
_autoButton.frame = [self _selectedButtonFrameForSize:_autoButton.frame.size interfaceOrientation:_interfaceOrientation];
_autoButton.alpha = 1.0f;
_autoButton.hidden = false;
_autoButton.selected = false;
_onButton.alpha = 0.0f;
_onButton.hidden = true;
_onButton.selected = false;
_offButton.alpha = 0.0f;
_offButton.hidden = true;
_offButton.selected = false;
}
break;
default:
break;
}
}
}
if (active && self.becameActive != nil)
self.becameActive();
return;
}
- (void)setMode:(PGCameraFlashMode)mode
@ -484,130 +143,4 @@ const CGFloat TGCameraFlashControlHeight = 44.0f;
[self setActive:false animated:false];
}
- (CGRect)_flashIconFrameForActive:(bool)active interfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
CGPoint origin = CGPointZero;
CGSize size = self.frame.size;
if (UIInterfaceOrientationIsLandscape(interfaceOrientation))
size = CGSizeMake(size.height, size.width);
switch (interfaceOrientation)
{
case UIInterfaceOrientationLandscapeLeft:
{
if (active)
origin = CGPointMake(size.width - _flashIconView.frame.size.width - 5, (size.height - _flashIconView.frame.size.height) / 2);
else
origin = CGPointMake(size.width - _flashIconView.frame.size.width - 5, (size.height - _flashIconView.frame.size.height) / 2 - 9);
}
break;
case UIInterfaceOrientationLandscapeRight:
{
if (active)
origin = CGPointMake(5, (size.height - _flashIconView.frame.size.height) / 2);
else
origin = CGPointMake(5, (size.height - _flashIconView.frame.size.height) / 2 - 9);
}
break;
case UIInterfaceOrientationPortraitUpsideDown:
{
if (active)
{
origin = CGPointMake(0, 0);
}
else
{
CGFloat maxWidth = MAX(MAX(_offButton.frame.size.width, _onButton.frame.size.width), _autoButton.frame.size.width);
origin = CGPointMake(size.width - _flashIconView.frame.size.width - maxWidth, 0);
}
}
break;
default:
{
origin = CGPointZero;
}
break;
}
return CGRectMake(origin.x, origin.y, _flashIconView.frame.size.width, _flashIconView.frame.size.height);
}
- (CGRect)_selectedButtonFrameForSize:(CGSize)buttonSize interfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
CGPoint origin = CGPointZero;
CGSize size = self.frame.size;
if (UIInterfaceOrientationIsLandscape(interfaceOrientation))
size = CGSizeMake(size.height, size.width);
switch (interfaceOrientation)
{
case UIInterfaceOrientationLandscapeLeft:
case UIInterfaceOrientationLandscapeRight:
{
origin = CGPointMake(CGRectGetMidX(_flashIconView.frame) - buttonSize.width / 2, 21);
}
break;
case UIInterfaceOrientationPortraitUpsideDown:
{
CGRect iconFrame = [self _flashIconFrameForActive:false interfaceOrientation:interfaceOrientation];
origin = CGPointMake(iconFrame.origin.x + iconFrame.size.width - 3, (size.height - buttonSize.height) / 2);
}
break;
default:
{
origin = CGPointMake(_flashIconView.frame.size.width - 5,
(size.height - buttonSize.height) / 2);
}
break;
}
return CGRectMake(origin.x, origin.y, buttonSize.width, buttonSize.height);
}
- (CGRect)_autoButtonFrameForInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
CGSize size = self.frame.size;
if (UIInterfaceOrientationIsLandscape(interfaceOrientation))
size = CGSizeMake(size.height, size.width);
return CGRectMake(size.width / 4 - _autoButton.frame.size.width / 2,
(size.height - _autoButton.frame.size.height) / 2,
_autoButton.frame.size.width, _autoButton.frame.size.height);
}
- (CGRect)_onButtonFrameForInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
CGSize size = self.frame.size;
if (UIInterfaceOrientationIsLandscape(interfaceOrientation))
size = CGSizeMake(size.height, size.width);
return CGRectMake((size.width - _onButton.frame.size.width) / 2,
(size.height - _onButton.frame.size.height) / 2,
_onButton.frame.size.width, _onButton.frame.size.height);
}
- (CGRect)_offButtonFrameForInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
CGSize size = self.frame.size;
if (UIInterfaceOrientationIsLandscape(interfaceOrientation))
size = CGSizeMake(size.height, size.width);
return CGRectMake(size.width / 4 * 3 - _offButton.frame.size.width / 2,
(size.height - _offButton.frame.size.height) / 2,
_offButton.frame.size.width, _offButton.frame.size.height);
}
+ (CGSize)_sizeForModeButtonWithTitle:(NSAttributedString *)title
{
CGSize size = title.size;
CGFloat width = CGCeil(size.width);
if (iosMajorVersion() < 7)
width += 2;
return CGSizeMake(width, 20);
}
@end

View File

@ -1,18 +1,20 @@
#import "TGCameraFlipButton.h"
#import "TGCameraInterfaceAssets.h"
#import "LegacyComponentsInternal.h"
#import "TGImageUtils.h"
@implementation TGCameraFlipButton
- (instancetype)initWithFrame:(CGRect)frame large:(bool)large
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self != nil)
{
self.exclusiveTouch = true;
UIImage *image = large ? TGComponentsImageNamed(@"CameraLargeFlipButton") : TGTintedImage(TGComponentsImageNamed(@"CameraFlipButton"), [UIColor whiteColor]);
[self setImage:image forState:UIControlStateNormal];
self.backgroundColor = [TGCameraInterfaceAssets buttonColor];
self.layer.cornerRadius = 24.0;
[self setImage:[UIImage imageNamed:@"Camera/Flip"] forState:UIControlStateNormal];
}
return self;
}
@ -30,17 +32,65 @@
super.hidden = false;
self.userInteractionEnabled = false;
[UIView animateWithDuration:0.25f
animations:^
{
self.alpha = hidden ? 0.0f : 1.0f;
} completion:^(BOOL finished)
{
self.userInteractionEnabled = true;
[UIView animateWithDuration:0.25f animations:^
{
self.alpha = hidden ? 0.0f : 1.0f;
} completion:^(BOOL finished)
{
self.userInteractionEnabled = true;
if (finished)
self.hidden = hidden;
}];
if (finished)
self.hidden = hidden;
}];
}
else
{
self.alpha = hidden ? 0.0f : 1.0f;
super.hidden = hidden;
}
}
@end
@implementation TGCameraCancelButton
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self != nil)
{
self.exclusiveTouch = true;
self.backgroundColor = [TGCameraInterfaceAssets buttonColor];
self.layer.cornerRadius = 24.0;
[self setImage:[UIImage imageNamed:@"Camera/Cancel"] forState:UIControlStateNormal];
}
return self;
}
- (void)setHidden:(BOOL)hidden
{
self.alpha = hidden ? 0.0f : 1.0f;
super.hidden = hidden;
}
- (void)setHidden:(bool)hidden animated:(bool)animated
{
if (animated)
{
super.hidden = false;
self.userInteractionEnabled = false;
[UIView animateWithDuration:0.25f animations:^
{
self.alpha = hidden ? 0.0f : 1.0f;
} completion:^(BOOL finished)
{
self.userInteractionEnabled = true;
if (finished)
self.hidden = hidden;
}];
}
else
{

View File

@ -412,8 +412,8 @@
- (void)updateExposureIndicatorPositionForOrientation:(UIInterfaceOrientation)orientation
{
CGRect defaultPositionFrame = _exposureClipView.frame = CGRectMake(45 + _focusIndicatorImageView.frame.size.width + 5, 45 + (_focusIndicatorImageView.frame.size.height - 144) / 2, 25, 144);;
CGRect mirroredPositionFrame = _exposureClipView.frame = CGRectMake(15, 45 + (_focusIndicatorImageView.frame.size.height - 144) / 2, 25, 144);;
CGRect defaultPositionFrame = CGRectMake(45 + _focusIndicatorImageView.frame.size.width + 5, 45 + (_focusIndicatorImageView.frame.size.height - 144) / 2, 25, 144);
CGRect mirroredPositionFrame = CGRectMake(15, 45 + (_focusIndicatorImageView.frame.size.height - 144) / 2, 25, 144);
switch (orientation)
{
case UIInterfaceOrientationPortraitUpsideDown:

View File

@ -1,7 +1,22 @@
#import "TGCameraInterfaceAssets.h"
#import <CoreText/CoreText.h>
#import "LegacyComponentsInternal.h"
static NSString *TGCameraEncodeText(NSString *string, int key)
{
NSMutableString *result = [[NSMutableString alloc] init];
for (int i = 0; i < (int)[string length]; i++)
{
unichar c = [string characterAtIndex:i];
c += key;
[result appendString:[NSString stringWithCharacters:&c length:1]];
}
return result;
}
@implementation TGCameraInterfaceAssets
+ (UIColor *)normalColor
@ -11,12 +26,12 @@
+ (UIColor *)accentColor
{
return UIColorRGB(0xffcc00);
return UIColorRGB(0xffd60a);
}
+ (UIColor *)redColor
{
return UIColorRGB(0xf53333);
return UIColorRGB(0xfe3b30);
}
+ (UIColor *)panelBackgroundColor
@ -24,6 +39,11 @@
return [UIColor blackColor];
}
+ (UIColor *)buttonColor
{
return UIColorRGBA(0x393737, 0.6);
}
+ (UIColor *)transparentPanelBackgroundColor
{
return [UIColor colorWithWhite:0.0f alpha:0.5];
@ -34,9 +54,14 @@
return [UIColor colorWithWhite:0.0f alpha:0.7];
}
+ (UIFont *)normalFontOfSize:(CGFloat)size
+ (UIFont *)regularFontOfSize:(CGFloat)size
{
return [UIFont fontWithName:@"DINAlternate-Bold" size:size];
return [UIFont fontWithName:TGCameraEncodeText(@"TGDbnfsb.Sfhvmbs", -1) size:size];
}
+ (UIFont *)boldFontOfSize:(CGFloat)size
{
return [UIFont fontWithName:TGCameraEncodeText(@"TGDbnfsb.Tfnjcpme", -1) size:size];
}
@end

View File

@ -19,7 +19,7 @@
#import "TGCameraFlipButton.h"
#import "TGCameraTimeCodeView.h"
#import "TGCameraZoomView.h"
#import "TGCameraSegmentsView.h"
#import "TGCameraToastView.h"
#import "TGMenuView.h"
@ -127,8 +127,8 @@
_topPanelHeight = 44.0f;
_bottomPanelOffset = 94.0f;
_bottomPanelHeight = 123.0f;
_modeControlOffset = 0.0f;
_modeControlHeight = 52.0f;
_modeControlOffset = -5.0f;
_modeControlHeight = 56.0f;
_counterOffset = 7.0f;
shutterButtonWidth = 72.0f;
}
@ -206,16 +206,7 @@
_bottomPanelBackgroundView.backgroundColor = [TGCameraInterfaceAssets transparentPanelBackgroundColor];
[_bottomPanelView addSubview:_bottomPanelBackgroundView];
_cancelButton = [[TGModernButton alloc] initWithFrame:CGRectMake(0, 0, 60, 44)];
_cancelButton.backgroundColor = [UIColor clearColor];
_cancelButton.contentHorizontalAlignment = UIControlContentHorizontalAlignmentLeft;
_cancelButton.exclusiveTouch = true;
_cancelButton.titleLabel.font = TGSystemFontOfSize(18);
_cancelButton.contentEdgeInsets = UIEdgeInsetsMake(0, 20, 0, 0);
[_cancelButton setTitle:TGLocalized(@"Common.Cancel") forState:UIControlStateNormal];
[_cancelButton setTintColor:[TGCameraInterfaceAssets normalColor]];
[_cancelButton sizeToFit];
_cancelButton.frame = CGRectMake(0, 0, MAX(60.0f, _cancelButton.frame.size.width), 44);
_cancelButton = [[TGCameraCancelButton alloc] initWithFrame:CGRectMake(0, 0, 48, 48)];
[_cancelButton addTarget:self action:@selector(cancelButtonPressed) forControlEvents:UIControlEventTouchUpInside];
[_bottomPanelView addSubview:_cancelButton];
@ -241,19 +232,19 @@
_modeControl = [[TGCameraModeControl alloc] initWithFrame:CGRectMake(0, 0, frame.size.width, _modeControlHeight) avatar:avatar];
[_bottomPanelView addSubview:_modeControl];
_flipButton = [[TGCameraFlipButton alloc] initWithFrame:CGRectMake(0, 0, 56, 56) large:true];
_flipButton = [[TGCameraFlipButton alloc] initWithFrame:CGRectMake(0, 0, 48, 48)];
[_flipButton addTarget:self action:@selector(flipButtonPressed) forControlEvents:UIControlEventTouchUpInside];
[_bottomPanelView addSubview:_flipButton];
_flashControl = [[TGCameraFlashControl alloc] initWithFrame:CGRectMake(0, 0, frame.size.width, TGCameraFlashControlHeight)];
_flashControl = [[TGCameraFlashControl alloc] initWithFrame:CGRectMake(3.0, 0, TGCameraFlashControlHeight, TGCameraFlashControlHeight)];
[_topPanelView addSubview:_flashControl];
_topFlipButton = [[TGCameraFlipButton alloc] initWithFrame:CGRectMake(0, 0, 44, 44) large:false];
_topFlipButton = [[TGCameraFlipButton alloc] initWithFrame:CGRectMake(0, 0, 44, 44)];
_topFlipButton.hidden = true;
[_topFlipButton addTarget:self action:@selector(flipButtonPressed) forControlEvents:UIControlEventTouchUpInside];
[_topPanelView addSubview:_topFlipButton];
// [_topPanelView addSubview:_topFlipButton];
_timecodeView = [[TGCameraTimeCodeView alloc] initWithFrame:CGRectMake((frame.size.width - 120) / 2, 12, 120, 20)];
_timecodeView = [[TGCameraTimeCodeView alloc] initWithFrame:CGRectMake((frame.size.width - 120) / 2, 12, 120, 28)];
_timecodeView.hidden = true;
_timecodeView.requestedRecordingDuration = ^NSTimeInterval
{
@ -273,7 +264,10 @@
[self addSubview:_videoLandscapePanelView];
_flashActiveView = [[TGCameraFlashActiveView alloc] initWithFrame:CGRectMake((frame.size.width - 40) / 2, frame.size.height - _bottomPanelHeight - 37, 40, 21)];
[self addSubview:_flashActiveView];
// [self addSubview:_flashActiveView];
_toastView = [[TGCameraToastView alloc] initWithFrame:CGRectMake(0, frame.size.height - _bottomPanelHeight - 42, frame.size.width, 32)];
[self addSubview:_toastView];
_zoomView = [[TGCameraZoomView alloc] initWithFrame:CGRectMake(10, frame.size.height - _bottomPanelHeight - _bottomPanelOffset - 18, frame.size.width - 20, 1.5f)];
_zoomView.activityChanged = ^(bool active)
@ -386,9 +380,12 @@
else
{
_hasResults = true;
_topFlipButton.hidden = false;
_topFlipButton.hidden = _modeControl.cameraMode == PGCameraModePhotoScan;
_flipButton.hidden = true;
_doneButton.hidden = false;
if (_modeControl.cameraMode == PGCameraModePhotoScan) {
_modeControl.hidden = true;
}
}
}
@ -830,18 +827,18 @@
if (superview == _videoLandscapePanelView && superviewSize.width < superviewSize.height)
superviewSize = CGSizeMake(superviewSize.height, superviewSize.width);
if (UIInterfaceOrientationIsLandscape(orientation) && _flashControl.interfaceOrientation == orientation && _flashControl.superview == _topPanelView)
{
if (orientation == UIInterfaceOrientationLandscapeLeft)
_flashControl.frame = CGRectMake(7, 0, TGCameraFlashControlHeight, 370);
else if (orientation == UIInterfaceOrientationLandscapeRight)
_flashControl.frame = CGRectMake(7, 0, TGCameraFlashControlHeight, 370);
}
else
{
_flashControl.frame = CGRectMake(0, (superviewSize.height - TGCameraFlashControlHeight) / 2, superviewSize.width, TGCameraFlashControlHeight);
}
_timecodeView.frame = CGRectMake((superviewSize.width - 120) / 2, (superviewSize.height - 20) / 2, 120, 20);
// if (UIInterfaceOrientationIsLandscape(orientation) && _flashControl.interfaceOrientation == orientation && _flashControl.superview == _topPanelView)
// {
// if (orientation == UIInterfaceOrientationLandscapeLeft)
// _flashControl.frame = CGRectMake(7, 0, TGCameraFlashControlHeight, 370);
// else if (orientation == UIInterfaceOrientationLandscapeRight)
// _flashControl.frame = CGRectMake(7, 0, TGCameraFlashControlHeight, 370);
// }
// else
// {
// _flashControl.frame = CGRectMake(0, (superviewSize.height - TGCameraFlashControlHeight) / 2, superviewSize.width, TGCameraFlashControlHeight);
// }
_timecodeView.frame = CGRectMake((superviewSize.width - 120) / 2, (superviewSize.height - 28) / 2, 120, 28);
}
- (void)layoutPreviewRelativeViews
@ -865,13 +862,17 @@
_modeControl.frame = CGRectMake(0, _modeControlOffset, self.frame.size.width, _modeControlHeight);
_shutterButton.frame = CGRectMake(round((self.frame.size.width - _shutterButton.frame.size.width) / 2), _modeControlHeight + _modeControlOffset, _shutterButton.frame.size.width, _shutterButton.frame.size.height);
_cancelButton.frame = CGRectMake(0, round(_shutterButton.center.y - _cancelButton.frame.size.height / 2.0f), _cancelButton.frame.size.width, _cancelButton.frame.size.height);
_cancelButton.frame = CGRectMake(20.0, round(_shutterButton.center.y - _cancelButton.frame.size.height / 2.0f), _cancelButton.frame.size.width, _cancelButton.frame.size.height);
_doneButton.frame = CGRectMake(_bottomPanelView.frame.size.width - _doneButton.frame.size.width, round(_shutterButton.center.y - _doneButton.frame.size.height / 2.0f), _doneButton.frame.size.width, _doneButton.frame.size.height);
_flipButton.frame = CGRectMake(self.frame.size.width - _flipButton.frame.size.width - 4.0f - 7.0f, round(_shutterButton.center.y - _flipButton.frame.size.height / 2.0f), _flipButton.frame.size.width, _flipButton.frame.size.height);
_flipButton.frame = CGRectMake(self.frame.size.width - _flipButton.frame.size.width - 20.0f, round(_shutterButton.center.y - _flipButton.frame.size.height / 2.0f), _flipButton.frame.size.width, _flipButton.frame.size.height);
_topFlipButton.frame = CGRectMake(self.frame.size.width - _topFlipButton.frame.size.width - 4.0f, 0.0f, _topFlipButton.frame.size.width, _topFlipButton.frame.size.height);
_toastView.frame = CGRectMake(0, self.frame.size.height - _bottomPanelHeight - _bottomPanelOffset - 32 - 16, self.frame.size.width, 32);
CGFloat photosViewSize = TGPhotoThumbnailSizeForCurrentScreen().height + 4 * 2;
_photoCounterButton.frame = CGRectMake(self.frame.size.width - 56.0f - 10.0f, _counterOffset, 64, 38);
_selectedPhotosView.frame = CGRectMake(4.0f, [_photoCounterButton convertRect:_photoCounterButton.bounds toView:self].origin.y - photosViewSize - 20.0f, self.frame.size.width - 4.0f * 2.0f, photosViewSize);

View File

@ -100,7 +100,7 @@ const CGFloat TGCameraTabletPanelViewWidth = 102.0f;
};
[_panelView addSubview:_timecodeView];
_flipButton = [[TGCameraFlipButton alloc] initWithFrame:CGRectMake(0, 0, 44, 44) large:true];
_flipButton = [[TGCameraFlipButton alloc] initWithFrame:CGRectMake(0, 0, 44, 44)];
[_flipButton addTarget:self action:@selector(flipButtonPressed) forControlEvents:UIControlEventTouchUpInside];
[_panelView addSubview:_flipButton];

View File

@ -5,10 +5,11 @@
#import <LegacyComponents/TGModernButton.h>
#import "TGCameraShutterButton.h"
#import "TGCameraFlipButton.h"
#import "TGCameraModeControl.h"
#import "TGCameraTimeCodeView.h"
#import "TGCameraZoomView.h"
#import "TGCameraSegmentsView.h"
#import "TGCameraToastView.h"
#import "TGMediaPickerPhotoCounterButton.h"
#import "TGMediaPickerPhotoStripView.h"
@ -37,15 +38,22 @@
[self updateForCameraModeChangeWithPreviousMode:previousMode];
}
- (void)setToastMessage:(NSString *)message animated:(bool)animated
{
[_toastView setText:message animated:animated];
}
- (void)updateForCameraModeChangeWithPreviousMode:(PGCameraMode)__unused previousMode
{
switch (_modeControl.cameraMode)
{
case PGCameraModePhoto:
case PGCameraModeSquarePhoto:
case PGCameraModePhotoScan:
{
[_shutterButton setButtonMode:TGCameraShutterButtonNormalMode animated:true];
[_timecodeView setHidden:true animated:true];
[_flipButton setHidden:_modeControl.cameraMode == PGCameraModePhotoScan animated:true];
}
break;

View File

@ -26,10 +26,7 @@ const CGFloat TGCameraModeControlVerticalInteritemSpace = 29.0f;
self = [super initWithFrame:frame];
if (self != nil)
{
if (frame.size.width > frame.size.height)
_kerning = 3.5f;
else
_kerning = 2.0f;
_kerning = 0.75f;
_maskView = [[UIView alloc] initWithFrame:self.bounds];
_maskView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
@ -53,6 +50,7 @@ const CGFloat TGCameraModeControlVerticalInteritemSpace = 29.0f;
[
[self _createButtonForMode:PGCameraModeVideo title:TGLocalized(@"Camera.VideoMode")],
[self _createButtonForMode:PGCameraModePhoto title:TGLocalized(@"Camera.PhotoMode")]
// [self _createButtonForMode:PGCameraModePhotoScan title:TGLocalized(@"Camera.ScanMode")]
];
}
@ -72,7 +70,7 @@ const CGFloat TGCameraModeControlVerticalInteritemSpace = 29.0f;
_maskLayer = [CAGradientLayer layer];
_maskLayer.colors = @[ (id)[UIColor clearColor].CGColor, (id)[UIColor whiteColor].CGColor, (id)[UIColor whiteColor].CGColor, (id)[UIColor clearColor].CGColor ];
_maskLayer.locations = @[ @0.0f, @0.33f, @0.67f, @1.0f ];
_maskLayer.locations = @[ @0.0f, @0.4f, @0.6f, @1.0f ];
_maskLayer.startPoint = CGPointMake(0.0f, 0.5f);
_maskLayer.endPoint = CGPointMake(1.0f, 0.5f);
_maskView.layer.mask = _maskLayer;
@ -94,19 +92,14 @@ const CGFloat TGCameraModeControlVerticalInteritemSpace = 29.0f;
return self;
}
+ (UIFont *)_buttonFont
{
return [UIFont fontWithName:@"SFCompactText-Regular" size:14];
}
+ (CGFloat)_buttonHorizontalSpacing
{
return 19;
return 25;
}
+ (CGFloat)_buttonVerticalSpacing
{
return 19;
return 25;
}
- (UIButton *)_createButtonForMode:(PGCameraMode)mode title:(NSString *)title
@ -116,11 +109,17 @@ const CGFloat TGCameraModeControlVerticalInteritemSpace = 29.0f;
button.exclusiveTouch = true;
button.hitTestEdgeInsets = UIEdgeInsetsMake(-10, -10, -10, -10);
button.tag = mode;
button.titleLabel.font = [TGCameraInterfaceAssets normalFontOfSize:13];
[button setAttributedTitle:[[NSAttributedString alloc] initWithString:title attributes:@{ NSForegroundColorAttributeName: [TGCameraInterfaceAssets normalColor], NSKernAttributeName: @(_kerning) }] forState:UIControlStateNormal];
[button setAttributedTitle:[[NSAttributedString alloc] initWithString:title attributes:@{ NSForegroundColorAttributeName: [TGCameraInterfaceAssets accentColor], NSKernAttributeName: @(_kerning) }] forState:UIControlStateSelected];
[button setAttributedTitle:[[NSAttributedString alloc] initWithString:title attributes:@{ NSForegroundColorAttributeName: [TGCameraInterfaceAssets normalColor], NSKernAttributeName: @(_kerning), NSFontAttributeName: [TGCameraInterfaceAssets regularFontOfSize:14] }] forState:UIControlStateNormal];
[button setAttributedTitle:[[NSAttributedString alloc] initWithString:title attributes:@{ NSForegroundColorAttributeName: [TGCameraInterfaceAssets accentColor], NSKernAttributeName: @(_kerning), NSFontAttributeName: [TGCameraInterfaceAssets boldFontOfSize:14] }] forState:UIControlStateSelected];
[button setAttributedTitle:[button attributedTitleForState:UIControlStateSelected] forState:UIControlStateHighlighted | UIControlStateSelected];
[button sizeToFit];
button.titleLabel.shadowColor = [UIColor blackColor];
button.titleLabel.shadowOffset = CGSizeMake(0.0, 0.0);
button.titleLabel.layer.shadowRadius = 2.0;
button.titleLabel.layer.shadowOpacity = 0.3;
button.titleLabel.layer.masksToBounds = false;
button.titleLabel.layer.shouldRasterize = true;
button.frame = CGRectMake(0.0, 0.0, button.frame.size.width + 2.0, button.frame.size.height);
[button addTarget:self action:@selector(buttonPressed:) forControlEvents:UIControlEventTouchUpInside];
return button;
@ -191,7 +190,7 @@ const CGFloat TGCameraModeControlVerticalInteritemSpace = 29.0f;
CGFloat angle = ABS(offset / _wrapperView.frame.size.width * 0.99f);
CGFloat sign = offset > 0 ? 1.0f : -1.0f;
CATransform3D transform = CATransform3DTranslate(CATransform3DIdentity, -28 * angle * angle * sign, 0.0f, 0.0f);
CATransform3D transform = CATransform3DTranslate(CATransform3DIdentity, -2 * angle * angle * sign, 0.0f, 0.0f);
transform = CATransform3DRotate(transform, angle, 0.0f, sign, 0.0f);
return transform;
}

View File

@ -0,0 +1,14 @@
#import <UIKit/UIKit.h>
@class TGCameraPreviewView;
@class PGRectangle;
@interface TGCameraRectangleView : UIView
@property (nonatomic, weak) TGCameraPreviewView *previewView;
@property (nonatomic, assign) bool enabled;
- (void)drawRectangle:(PGRectangle *)rectangle;
@end

View File

@ -0,0 +1,102 @@
#import "TGCameraRectangleView.h"
#import "TGCameraInterfaceAssets.h"
#import "LegacyComponentsInternal.h"
#import "TGImageUtils.h"
#import "TGCameraPreviewView.h"
#import "PGRectangleDetector.h"
@interface TGCameraRectangleView ()
{
CAShapeLayer *_quadLayer;
bool _clearing;
}
@end
@implementation TGCameraRectangleView
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self != nil) {
_enabled = true;
self.backgroundColor = [UIColor clearColor];
self.alpha = 0.0f;
_quadLayer = [[CAShapeLayer alloc] init];
_quadLayer.strokeColor = [[TGCameraInterfaceAssets accentColor] colorWithAlphaComponent:0.7].CGColor;
_quadLayer.fillColor = [[TGCameraInterfaceAssets accentColor] colorWithAlphaComponent:0.45].CGColor;
_quadLayer.lineWidth = 2.0;
[self.layer addSublayer:_quadLayer];
}
return self;
}
- (CGPathRef)pathForRectangle:(PGRectangle *)rectangle
{
CGAffineTransform transform = CGAffineTransformMakeScale(self.previewView.frame.size.width, self.previewView.frame.size.height);
PGRectangle *displayRectangle = [[rectangle rotate90] transform:transform];
UIBezierPath *path = [[UIBezierPath alloc] init];
[path moveToPoint:displayRectangle.topLeft];
[path addLineToPoint:displayRectangle.topRight];
[path addLineToPoint:displayRectangle.bottomRight];
[path addLineToPoint:displayRectangle.bottomLeft];
[path closePath];
return path.CGPath;
}
- (void)drawRectangle:(PGRectangle *)rectangle
{
if (!_enabled) {
return;
}
if (rectangle == nil) {
[self clear];
return;
}
_clearing = false;
[self.layer removeAllAnimations];
bool animated = _quadLayer.path != nil;
if (animated) {
CAAnimation *animation = [CABasicAnimation animationWithKeyPath:@"path"];
animation.duration = 0.2;
[_quadLayer addAnimation:animation forKey:@"path"];
} else {
self.transform = CGAffineTransformMakeScale(1.1, 1.1);
[UIView animateWithDuration:0.2 delay:0.0 options:UIViewAnimationOptionAllowAnimatedContent animations:^{
self.transform = CGAffineTransformIdentity;
self.alpha = 1.0f;
} completion:nil];
}
_quadLayer.path = [self pathForRectangle:rectangle];
}
- (void)clear
{
if (_quadLayer.path == nil || _clearing)
return;
_clearing = true;
[UIView animateWithDuration:0.2 delay:0.0 options:UIViewAnimationOptionAllowAnimatedContent animations:^{
self.alpha = 0.0f;
} completion:^(BOOL finished) {
if (_clearing) {
_quadLayer.path = nil;
_clearing = false;
}
}];
}
- (void)layoutSubviews
{
_quadLayer.frame = self.bounds;
}
@end

View File

@ -1,260 +0,0 @@
#import "TGCameraSegmentsView.h"
#import "LegacyComponentsInternal.h"
#import "TGImageUtils.h"
#import "TGCameraInterfaceAssets.h"
#import <LegacyComponents/TGModernButton.h>
const CGFloat TGCameraSegmentsBackgroundInset = 21.0f;
const CGFloat TGCameraSegmentsBackgroundHeight = 10.0f;
const CGFloat TGCameraSegmentsSpacing = 1.5f;
const CGFloat TGCameraSegmentsMinimumWidth = 4.0f;
@interface TGCameraSegmentView : UIImageView
- (void)setBlinking;
- (void)setRecording;
- (void)setCommittingWithCompletion:(void (^)(void))completion;
@end
@interface TGCameraSegmentsView ()
{
UIImageView *_backgroundView;
UIView *_segmentWrapper;
NSArray *_segmentViews;
TGCameraSegmentView *_currentSegmentView;
CGFloat _currentSegment;
TGModernButton *_deleteButton;
}
@end
@implementation TGCameraSegmentsView
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self != nil)
{
static dispatch_once_t onceToken;
static UIImage *segmentImage = nil;
dispatch_once(&onceToken, ^
{
UIGraphicsBeginImageContextWithOptions(CGSizeMake(4, 4), false, 0.0f);
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetFillColorWithColor(context, [UIColor whiteColor].CGColor);
[[UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, 4, 4) cornerRadius:0.5f] fill];
segmentImage = [UIGraphicsGetImageFromCurrentImageContext() resizableImageWithCapInsets:UIEdgeInsetsMake(4, 4, 4, 4)];
UIGraphicsEndImageContext();
});
_backgroundView = [[UIImageView alloc] initWithImage:[TGComponentsImageNamed(@"CameraSegmentsBack") resizableImageWithCapInsets:UIEdgeInsetsMake(4, 4, 4, 4)]];
[self addSubview:_backgroundView];
_segmentWrapper = [[UIView alloc] init];
[_backgroundView addSubview:_segmentWrapper];
_currentSegmentView = [[TGCameraSegmentView alloc] initWithImage:[TGTintedImage(segmentImage, [TGCameraInterfaceAssets accentColor]) resizableImageWithCapInsets:UIEdgeInsetsMake(4, 4, 4, 4)]];
[_segmentWrapper addSubview:_currentSegmentView];
_deleteButton = [[TGModernButton alloc] initWithFrame:CGRectMake(0, 0, 32, 32)];
_deleteButton.exclusiveTouch = true;
[_deleteButton setImage:TGComponentsImageNamed(@"CameraDeleteIcon") forState:UIControlStateNormal];
[_deleteButton addTarget:self action:@selector(deleteButtonPressed) forControlEvents:UIControlEventTouchUpInside];
[self addSubview:_deleteButton];
[self setDeleteButtonHidden:true animated:false];
}
return self;
}
- (void)deleteButtonPressed
{
if (self.deletePressed != nil)
self.deletePressed();
}
- (void)setSegments:(NSArray *)__unused segments
{
}
- (void)startCurrentSegment
{
[_currentSegmentView setRecording];
}
- (void)setCurrentSegment:(CGFloat)length
{
_currentSegment = length;
[self _layoutSegmentViews];
}
- (void)commitCurrentSegmentWithCompletion:(void (^)(void))completion
{
__weak TGCameraSegmentView *weakSegmentView = _currentSegmentView;
[_currentSegmentView setCommittingWithCompletion:^
{
__strong TGCameraSegmentView *strongSegmentView = weakSegmentView;
if (strongSegmentView == nil)
return;
_currentSegment = 0;
if (completion != nil)
completion();
[strongSegmentView setBlinking];
}];
}
- (void)highlightLastSegment
{
}
- (void)removeLastSegment
{
}
- (void)setHidden:(BOOL)hidden
{
self.alpha = hidden ? 0.0f : 1.0f;
super.hidden = hidden;
if (!hidden)
[_currentSegmentView setBlinking];
}
- (void)setHidden:(bool)hidden animated:(bool)animated delay:(NSTimeInterval)delay
{
if (animated)
{
super.hidden = false;
[UIView animateWithDuration:0.25f delay:delay options:UIViewAnimationOptionCurveLinear animations:^
{
self.alpha = hidden ? 0.0f : 1.0f;
} completion:^(BOOL finished)
{
if (finished)
self.hidden = hidden;
if (!hidden)
[_currentSegmentView setBlinking];
}];
}
else
{
[self setHidden:hidden];
}
}
- (void)setDeleteButtonHidden:(bool)hidden animated:(bool)animated
{
if (animated)
{
_deleteButton.hidden = false;
[UIView animateWithDuration:0.25f animations:^
{
_deleteButton.alpha = hidden ? 0.0f : 1.0f;
} completion:^(BOOL finished)
{
if (finished)
_deleteButton.hidden = hidden;
}];
}
else
{
_deleteButton.hidden = hidden;
_deleteButton.alpha = hidden ? 0.0f : 1.0f;
}
}
- (void)_layoutBackgroundView
{
CGFloat backgroundRightPadding = 0.0f;
CGFloat deleteButtonMargin = _deleteButton.frame.size.width + 9.0f;
if (!_deleteButton.hidden)
backgroundRightPadding = deleteButtonMargin;
_backgroundView.frame = CGRectMake(TGCameraSegmentsBackgroundInset, (self.frame.size.height - TGCameraSegmentsBackgroundHeight) / 2, self.frame.size.width - TGCameraSegmentsBackgroundInset * 2 - backgroundRightPadding, TGCameraSegmentsBackgroundHeight);
_segmentWrapper.frame = CGRectMake(3, 3, self.frame.size.width - TGCameraSegmentsBackgroundInset * 2 - deleteButtonMargin, TGCameraSegmentsBackgroundHeight - 3 * 2);
}
- (void)_layoutDeleteButton
{
_deleteButton.frame = CGRectMake(CGRectGetMaxX(_backgroundView.frame) + 14, (self.frame.size.height - _deleteButton.frame.size.height) / 2, _deleteButton.frame.size.width, _deleteButton.frame.size.height);
}
- (void)_layoutSegmentViews
{
}
- (void)layoutSubviews
{
[self _layoutBackgroundView];
[self _layoutDeleteButton];
}
@end
@interface TGCameraSegmentView ()
{
}
@end
@implementation TGCameraSegmentView
- (instancetype)initWithImage:(UIImage *)image
{
self = [super initWithImage:image];
if (self != nil)
{
}
return self;
}
- (void)setBlinking
{
[self _playBlinkAnimation];
}
- (void)setRecording
{
[self _stopBlinkAnimation];
}
- (void)setCommittingWithCompletion:(void (^)(void))__unused completion
{
}
- (void)_playBlinkAnimation
{
CAKeyframeAnimation *blinkAnim = [CAKeyframeAnimation animationWithKeyPath:@"opacity"];
blinkAnim.duration = 1.2f;
blinkAnim.autoreverses = false;
blinkAnim.fillMode = kCAFillModeForwards;
blinkAnim.repeatCount = HUGE_VALF;
blinkAnim.keyTimes = @[ @0.0f, @0.4f, @0.5f, @0.9f, @1.0f ];
blinkAnim.values = @[ @1.0f, @1.0f, @0.0f, @0.0f, @1.0f ];
[self.layer addAnimation:blinkAnim forKey:@"opacity"];
}
- (void)_stopBlinkAnimation
{
[self.layer removeAllAnimations];
}
@end

View File

@ -1,4 +1,5 @@
#import "TGCameraShutterButton.h"
#import "TGImageUtils.h"
#import <LegacyComponents/JNWSpringAnimation.h>
@ -36,7 +37,7 @@
UIGraphicsBeginImageContextWithOptions(CGSizeMake(frame.size.width, frame.size.height), false, 0.0f);
CGContextRef context = UIGraphicsGetCurrentContext();
CGFloat thickness = (padding < 8.0f) ? 5.0f : 6.0f;
CGFloat thickness = 4.0 - TGScreenPixel;
CGContextSetStrokeColorWithColor(context, [TGCameraInterfaceAssets normalColor].CGColor);
CGContextSetLineWidth(context, thickness);
@ -70,17 +71,17 @@
- (CGFloat)innerPadding
{
if (self.frame.size.width == 50.0f)
return 7.0f;
return 6.0f;
return 8.0f;
return 6.0f;
}
- (CGFloat)squarePadding
{
if (self.frame.size.width == 50.0f)
return 15.0f;
return 19.0f;
return 19.0f;
return 23.0f;
}
- (void)setButtonMode:(TGCameraShutterButtonMode)mode animated:(bool)animated

View File

@ -7,7 +7,7 @@
@interface TGCameraTimeCodeView ()
{
UIImageView *_dotView;
UIView *_backgroundView;
UILabel *_timeLabel;
NSUInteger _recordingDurationSeconds;
@ -22,29 +22,17 @@
self = [super initWithFrame:frame];
if (self != nil)
{
static UIImage *dotImage = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^
{
UIGraphicsBeginImageContextWithOptions(CGSizeMake(6, 6), false, 0.0f);
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetFillColorWithColor(context, [TGCameraInterfaceAssets redColor].CGColor);
CGContextFillEllipseInRect(context, CGRectMake(0, 0, 6, 6));
dotImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
});
_dotView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 7, 6, 6)];
_dotView.layer.opacity = 0.0f;
_dotView.image = dotImage;
[self addSubview:_dotView];
_backgroundView = [[UIView alloc] init];
_backgroundView.clipsToBounds = true;
_backgroundView.layer.cornerRadius = 4.0;
_backgroundView.backgroundColor = [TGCameraInterfaceAssets redColor];
_backgroundView.alpha = 0.0;
[self addSubview:_backgroundView];
_timeLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, self.frame.size.width, self.frame.size.height)];
_timeLabel.backgroundColor = [UIColor clearColor];
_timeLabel.font = [TGCameraInterfaceAssets normalFontOfSize:21];
_timeLabel.text = @"00:00:00";
_timeLabel.font = [TGCameraInterfaceAssets regularFontOfSize:21];
_timeLabel.text = @"00:00";
_timeLabel.textAlignment = NSTextAlignmentCenter;
_timeLabel.textColor = [TGCameraInterfaceAssets normalColor];
[self addSubview:_timeLabel];
@ -59,7 +47,18 @@
- (void)_updateRecordingTime
{
_timeLabel.text = [NSString stringWithFormat:@"%02d:%02d:%02d", (int)(_recordingDurationSeconds / 3600), (int)(_recordingDurationSeconds / 60) % 60, (int)(_recordingDurationSeconds % 60)];
if (_recordingDurationSeconds > 60 * 60) {
_timeLabel.text = [NSString stringWithFormat:@"%02d:%02d:%02d", (int)(_recordingDurationSeconds / 3600), (int)(_recordingDurationSeconds / 60) % 60, (int)(_recordingDurationSeconds % 60)];
} else {
_timeLabel.text = [NSString stringWithFormat:@"%02d:%02d", (int)(_recordingDurationSeconds / 60) % 60, (int)(_recordingDurationSeconds % 60)];
}
[_timeLabel sizeToFit];
CGFloat inset = 8.0f;
CGFloat backgroundWidth = _timeLabel.frame.size.width + inset * 2.0;
_backgroundView.frame = CGRectMake(floor((self.frame.size.width - backgroundWidth) / 2.0), 0.0, backgroundWidth, 28.0);
_timeLabel.frame = CGRectMake(floor((self.frame.size.width - _timeLabel.frame.size.width) / 2.0), floor((28 - _timeLabel.frame.size.height) / 2.0), _timeLabel.frame.size.width, _timeLabel.frame.size.height);
}
- (void)startRecording
@ -67,22 +66,18 @@
[self reset];
_recordingTimer = [TGTimerTarget scheduledMainThreadTimerWithTarget:self action:@selector(recordingTimerEvent) interval:1.0 repeat:false];
[self playBlinkAnimation];
}
- (void)stopRecording
{
[_recordingTimer invalidate];
_recordingTimer = nil;
[self stopBlinkAnimation];
}
- (void)reset
{
_timeLabel.text = @"00:00:00";
_recordingDurationSeconds = 0;
[self _updateRecordingTime];
}
- (void)recordingTimerEvent
@ -108,24 +103,6 @@
}
}
- (void)playBlinkAnimation
{
CAKeyframeAnimation *blinkAnim = [CAKeyframeAnimation animationWithKeyPath:@"opacity"];
blinkAnim.duration = 0.75f;
blinkAnim.autoreverses = false;
blinkAnim.fillMode = kCAFillModeForwards;
blinkAnim.repeatCount = HUGE_VALF;
blinkAnim.keyTimes = @[ @0.0f, @0.4f, @0.5f, @0.9f, @1.0f ];
blinkAnim.values = @[ @1.0f, @1.0f, @0.0f, @0.0f, @1.0f ];
[_dotView.layer addAnimation:blinkAnim forKey:@"opacity"];
}
- (void)stopBlinkAnimation
{
[_dotView.layer removeAllAnimations];
}
- (void)setHidden:(BOOL)hidden
{
self.alpha = hidden ? 0.0f : 1.0f;
@ -156,7 +133,7 @@
- (void)layoutSubviews
{
_dotView.frame = CGRectMake(CGFloor(self.frame.size.width / 2 - 48), 7, 6, 6);
[self _updateRecordingTime];
}
@end

View File

@ -0,0 +1,7 @@
#import <UIKit/UIKit.h>
@interface TGCameraToastView : UIView
- (void)setText:(NSString *)text animated:(bool)animated;
@end

View File

@ -0,0 +1,70 @@
#import "TGCameraToastView.h"
#import "TGCameraInterfaceAssets.h"
#import "TGFont.h"
@implementation TGCameraToastView
{
UIView *_backgroundView;
UILabel *_label;
}
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self != nil)
{
_backgroundView = [[UIView alloc] init];
_backgroundView.alpha = 0.0f;
_backgroundView.clipsToBounds = true;
_backgroundView.layer.cornerRadius = 5.0f;
_backgroundView.backgroundColor = [TGCameraInterfaceAssets transparentPanelBackgroundColor];
[self addSubview:_backgroundView];
_label = [[UILabel alloc] init];
_label.alpha = 0.0f;
_label.textColor = [UIColor whiteColor];
_label.font = [TGFont systemFontOfSize:17.0f];
[self addSubview:_label];
}
return self;
}
- (void)setText:(NSString *)text animated:(bool)animated
{
if (text.length == 0)
{
if (animated) {
[UIView animateWithDuration:0.2 animations:^{
_backgroundView.alpha = 0.0f;
_label.alpha = 0.0f;
}];
} else {
_backgroundView.alpha = 0.0f;
_label.alpha = 0.0f;
}
return;
}
if (animated) {
[UIView animateWithDuration:0.2 animations:^{
_backgroundView.alpha = 1.0f;
_label.alpha = 1.0f;
}];
} else {
_backgroundView.alpha = 1.0f;
_label.alpha = 1.0f;
}
_label.text = text;
[_label sizeToFit];
CGFloat inset = 8.0f;
CGFloat backgroundWidth = _label.frame.size.width + inset * 2.0;
_backgroundView.frame = CGRectMake(floor((self.frame.size.width - backgroundWidth) / 2.0), 0.0, backgroundWidth, 32.0);
_label.frame = CGRectMake(floor((self.frame.size.width - _label.frame.size.width) / 2.0), floor((32 - _label.frame.size.height) / 2.0), _label.frame.size.width, _label.frame.size.height);
[self setNeedsLayout];
}
@end

View File

@ -117,7 +117,7 @@ UIFont *TGFixedSystemFontOfSize(CGFloat size)
+ (UIFont *)roundedFontOfSize:(CGFloat)size
{
if (@available(iOSApplicationExtension 13.0, iOS 13.0, *)) {
UIFontDescriptor *descriptor = [UIFont boldSystemFontOfSize: size].fontDescriptor;
UIFontDescriptor *descriptor = [UIFont boldSystemFontOfSize:size].fontDescriptor;
descriptor = [descriptor fontDescriptorWithDesign:UIFontDescriptorSystemDesignRounded];
return [UIFont fontWithDescriptor:descriptor size:size];
} else {

View File

@ -1266,7 +1266,7 @@ static CGFloat progressOfSampleBufferInTimeRange(CMSampleBufferRef sampleBuffer,
return (CGSize){ 1920.0f, 1920.0f };
case TGMediaVideoConversionPresetVideoMessage:
return (CGSize){ 240.0f, 240.0f };
return (CGSize){ 384.0f, 384.0f };
case TGMediaVideoConversionPresetProfileLow:
return (CGSize){ 720.0f, 720.0f };
@ -1375,7 +1375,7 @@ static CGFloat progressOfSampleBufferInTimeRange(CMSampleBufferRef sampleBuffer,
return 4000;
case TGMediaVideoConversionPresetVideoMessage:
return 300;
return 1000;
case TGMediaVideoConversionPresetProfile:
return 1500;

View File

@ -1,26 +0,0 @@
#import "TGPhotoEditorTabController.h"
@class PGPhotoEditor;
@class TGPhotoEditorPreviewView;
@class AVPlayer;
@interface TGPhotoAvatarCropController : TGPhotoEditorTabController
@property (nonatomic, readonly) UIView *transitionParentView;
@property (nonatomic, assign) bool switching;
@property (nonatomic, assign) bool skipTransitionIn;
@property (nonatomic, assign) bool fromCamera;
@property (nonatomic, copy) void (^finishedPhotoProcessing)(void);
- (instancetype)initWithContext:(id<LegacyComponentsContext>)context photoEditor:(PGPhotoEditor *)photoEditor previewView:(TGPhotoEditorPreviewView *)previewView;
- (void)setImage:(UIImage *)image;
- (void)setPlayer:(AVPlayer *)player;
- (void)setSnapshotImage:(UIImage *)snapshotImage;
- (void)setSnapshotView:(UIView *)snapshotView;
- (void)_finishedTransitionIn;
@end

View File

@ -1,707 +0,0 @@
#import "TGPhotoAvatarCropController.h"
#import "LegacyComponentsInternal.h"
#import "TGPhotoEditorInterfaceAssets.h"
#import <LegacyComponents/TGPhotoEditorAnimation.h>
#import <LegacyComponents/UIControl+HitTestEdgeInsets.h>
#import <LegacyComponents/TGPhotoEditorUtils.h>
#import "PGPhotoEditor.h"
#import "TGPhotoEditorPreviewView.h"
#import "TGPhotoAvatarCropView.h"
#import <LegacyComponents/TGModernButton.h>
#import "TGPhotoPaintController.h"
const CGFloat TGPhotoAvatarCropButtonsWrapperSize = 61.0f;
@interface TGPhotoAvatarCropController ()
{
UIView *_wrapperView;
UIView *_buttonsWrapperView;
TGModernButton *_rotateButton;
TGModernButton *_mirrorButton;
TGModernButton *_resetButton;
TGPhotoAvatarCropView *_cropView;
UIView *_snapshotView;
UIImage *_snapshotImage;
bool _appeared;
UIImage *_imagePendingLoad;
dispatch_semaphore_t _waitSemaphore;
}
@property (nonatomic, weak) PGPhotoEditor *photoEditor;
@property (nonatomic, weak) TGPhotoEditorPreviewView *previewView;
@end
@implementation TGPhotoAvatarCropController
- (instancetype)initWithContext:(id<LegacyComponentsContext>)context photoEditor:(PGPhotoEditor *)photoEditor previewView:(TGPhotoEditorPreviewView *)previewView
{
self = [super initWithContext:context];
if (self != nil)
{
self.photoEditor = photoEditor;
self.previewView = previewView;
_waitSemaphore = dispatch_semaphore_create(0);
}
return self;
}
- (void)loadView
{
[super loadView];
__weak TGPhotoAvatarCropController *weakSelf = self;
void(^interactionBegan)(void) = ^
{
__strong TGPhotoAvatarCropController *strongSelf = weakSelf;
if (strongSelf == nil)
return;
self.controlVideoPlayback(false);
};
void(^interactionEnded)(void) = ^
{
__strong TGPhotoAvatarCropController *strongSelf = weakSelf;
if (strongSelf == nil)
return;
if ([strongSelf shouldAutorotate])
[TGViewController attemptAutorotation];
self.controlVideoPlayback(true);
};
_wrapperView = [[UIView alloc] initWithFrame:self.view.bounds];
_wrapperView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
[self.view addSubview:_wrapperView];
PGPhotoEditor *photoEditor = self.photoEditor;
_cropView = [[TGPhotoAvatarCropView alloc] initWithOriginalSize:photoEditor.originalSize screenSize:[self referenceViewSize] fullPreviewView:nil fullPaintingView:nil fullEntitiesView:nil];
[_cropView setCropRect:photoEditor.cropRect];
[_cropView setCropOrientation:photoEditor.cropOrientation];
[_cropView setCropMirrored:photoEditor.cropMirrored];
_cropView.croppingChanged = ^
{
__strong TGPhotoAvatarCropController *strongSelf = weakSelf;
if (strongSelf == nil)
return;
photoEditor.cropRect = strongSelf->_cropView.cropRect;
photoEditor.cropOrientation = strongSelf->_cropView.cropOrientation;
photoEditor.cropMirrored = strongSelf->_cropView.cropMirrored;
};
if (_snapshotView != nil)
{
[_cropView setSnapshotView:_snapshotView];
_snapshotView = nil;
}
else if (_snapshotImage != nil)
{
[_cropView setSnapshotImage:_snapshotImage];
_snapshotImage = nil;
}
_cropView.interactionBegan = interactionBegan;
_cropView.interactionEnded = interactionEnded;
[_wrapperView addSubview:_cropView];
_buttonsWrapperView = [[UIView alloc] initWithFrame:CGRectZero];
[_wrapperView addSubview:_buttonsWrapperView];
_rotateButton = [[TGModernButton alloc] initWithFrame:CGRectMake(0, 0, 36, 36)];
_rotateButton.exclusiveTouch = true;
_rotateButton.hitTestEdgeInsets = UIEdgeInsetsMake(-10, -10, -10, -10);
[_rotateButton addTarget:self action:@selector(rotate) forControlEvents:UIControlEventTouchUpInside];
[_rotateButton setImage:TGComponentsImageNamed(@"PhotoEditorRotateIcon") forState:UIControlStateNormal];
// [_buttonsWrapperView addSubview:_rotateButton];
_mirrorButton = [[TGModernButton alloc] initWithFrame:CGRectMake(0, 0, 36, 36)];
_mirrorButton.exclusiveTouch = true;
_mirrorButton.imageEdgeInsets = UIEdgeInsetsMake(4.0f, 0.0f, 0.0f, 0.0f);
_mirrorButton.hitTestEdgeInsets = UIEdgeInsetsMake(-10, -10, -10, -10);
[_mirrorButton addTarget:self action:@selector(mirror) forControlEvents:UIControlEventTouchUpInside];
[_mirrorButton setImage:TGComponentsImageNamed(@"PhotoEditorMirrorIcon") forState:UIControlStateNormal];
// [_buttonsWrapperView addSubview:_mirrorButton];
_resetButton = [[TGModernButton alloc] init];
_resetButton.contentEdgeInsets = UIEdgeInsetsMake(0.0f, 8.0f, 0.0f, 8.0f);
_resetButton.exclusiveTouch = true;
_resetButton.hitTestEdgeInsets = UIEdgeInsetsMake(-10, -10, -10, -10);
_resetButton.titleLabel.font = [TGFont systemFontOfSize:13];
[_resetButton addTarget:self action:@selector(reset) forControlEvents:UIControlEventTouchUpInside];
[_resetButton setTitle:TGLocalized(@"PhotoEditor.CropReset") forState:UIControlStateNormal];
[_resetButton setTitleColor:[UIColor whiteColor]];
[_resetButton sizeToFit];
_resetButton.frame = CGRectMake(0, 0, _resetButton.frame.size.width, 24);
[_buttonsWrapperView addSubview:_resetButton];
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
if (_appeared)
return;
if (self.initialAppearance && self.skipTransitionIn)
{
[self _finishedTransitionInWithView:nil];
if (self.finishedTransitionIn != nil)
{
self.finishedTransitionIn();
self.finishedTransitionIn = nil;
}
}
else
{
[self transitionIn];
}
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
_appeared = true;
if (_imagePendingLoad != nil)
[_cropView setImage:_imagePendingLoad];
}
- (BOOL)shouldAutorotate
{
return (!_cropView.isTracking && [super shouldAutorotate]);
}
- (bool)isDismissAllowed
{
return _appeared && !_cropView.isTracking && !_cropView.isAnimating;
}
#pragma mark -
- (void)setImage:(UIImage *)image
{
if (_dismissing && !_switching)
return;
if (_waitSemaphore != nil)
dispatch_semaphore_signal(_waitSemaphore);
if (!_appeared)
{
_imagePendingLoad = image;
return;
}
[_cropView setImage:image];
}
- (void)setPlayer:(AVPlayer *)player
{
}
- (void)setSnapshotImage:(UIImage *)snapshotImage
{
_snapshotImage = snapshotImage;
[_cropView _replaceSnapshotImage:snapshotImage];
}
- (void)setSnapshotView:(UIView *)snapshotView
{
_snapshotView = snapshotView;
}
#pragma mark - Transition
- (void)prepareTransitionInWithReferenceView:(UIView *)referenceView referenceFrame:(CGRect)referenceFrame parentView:(UIView *)parentView noTransitionView:(bool)noTransitionView
{
[super prepareTransitionInWithReferenceView:referenceView referenceFrame:referenceFrame parentView:parentView noTransitionView:noTransitionView];
[self.view insertSubview:_transitionView belowSubview:_wrapperView];
}
- (void)transitionIn
{
_buttonsWrapperView.alpha = 0.0f;
[UIView animateWithDuration:0.3f animations:^
{
_buttonsWrapperView.alpha = 1.0f;
}];
[_cropView animateTransitionIn];
}
- (void)animateTransitionIn
{
if ([_transitionView isKindOfClass:[TGPhotoEditorPreviewView class]])
[(TGPhotoEditorPreviewView *)_transitionView performTransitionToCropAnimated:true];
[super animateTransitionIn];
}
- (void)_finishedTransitionInWithView:(UIView *)transitionView
{
if ([transitionView isKindOfClass:[TGPhotoEditorPreviewView class]]) {
} else {
[transitionView removeFromSuperview];
}
_buttonsWrapperView.alpha = 1.0f;
[_cropView transitionInFinishedFromCamera:(self.fromCamera && self.initialAppearance)];
}
- (void)_finishedTransitionIn
{
// [_cropView animateTransitionIn];
[_cropView transitionInFinishedFromCamera:true];
self.finishedTransitionIn();
self.finishedTransitionIn = nil;
}
- (void)prepareForCustomTransitionOut
{
[_cropView hideImageForCustomTransition];
[_cropView animateTransitionOutSwitching:false];
[UIView animateWithDuration:0.3f animations:^
{
_buttonsWrapperView.alpha = 0.0f;
} completion:nil];
}
- (void)transitionOutSwitching:(bool)switching completion:(void (^)(void))completion
{
_dismissing = true;
[_cropView animateTransitionOutSwitching:switching];
if (switching)
{
_switching = true;
TGPhotoEditorPreviewView *previewView = self.previewView;
[previewView performTransitionToCropAnimated:false];
[previewView setSnapshotView:[_cropView cropSnapshotView]];
PGPhotoEditor *photoEditor = self.photoEditor;
if (self.item.isVideo) {
if (!previewView.hidden)
[previewView performTransitionInWithCompletion:nil];
else
[previewView setNeedsTransitionIn];
if (self.finishedPhotoProcessing != nil)
self.finishedPhotoProcessing();
} else {
UIImage *image = _cropView.currentImage;
CGRect cropRect = _cropView.cropRect;
UIImageOrientation cropOrientation = _cropView.cropOrientation;
bool cropMirrored = _cropView.cropMirrored;
CGSize originalSize = _cropView.originalSize;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^
{
if (dispatch_semaphore_wait(_waitSemaphore, dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC))))
{
TGLegacyLog(@"Photo crop on switching failed");
return;
}
UIImage *croppedImage = TGPhotoEditorCrop(image, nil, cropOrientation, 0.0f, cropRect, false, TGPhotoEditorScreenImageMaxSize(), originalSize, true);
[photoEditor setImage:croppedImage forCropRect:cropRect cropRotation:0.0f cropOrientation:cropOrientation cropMirrored:cropMirrored fullSize:false];
[photoEditor processAnimated:false completion:^
{
TGDispatchOnMainThread(^
{
[previewView setSnapshotImage:croppedImage];
if (!previewView.hidden)
[previewView performTransitionInWithCompletion:nil];
else
[previewView setNeedsTransitionIn];
});
}];
if (self.finishedPhotoProcessing != nil)
self.finishedPhotoProcessing();
});
}
UIInterfaceOrientation orientation = self.effectiveOrientation;
CGRect cropRectFrame = [_cropView cropRectFrameForView:self.view];
CGSize referenceSize = [self referenceViewSizeForOrientation:orientation];
CGRect referenceBounds = CGRectMake(0, 0, referenceSize.width, referenceSize.height);
CGRect containerFrame = [TGPhotoEditorTabController photoContainerFrameForParentViewFrame:referenceBounds toolbarLandscapeSize:self.toolbarLandscapeSize orientation:orientation panelSize:TGPhotoEditorPanelSize hasOnScreenNavigation:self.hasOnScreenNavigation];
// if (self.switchingToTab == TGPhotoEditorPreviewTab)
// {
// containerFrame = [TGPhotoEditorTabController photoContainerFrameForParentViewFrame:referenceBounds toolbarLandscapeSize:self.toolbarLandscapeSize orientation:orientation panelSize:0 hasOnScreenNavigation:self.hasOnScreenNavigation];
// }
// else if (self.switchingToTab == TGPhotoEditorPaintTab)
// {
// containerFrame = [TGPhotoPaintController photoContainerFrameForParentViewFrame:referenceBounds toolbarLandscapeSize:self.toolbarLandscapeSize orientation:orientation panelSize:TGPhotoPaintTopPanelSize + TGPhotoPaintBottomPanelSize hasOnScreenNavigation:self.hasOnScreenNavigation];
// }
CGSize fittedSize = TGScaleToSize(cropRectFrame.size, containerFrame.size);
CGRect targetFrame = CGRectMake(containerFrame.origin.x + (containerFrame.size.width - fittedSize.width) / 2,
containerFrame.origin.y + (containerFrame.size.height - fittedSize.height) / 2,
fittedSize.width,
fittedSize.height);
UIView *snapshotView = [_cropView cropSnapshotView];
snapshotView.alpha = 0.0f;
snapshotView.frame = cropRectFrame;
[self.view addSubview:snapshotView];
CGRect targetCropViewFrame = [self.view convertRect:targetFrame toView:_wrapperView];
if (!self.item.isVideo) {
_previewView.hidden = true;
}
[UIView animateWithDuration:0.3f delay:0.0f options:UIViewAnimationOptionCurveEaseInOut | UIViewAnimationOptionLayoutSubviews animations:^
{
snapshotView.frame = targetFrame;
if (self.item.isVideo) {
_cropView.alpha = 0.0f;
} else {
snapshotView.alpha = 1.0f;
}
_cropView.frame = targetCropViewFrame;
[_cropView invalidateCropRect];
} completion:^(__unused BOOL finished)
{
_previewView.hidden = false;
if (self.finishedTransitionOut != nil)
self.finishedTransitionOut();
}];
}
[UIView animateWithDuration:0.3f animations:^
{
_buttonsWrapperView.alpha = 0.0f;
} completion:^(__unused BOOL finished)
{
if (completion != nil)
completion();
}];
}
- (void)transitionOutSaving:(bool)__unused saving completion:(void (^)(void))completion
{
CGRect referenceFrame = [_cropView contentFrameForView:self.view];
CGSize referenceSize = [self referenceViewSize];
UIImageView *snapshotView = [[UIImageView alloc] initWithImage:_cropView.image];
snapshotView.frame = [_wrapperView convertRect:referenceFrame fromView:nil];
snapshotView.alpha = 0.0f;
[_wrapperView insertSubview:snapshotView belowSubview:_cropView];
[self transitionOutSwitching:false completion:nil];
if (self.intent & TGPhotoEditorControllerFromCameraIntent && self.intent & (TGPhotoEditorControllerAvatarIntent | TGPhotoEditorControllerSignupAvatarIntent))
{
if (self.interfaceOrientation == UIInterfaceOrientationLandscapeLeft)
{
referenceFrame = CGRectMake(referenceSize.height - referenceFrame.size.height - referenceFrame.origin.y,
referenceSize.width - referenceFrame.size.width - referenceFrame.origin.x,
referenceFrame.size.height, referenceFrame.size.width);
}
else if (self.interfaceOrientation == UIInterfaceOrientationLandscapeRight)
{
referenceFrame = CGRectMake(referenceFrame.origin.y,
referenceFrame.origin.x,
referenceFrame.size.height, referenceFrame.size.width);
}
}
UIView *referenceView = nil;
UIView *parentView = nil;
if (self.beginTransitionOut != nil)
referenceView = self.beginTransitionOut(&referenceFrame, &parentView);
if (self.intent & TGPhotoEditorControllerFromCameraIntent && self.intent & (TGPhotoEditorControllerAvatarIntent | TGPhotoEditorControllerSignupAvatarIntent))
{
if (self.interfaceOrientation == UIInterfaceOrientationLandscapeLeft)
{
referenceFrame = CGRectMake(referenceSize.width - referenceFrame.size.height - referenceFrame.origin.y,
referenceFrame.origin.x,
referenceFrame.size.height, referenceFrame.size.width);
}
else if (self.interfaceOrientation == UIInterfaceOrientationLandscapeRight)
{
referenceFrame = CGRectMake(referenceFrame.origin.y,
referenceSize.height - referenceFrame.size.width - referenceFrame.origin.x,
referenceFrame.size.height, referenceFrame.size.width);
}
}
POPSpringAnimation *animation = [TGPhotoEditorAnimation prepareTransitionAnimationForPropertyNamed:kPOPViewFrame];
animation.fromValue = [NSValue valueWithCGRect:snapshotView.frame];
animation.toValue = [NSValue valueWithCGRect:[_wrapperView convertRect:referenceFrame fromView:nil]];
POPSpringAnimation *alphaAnimation = [TGPhotoEditorAnimation prepareTransitionAnimationForPropertyNamed:kPOPViewAlpha];
alphaAnimation.fromValue = @(snapshotView.alpha);
alphaAnimation.toValue = @(0.0f);
[TGPhotoEditorAnimation performBlock:^(__unused bool allFinished)
{
[snapshotView removeFromSuperview];
if (completion != nil)
completion();
} whenCompletedAllAnimations:@[ animation, alphaAnimation ]];
[snapshotView pop_addAnimation:animation forKey:@"frame"];
[snapshotView pop_addAnimation:alphaAnimation forKey:@"alpha"];
}
- (CGRect)_targetFrameForTransitionInFromFrame:(CGRect)fromFrame
{
CGSize referenceSize = [self referenceViewSize];
UIInterfaceOrientation orientation = self.effectiveOrientation;
CGRect containerFrame = [TGPhotoEditorTabController photoContainerFrameForParentViewFrame:CGRectMake(0, 0, referenceSize.width, referenceSize.height) toolbarLandscapeSize:self.toolbarLandscapeSize orientation:orientation panelSize:0.0f hasOnScreenNavigation:self.hasOnScreenNavigation];
CGRect targetFrame = CGRectZero;
CGFloat shortSide = MIN(referenceSize.width, referenceSize.height);
CGFloat diameter = shortSide - [TGPhotoAvatarCropView areaInsetSize].width * 2;
if (self.initialAppearance && (self.fromCamera || !self.skipTransitionIn))
{
CGSize referenceSize = fromFrame.size;
if ([_transitionView isKindOfClass:[UIImageView class]])
referenceSize = ((UIImageView *)_transitionView).image.size;
CGSize fittedSize = TGScaleToFill(referenceSize, CGSizeMake(diameter, diameter));
targetFrame = CGRectMake(containerFrame.origin.x + (containerFrame.size.width - fittedSize.width) / 2,
containerFrame.origin.y + (containerFrame.size.height - fittedSize.height) / 2,
fittedSize.width, fittedSize.height);
}
else
{
targetFrame = CGRectMake(containerFrame.origin.x + (containerFrame.size.width - diameter) / 2,
containerFrame.origin.y + (containerFrame.size.height - diameter) / 2,
diameter, diameter);
}
return targetFrame;
}
- (CGRect)transitionOutReferenceFrame
{
return [_cropView cropRectFrameForView:self.view];
}
- (UIView *)transitionOutReferenceView
{
return [_cropView cropSnapshotView];
}
- (id)currentResultRepresentation
{
return [_cropView cropSnapshotView];
}
#pragma mark - Actions
- (void)rotate
{
[_cropView rotate90DegreesCCWAnimated:true];
}
- (void)mirror
{
[_cropView mirror];
}
- (void)reset
{
[_cropView resetAnimated:true];
}
#pragma mark - Layout
- (void)viewWillLayoutSubviews
{
[super viewWillLayoutSubviews];
[self updateLayout:[[LegacyComponentsGlobals provider] applicationStatusBarOrientation]];
}
- (void)updateLayout:(UIInterfaceOrientation)orientation
{
if ([self inFormSheet] || [UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad)
{
_resetButton.hidden = true;
}
orientation = [self effectiveOrientation:orientation];
CGSize referenceSize = [self referenceViewSize];
if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad)
[_cropView updateCircleImageWithReferenceSize:referenceSize];
CGFloat screenSide = MAX(referenceSize.width, referenceSize.height) + 2 * TGPhotoEditorPanelSize;
_wrapperView.frame = CGRectMake((referenceSize.width - screenSide) / 2, (referenceSize.height - screenSide) / 2, screenSide, screenSide);
bool hasOnScreenNavigation = false;
if (iosMajorVersion() >= 11)
hasOnScreenNavigation = (self.viewLoaded && self.view.safeAreaInsets.bottom > FLT_EPSILON) || self.context.safeAreaInset.bottom > FLT_EPSILON;
UIEdgeInsets safeAreaInset = [TGViewController safeAreaInsetForOrientation:orientation hasOnScreenNavigation:hasOnScreenNavigation];
UIEdgeInsets screenEdges = UIEdgeInsetsMake((screenSide - self.view.frame.size.height) / 2, (screenSide - self.view.frame.size.width) / 2, (screenSide + self.view.frame.size.height) / 2, (screenSide + self.view.frame.size.width) / 2);
UIEdgeInsets initialScreenEdges = screenEdges;
screenEdges.top += safeAreaInset.top;
screenEdges.left += safeAreaInset.left;
screenEdges.bottom -= safeAreaInset.bottom;
screenEdges.right -= safeAreaInset.right;
switch (orientation)
{
case UIInterfaceOrientationLandscapeLeft:
{
[UIView performWithoutAnimation:^
{
_buttonsWrapperView.frame = CGRectMake(screenEdges.left + self.toolbarLandscapeSize,
screenEdges.top,
TGPhotoAvatarCropButtonsWrapperSize,
referenceSize.height);
_rotateButton.frame = CGRectMake(25 + 2.0f, 10, _rotateButton.frame.size.width, _rotateButton.frame.size.height);
_mirrorButton.frame = CGRectMake(25, 60, _mirrorButton.frame.size.width, _mirrorButton.frame.size.height);
_resetButton.transform = CGAffineTransformIdentity;
[_resetButton sizeToFit];
_resetButton.frame = CGRectMake(0, 0, _resetButton.frame.size.width, 24);
CGFloat xOrigin = 0;
if (_resetButton.frame.size.width > _buttonsWrapperView.frame.size.width)
{
_resetButton.transform = CGAffineTransformMakeRotation((CGFloat)M_PI_2);
xOrigin = 12;
}
_resetButton.frame = CGRectMake(_buttonsWrapperView.frame.size.width - _resetButton.frame.size.width - xOrigin,
(_buttonsWrapperView.frame.size.height - _resetButton.frame.size.height) / 2,
_resetButton.frame.size.width,
_resetButton.frame.size.height);
}];
}
break;
case UIInterfaceOrientationLandscapeRight:
{
[UIView performWithoutAnimation:^
{
_buttonsWrapperView.frame = CGRectMake(screenEdges.right - self.toolbarLandscapeSize - TGPhotoAvatarCropButtonsWrapperSize,
screenEdges.top,
TGPhotoAvatarCropButtonsWrapperSize,
referenceSize.height);
_rotateButton.frame = CGRectMake(_buttonsWrapperView.frame.size.width - _rotateButton.frame.size.width - 25 + 2.0f, 10, _rotateButton.frame.size.width, _rotateButton.frame.size.height);
_mirrorButton.frame = CGRectMake(_buttonsWrapperView.frame.size.width - _mirrorButton.frame.size.width - 25, 60, _mirrorButton.frame.size.width, _mirrorButton.frame.size.height);
_resetButton.transform = CGAffineTransformIdentity;
[_resetButton sizeToFit];
CGSize resetButtonSize = _resetButton.frame.size;
CGFloat xOrigin = 0;
if (resetButtonSize.width > _buttonsWrapperView.frame.size.width)
{
_resetButton.transform = CGAffineTransformMakeRotation((CGFloat)-M_PI_2);
xOrigin = 12;
}
_resetButton.frame = CGRectMake(xOrigin,
(_buttonsWrapperView.frame.size.height - _resetButton.frame.size.height) / 2,
_resetButton.frame.size.width,
_resetButton.frame.size.height);
}];
}
break;
default:
{
[UIView performWithoutAnimation:^
{
_buttonsWrapperView.frame = CGRectMake(screenEdges.left,
screenEdges.bottom - TGPhotoEditorToolbarSize - TGPhotoAvatarCropButtonsWrapperSize,
referenceSize.width,
TGPhotoAvatarCropButtonsWrapperSize);
_rotateButton.frame = CGRectMake(10, _buttonsWrapperView.frame.size.height - _rotateButton.frame.size.height - 25 + 2.0f, _rotateButton.frame.size.width, _rotateButton.frame.size.height);
_mirrorButton.frame = CGRectMake(60, _buttonsWrapperView.frame.size.height - _mirrorButton.frame.size.height - 25, _mirrorButton.frame.size.width, _mirrorButton.frame.size.height);
_resetButton.transform = CGAffineTransformIdentity;
[_resetButton sizeToFit];
_resetButton.frame = CGRectMake((_buttonsWrapperView.frame.size.width - _resetButton.frame.size.width) / 2,
10,
_resetButton.frame.size.width,
24);
}];
}
break;
}
if (_dismissing)
return;
CGRect containerFrame = [TGPhotoEditorTabController photoContainerFrameForParentViewFrame:CGRectMake(0, 0, referenceSize.width, referenceSize.height) toolbarLandscapeSize:self.toolbarLandscapeSize orientation:orientation panelSize:0.0f hasOnScreenNavigation:hasOnScreenNavigation];
containerFrame = CGRectOffset(containerFrame, initialScreenEdges.left, initialScreenEdges.top);
CGFloat shortSide = MIN(referenceSize.width, referenceSize.height);
CGFloat diameter = shortSide - [TGPhotoAvatarCropView areaInsetSize].width * 2;
_cropView.frame = CGRectMake(containerFrame.origin.x + (containerFrame.size.width - diameter) / 2,
containerFrame.origin.y + (containerFrame.size.height - diameter) / 2,
diameter,
diameter);
}
- (TGPhotoEditorTab)availableTabs
{
return TGPhotoEditorRotateTab | TGPhotoEditorMirrorTab;
}
- (void)handleTabAction:(TGPhotoEditorTab)tab
{
switch (tab)
{
case TGPhotoEditorRotateTab:
{
[self rotate];
}
break;
case TGPhotoEditorMirrorTab:
{
[self mirror];
}
break;
default:
break;
}
}
@end

View File

@ -0,0 +1,6 @@
#import "TGPhotoEditorTabController.h"
@interface TGPhotoRectangleCropController : TGPhotoEditorTabController
@end

View File

@ -0,0 +1,5 @@
#import "TGPhotoRectangleCropController.h"
@implementation TGPhotoRectangleCropController
@end

View File

@ -0,0 +1,7 @@
#import <UIKit/UIKit.h>
@interface TGWarpedView : UIImageView
- (void)transformToFitQuadTopLeft:(CGPoint)tl topRight:(CGPoint)tr bottomLeft:(CGPoint)bl bottomRight:(CGPoint)br;
@end

View File

@ -0,0 +1,92 @@
#import "TGWarpedView.h"
@implementation TGWarpedView
- (void)transformToFitQuadTopLeft:(CGPoint)tl topRight:(CGPoint)tr bottomLeft:(CGPoint)bl bottomRight:(CGPoint)br
{
CGRect boundingBox = [[self class] boundingBoxForQuadTR:tr tl:tl bl:bl br:br];
self.frame = boundingBox;
CGPoint frameTopLeft = boundingBox.origin;
CATransform3D transform = [[self class] rectToQuad:self.bounds
quadTL:CGPointMake(tl.x-frameTopLeft.x, tl.y-frameTopLeft.y)
quadTR:CGPointMake(tr.x-frameTopLeft.x, tr.y-frameTopLeft.y)
quadBL:CGPointMake(bl.x-frameTopLeft.x, bl.y-frameTopLeft.y)
quadBR:CGPointMake(br.x-frameTopLeft.x, br.y-frameTopLeft.y)];
self.layer.transform = transform;
}
+ (CGRect)boundingBoxForQuadTR:(CGPoint)tr tl:(CGPoint)tl bl:(CGPoint)bl br:(CGPoint)br
{
CGRect boundingBox = CGRectZero;
CGFloat xmin = MIN(MIN(MIN(tr.x, tl.x), bl.x),br.x);
CGFloat ymin = MIN(MIN(MIN(tr.y, tl.y), bl.y),br.y);
CGFloat xmax = MAX(MAX(MAX(tr.x, tl.x), bl.x),br.x);
CGFloat ymax = MAX(MAX(MAX(tr.y, tl.y), bl.y),br.y);
boundingBox.origin.x = xmin;
boundingBox.origin.y = ymin;
boundingBox.size.width = xmax - xmin;
boundingBox.size.height = ymax - ymin;
return boundingBox;
}
+ (CATransform3D)rectToQuad:(CGRect)rect
quadTL:(CGPoint)topLeft
quadTR:(CGPoint)topRight
quadBL:(CGPoint)bottomLeft
quadBR:(CGPoint)bottomRight
{
return [self rectToQuad:rect quadTLX:topLeft.x quadTLY:topLeft.y quadTRX:topRight.x quadTRY:topRight.y quadBLX:bottomLeft.x quadBLY:bottomLeft.y quadBRX:bottomRight.x quadBRY:bottomRight.y];
}
// http://stackoverflow.com/questions/9470493/transforming-a-rectangle-image-into-a-quadrilateral-using-a-catransform3d
+ (CATransform3D)rectToQuad:(CGRect)rect
quadTLX:(CGFloat)x1a
quadTLY:(CGFloat)y1a
quadTRX:(CGFloat)x2a
quadTRY:(CGFloat)y2a
quadBLX:(CGFloat)x3a
quadBLY:(CGFloat)y3a
quadBRX:(CGFloat)x4a
quadBRY:(CGFloat)y4a
{
CGFloat X = rect.origin.x;
CGFloat Y = rect.origin.y;
CGFloat W = rect.size.width;
CGFloat H = rect.size.height;
CGFloat y21 = y2a - y1a;
CGFloat y32 = y3a - y2a;
CGFloat y43 = y4a - y3a;
CGFloat y14 = y1a - y4a;
CGFloat y31 = y3a - y1a;
CGFloat y42 = y4a - y2a;
CGFloat a = -H*(x2a*x3a*y14 + x2a*x4a*y31 - x1a*x4a*y32 + x1a*x3a*y42);
CGFloat b = W*(x2a*x3a*y14 + x3a*x4a*y21 + x1a*x4a*y32 + x1a*x2a*y43);
CGFloat c = H*X*(x2a*x3a*y14 + x2a*x4a*y31 - x1a*x4a*y32 + x1a*x3a*y42) - H*W*x1a*(x4a*y32 - x3a*y42 + x2a*y43) - W*Y*(x2a*x3a*y14 + x3a*x4a*y21 + x1a*x4a*y32 + x1a*x2a*y43);
CGFloat d = H*(-x4a*y21*y3a + x2a*y1a*y43 - x1a*y2a*y43 - x3a*y1a*y4a + x3a*y2a*y4a);
CGFloat e = W*(x4a*y2a*y31 - x3a*y1a*y42 - x2a*y31*y4a + x1a*y3a*y42);
CGFloat f = -(W*(x4a*(Y*y2a*y31 + H*y1a*y32) - x3a*(H + Y)*y1a*y42 + H*x2a*y1a*y43 + x2a*Y*(y1a - y3a)*y4a + x1a*Y*y3a*(-y2a + y4a)) - H*X*(x4a*y21*y3a - x2a*y1a*y43 + x3a*(y1a - y2a)*y4a + x1a*y2a*(-y3a + y4a)));
CGFloat g = H*(x3a*y21 - x4a*y21 + (-x1a + x2a)*y43);
CGFloat h = W*(-x2a*y31 + x4a*y31 + (x1a - x3a)*y42);
CGFloat i = W*Y*(x2a*y31 - x4a*y31 - x1a*y42 + x3a*y42) + H*(X*(-(x3a*y21) + x4a*y21 + x1a*y43 - x2a*y43) + W*(-(x3a*y2a) + x4a*y2a + x2a*y3a - x4a*y3a - x2a*y4a + x3a*y4a));
const double kEpsilon = 0.0001;
if (fabs(i) < kEpsilon)
i = kEpsilon* (i > 0 ? 1.0 : -1.0);
CATransform3D transform = {a/i, d/i, 0, g/i, b/i, e/i, 0, h/i, 0, 0, 1, 0, c/i, f/i, 0, 1.0};
return transform;
}
@end

View File

@ -419,10 +419,21 @@ public func legacyAssetPickerEnqueueMessages(account: Account, signals: [Any]) -
case let .file(data, thumbnail, mimeType, name, caption):
switch data {
case let .tempFile(path):
var previewRepresentations: [TelegramMediaImageRepresentation] = []
if let thumbnail = thumbnail {
let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max))
let thumbnailSize = thumbnail.size.aspectFitted(CGSize(width: 320.0, height: 320.0))
let thumbnailImage = TGScaleImageToPixelSize(thumbnail, thumbnailSize)!
if let thumbnailData = thumbnailImage.jpegData(compressionQuality: 0.4) {
account.postbox.mediaBox.storeResourceData(resource.id, data: thumbnailData)
previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(thumbnailSize), resource: resource, progressiveSizes: [], immediateThumbnailData: nil))
}
}
var randomId: Int64 = 0
arc4random_buf(&randomId, 8)
let resource = LocalFileReferenceMediaResource(localFilePath: path, randomId: randomId)
let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: nil, attributes: [.FileName(fileName: name)])
let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), partialReference: nil, resource: resource, previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: nil, attributes: [.FileName(fileName: name)])
messages.append(LegacyAssetPickerEnqueueMessage(message: .message(text: caption ?? "", attributes: [], mediaReference: .standalone(media: media), replyToMessageId: nil, localGroupingKey: item.groupedId, correlationId: nil), uniqueId: item.uniqueId))
case let .asset(asset):
var randomId: Int64 = 0

View File

@ -20,6 +20,13 @@ public final class ManagedAnimationState {
var relativeTime: Double = 0.0
public var frameIndex: Int?
public var position: CGFloat {
if let frameIndex = frameIndex {
return CGFloat(frameIndex) / CGFloat(frameCount)
} else {
return 0.0
}
}
public var executedCallbacks = Set<Int>()

View File

@ -8,6 +8,7 @@
@class MTMessageTransaction;
@class MTApiEnvironment;
@class MTSessionInfo;
@class MTTransportScheme;
@protocol MTMessageService <NSObject>
@ -17,7 +18,7 @@
- (void)mtProtoDidAddService:(MTProto *)mtProto;
- (void)mtProtoDidRemoveService:(MTProto *)mtProto;
- (void)mtProtoPublicKeysUpdated:(MTProto *)mtProto datacenterId:(NSInteger)datacenterId publicKeys:(NSArray<NSDictionary *> *)publicKeys;
- (MTMessageTransaction *)mtProtoMessageTransaction:(MTProto *)mtProto authInfoSelector:(MTDatacenterAuthInfoSelector)authInfoSelector sessionInfo:(MTSessionInfo *)sessionInfo;
- (MTMessageTransaction *)mtProtoMessageTransaction:(MTProto *)mtProto authInfoSelector:(MTDatacenterAuthInfoSelector)authInfoSelector sessionInfo:(MTSessionInfo *)sessionInfo scheme:(MTTransportScheme *)scheme;
- (void)mtProtoDidChangeSession:(MTProto *)mtProto;
- (void)mtProtoServerDidChangeSession:(MTProto *)mtProto firstValidMessageId:(int64_t)firstValidMessageId otherValidMessageIds:(NSArray *)otherValidMessageIds;
- (void)mtProto:(MTProto *)mtProto receivedMessage:(MTIncomingMessage *)message authInfoSelector:(MTDatacenterAuthInfoSelector)authInfoSelector;

View File

@ -42,7 +42,7 @@
[mtProto requestTransportTransaction];
}
- (MTMessageTransaction *)mtProtoMessageTransaction:(MTProto *)mtProto authInfoSelector:(MTDatacenterAuthInfoSelector)authInfoSelector sessionInfo:(MTSessionInfo *)sessionInfo
- (MTMessageTransaction *)mtProtoMessageTransaction:(MTProto *)mtProto authInfoSelector:(MTDatacenterAuthInfoSelector)authInfoSelector sessionInfo:(MTSessionInfo *)sessionInfo scheme:(MTTransportScheme *)scheme
{
if (_currentTransactionId != nil) {
return nil;

View File

@ -207,8 +207,12 @@ typedef enum {
}
}
- (MTMessageTransaction *)mtProtoMessageTransaction:(MTProto *)mtProto authInfoSelector:(MTDatacenterAuthInfoSelector)authInfoSelector sessionInfo:(MTSessionInfo *)sessionInfo
- (MTMessageTransaction *)mtProtoMessageTransaction:(MTProto *)mtProto authInfoSelector:(MTDatacenterAuthInfoSelector)authInfoSelector sessionInfo:(MTSessionInfo *)sessionInfo scheme:(MTTransportScheme *)scheme
{
if (MTLogEnabled()) {
MTLog(@"[MTDatacenterAuthMessageService#%p mtProto#%p (media: %s) mtProtoMessageTransaction scheme:%@]", self, mtProto, mtProto.media ? "true" : "false", scheme);
}
if (_currentStageTransactionId == nil)
{
switch (_stage)

View File

@ -952,9 +952,9 @@ static const NSUInteger MTMaxUnacknowledgedMessageCount = 64;
NSMutableArray *messageServiceTransactions = [[NSMutableArray alloc] init];
for (id<MTMessageService> messageService in _messageServices)
{
if ([messageService respondsToSelector:@selector(mtProtoMessageTransaction:authInfoSelector:sessionInfo:)])
if ([messageService respondsToSelector:@selector(mtProtoMessageTransaction:authInfoSelector:sessionInfo:scheme:)])
{
MTMessageTransaction *messageTransaction = [messageService mtProtoMessageTransaction:self authInfoSelector:authInfoSelector sessionInfo:transactionSessionInfo];
MTMessageTransaction *messageTransaction = [messageService mtProtoMessageTransaction:self authInfoSelector:authInfoSelector sessionInfo:transactionSessionInfo scheme:scheme];
if (messageTransaction != nil)
{
for (MTOutgoingMessage *message in messageTransaction.messagePayload)

View File

@ -405,7 +405,7 @@
return currentData;
}
- (MTMessageTransaction *)mtProtoMessageTransaction:(MTProto *)mtProto authInfoSelector:(MTDatacenterAuthInfoSelector)authInfoSelector sessionInfo:(MTSessionInfo *)sessionInfo
- (MTMessageTransaction *)mtProtoMessageTransaction:(MTProto *)mtProto authInfoSelector:(MTDatacenterAuthInfoSelector)authInfoSelector sessionInfo:(MTSessionInfo *)sessionInfo scheme:(MTTransportScheme *)scheme
{
NSMutableArray *messages = nil;
NSMutableDictionary *requestInternalIdToMessageInternalId = nil;

View File

@ -41,7 +41,7 @@
[mtProto requestTransportTransaction];
}
- (MTMessageTransaction *)mtProtoMessageTransaction:(MTProto *)mtProto authInfoSelector:(MTDatacenterAuthInfoSelector)authInfoSelector sessionInfo:(MTSessionInfo *)sessionInfo
- (MTMessageTransaction *)mtProtoMessageTransaction:(MTProto *)mtProto authInfoSelector:(MTDatacenterAuthInfoSelector)authInfoSelector sessionInfo:(MTSessionInfo *)sessionInfo scheme:(MTTransportScheme *)scheme
{
if (_currentRequestMessageId == 0 || _currentRequestTransactionId == nil)
{

View File

@ -45,7 +45,7 @@
[mtProto requestTransportTransaction];
}
- (MTMessageTransaction *)mtProtoMessageTransaction:(MTProto *)mtProto authInfoSelector:(MTDatacenterAuthInfoSelector)authInfoSelector sessionInfo:(MTSessionInfo *)sessionInfo
- (MTMessageTransaction *)mtProtoMessageTransaction:(MTProto *)mtProto authInfoSelector:(MTDatacenterAuthInfoSelector)authInfoSelector sessionInfo:(MTSessionInfo *)sessionInfo scheme:(MTTransportScheme *)scheme
{
if (_currentTransactionId == nil)
{

View File

@ -93,11 +93,13 @@ private final class SemanticStatusNodeIconContext: SemanticStatusNodeStateContex
let transitionFraction: CGFloat
let icon: SemanticStatusNodeIcon
let iconImage: UIImage?
let iconOffset: CGFloat
init(transitionFraction: CGFloat, icon: SemanticStatusNodeIcon, iconImage: UIImage?) {
init(transitionFraction: CGFloat, icon: SemanticStatusNodeIcon, iconImage: UIImage?, iconOffset: CGFloat) {
self.transitionFraction = transitionFraction
self.icon = icon
self.iconImage = iconImage
self.iconOffset = iconOffset
super.init()
}
@ -125,11 +127,11 @@ private final class SemanticStatusNodeIconContext: SemanticStatusNodeStateContex
let diameter = size.width
let factor = diameter / 50.0
let size: CGSize
var offset: CGFloat = 0.0
if let iconImage = self.iconImage {
size = iconImage.size
offset = self.iconOffset
} else {
offset = 1.5
size = CGSize(width: 15.0, height: 18.0)
@ -161,12 +163,15 @@ private final class SemanticStatusNodeIconContext: SemanticStatusNodeStateContex
let factor = diameter / 50.0
let size: CGSize
let offset: CGFloat
if let iconImage = self.iconImage {
size = iconImage.size
offset = self.iconOffset
} else {
size = CGSize(width: 15.0, height: 16.0)
offset = 0.0
}
context.translateBy(x: (diameter - size.width) / 2.0, y: (diameter - size.height) / 2.0)
context.translateBy(x: (diameter - size.width) / 2.0 + offset, y: (diameter - size.height) / 2.0)
if (diameter < 40.0) {
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
context.scaleBy(x: factor, y: factor)
@ -248,6 +253,7 @@ private final class SemanticStatusNodeIconContext: SemanticStatusNodeStateContex
var animationNode: PlayPauseIconNode?
var iconImage: UIImage?
var iconOffset: CGFloat = 0.0
init(icon: SemanticStatusNodeIcon) {
self.icon = icon
@ -255,10 +261,20 @@ private final class SemanticStatusNodeIconContext: SemanticStatusNodeStateContex
if [.play, .pause].contains(icon) {
self.animationNode = PlayPauseIconNode()
self.animationNode?.imageUpdated = { [weak self] image in
self?.iconImage = image
self?.requestUpdate()
if let strongSelf = self {
strongSelf.iconImage = image
if var position = strongSelf.animationNode?.state?.position {
position = position * 2.0
if position > 1.0 {
position = 2.0 - position
}
strongSelf.iconOffset = (1.0 - position) * 1.5
}
strongSelf.requestUpdate()
}
}
self.iconImage = self.animationNode?.image
self.iconOffset = 1.5
}
}
@ -269,7 +285,7 @@ private final class SemanticStatusNodeIconContext: SemanticStatusNodeStateContex
var requestUpdate: () -> Void = {}
func drawingState(transitionFraction: CGFloat) -> SemanticStatusNodeStateDrawingState {
return DrawingState(transitionFraction: transitionFraction, icon: self.icon, iconImage: self.iconImage)
return DrawingState(transitionFraction: transitionFraction, icon: self.icon, iconImage: self.iconImage, iconOffset: self.iconOffset)
}
}

View File

@ -91,6 +91,7 @@ swift_library(
"//submodules/TextFormat:TextFormat",
"//submodules/Markdown:Markdown",
"//submodules/ChatTitleActivityNode:ChatTitleActivityNode",
"//third-party/libyuv:LibYuvBinding",
],
visibility = [
"//visibility:public",

View File

@ -26,7 +26,7 @@ private func interpolate(from: CGFloat, to: CGFloat, value: CGFloat) -> CGFloat
return (1.0 - value) * from + value * to
}
private final class CallVideoNode: ASDisplayNode {
private final class CallVideoNode: ASDisplayNode, PreviewVideoNode {
private let videoTransformContainer: ASDisplayNode
private let videoView: PresentationCallVideoView
@ -40,6 +40,11 @@ private final class CallVideoNode: ASDisplayNode {
private(set) var isReady: Bool = false
private var isReadyTimer: SwiftSignalKit.Timer?
private let readyPromise = ValuePromise(false)
var ready: Signal<Bool, NoError> {
return self.readyPromise.get()
}
private let isFlippedUpdated: (CallVideoNode) -> Void
private(set) var currentOrientation: PresentationCallVideoView.Orientation
@ -87,6 +92,7 @@ private final class CallVideoNode: ASDisplayNode {
}
if !strongSelf.isReady {
strongSelf.isReady = true
strongSelf.readyPromise.set(true)
strongSelf.isReadyTimer?.invalidate()
strongSelf.isReadyUpdated()
}
@ -122,6 +128,7 @@ private final class CallVideoNode: ASDisplayNode {
}
if !strongSelf.isReady {
strongSelf.isReady = true
strongSelf.readyPromise.set(true)
strongSelf.isReadyUpdated()
}
}, queue: .mainQueue())
@ -177,6 +184,10 @@ private final class CallVideoNode: ASDisplayNode {
})
}
func updateLayout(size: CGSize, layoutMode: VideoNodeLayoutMode, transition: ContainedViewLayoutTransition) {
self.updateLayout(size: size, cornerRadius: self.currentCornerRadius, isOutgoing: true, deviceOrientation: .portrait, isCompactLayout: false, transition: transition)
}
func updateLayout(size: CGSize, cornerRadius: CGFloat, isOutgoing: Bool, deviceOrientation: UIDeviceOrientation, isCompactLayout: Bool, transition: ContainedViewLayoutTransition) {
self.currentCornerRadius = cornerRadius
@ -582,13 +593,58 @@ final class CallControllerNode: ViewControllerTracingNode, CallControllerNodePro
strongSelf.call.requestVideo()
}
if strongSelf.displayedCameraConfirmation {
proceed()
} else {
strongSelf.present?(textAlertController(sharedContext: strongSelf.sharedContext, title: nil, text: strongSelf.presentationData.strings.Call_CameraConfirmationText, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Call_CameraConfirmationConfirm, action: {
proceed()
})]))
}
strongSelf.call.makeOutgoingVideoView(completion: { [weak self] outgoingVideoView in
guard let strongSelf = self else {
return
}
if let outgoingVideoView = outgoingVideoView {
outgoingVideoView.view.backgroundColor = .black
outgoingVideoView.view.clipsToBounds = true
var updateLayoutImpl: ((ContainerViewLayout, CGFloat) -> Void)?
let outgoingVideoNode = CallVideoNode(videoView: outgoingVideoView, disabledText: nil, assumeReadyAfterTimeout: true, isReadyUpdated: {
guard let strongSelf = self, let (layout, navigationBarHeight) = strongSelf.validLayout else {
return
}
updateLayoutImpl?(layout, navigationBarHeight)
}, orientationUpdated: {
guard let strongSelf = self, let (layout, navigationBarHeight) = strongSelf.validLayout else {
return
}
updateLayoutImpl?(layout, navigationBarHeight)
}, isFlippedUpdated: { _ in
guard let strongSelf = self, let (layout, navigationBarHeight) = strongSelf.validLayout else {
return
}
updateLayoutImpl?(layout, navigationBarHeight)
})
let controller = VoiceChatCameraPreviewController(sharedContext: strongSelf.sharedContext, cameraNode: outgoingVideoNode, shareCamera: { [weak self] _, _ in
if let strongSelf = self {
proceed()
}
}, switchCamera: { [weak self] in
Queue.mainQueue().after(0.1) {
self?.call.switchVideoCamera()
}
})
strongSelf.present?(controller)
updateLayoutImpl = { [weak controller] layout, navigationBarHeight in
controller?.containerLayoutUpdated(layout, transition: .immediate)
}
}
})
// if strongSelf.displayedCameraConfirmation {
// proceed()
// } else {
// strongSelf.present?(textAlertController(sharedContext: strongSelf.sharedContext, title: nil, text: strongSelf.presentationData.strings.Call_CameraConfirmationText, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Call_CameraConfirmationConfirm, action: {
// proceed()
// })]))
// }
})
} else {
strongSelf.call.disableVideo()

View File

@ -6,22 +6,20 @@ import SwiftSignalKit
import AccountContext
import ContextUI
final class GroupVideoNode: ASDisplayNode {
static let useBlurTransparency: Bool = !UIAccessibility.isReduceTransparencyEnabled
enum VideoNodeLayoutMode {
case fillOrFitToSquare
case fillHorizontal
case fillVertical
case fit
}
final class GroupVideoNode: ASDisplayNode, PreviewVideoNode {
enum Position {
case tile
case list
case mainstage
}
enum LayoutMode {
case fillOrFitToSquare
case fillHorizontal
case fillVertical
case fit
}
let sourceContainerNode: PinchSourceContainerNode
private let containerNode: ASDisplayNode
private let videoViewContainer: UIView
@ -29,7 +27,6 @@ final class GroupVideoNode: ASDisplayNode {
private let backdropVideoViewContainer: UIView
private let backdropVideoView: VideoRenderingView?
private var backdropEffectView: UIVisualEffectView?
private var effectView: UIVisualEffectView?
private var isBlurred: Bool = false
@ -37,7 +34,7 @@ final class GroupVideoNode: ASDisplayNode {
private var isEnabled: Bool = false
private var isBlurEnabled: Bool = false
private var validLayout: (CGSize, LayoutMode)?
private var validLayout: (CGSize, VideoNodeLayoutMode)?
var tapped: (() -> Void)?
@ -61,21 +58,6 @@ final class GroupVideoNode: ASDisplayNode {
super.init()
if let backdropVideoView = backdropVideoView {
self.backdropVideoViewContainer.addSubview(backdropVideoView)
self.view.addSubview(self.backdropVideoViewContainer)
let effect: UIVisualEffect
if #available(iOS 13.0, *) {
effect = UIBlurEffect(style: .systemThinMaterialDark)
} else {
effect = UIBlurEffect(style: .dark)
}
//let backdropEffectView = UIVisualEffectView(effect: effect)
//self.view.addSubview(backdropEffectView)
//self.backdropEffectView = backdropEffectView
}
self.videoViewContainer.addSubview(self.videoView)
self.addSubnode(self.sourceContainerNode)
self.containerNode.view.addSubview(self.videoViewContainer)
@ -204,7 +186,7 @@ final class GroupVideoNode: ASDisplayNode {
return rotatedAspect
}
func updateLayout(size: CGSize, layoutMode: LayoutMode, transition: ContainedViewLayoutTransition) {
func updateLayout(size: CGSize, layoutMode: VideoNodeLayoutMode, transition: ContainedViewLayoutTransition) {
self.validLayout = (size, layoutMode)
let bounds = CGRect(origin: CGPoint(), size: size)
self.sourceContainerNode.update(size: size, transition: .immediate)
@ -310,20 +292,6 @@ final class GroupVideoNode: ASDisplayNode {
self.backdropVideoView?.updateIsEnabled(self.isEnabled && self.isBlurEnabled)
if self.isBlurEnabled {
self.backdropVideoView?.isHidden = false
self.backdropEffectView?.isHidden = false
}
transition.updatePosition(layer: backdropVideoView.layer, position: rotatedVideoFrame.center, force: true, completion: { [weak self] value in
guard let strongSelf = self, value else {
return
}
if !strongSelf.isBlurEnabled {
strongSelf.backdropVideoView?.updateIsEnabled(false)
strongSelf.backdropVideoView?.isHidden = true
strongSelf.backdropEffectView?.isHidden = false
}
})
transition.updateBounds(layer: backdropVideoView.layer, bounds: CGRect(origin: CGPoint(), size: normalizedVideoSize))
let transformScale: CGFloat = rotatedVideoFrame.width / normalizedVideoSize.width
@ -334,21 +302,6 @@ final class GroupVideoNode: ASDisplayNode {
transition.updateTransformRotation(view: backdropVideoView, angle: angle)
}
if let backdropEffectView = self.backdropEffectView {
let maxSide = max(bounds.width, bounds.height) + 32.0
let squareBounds = CGRect(x: (bounds.width - maxSide) / 2.0, y: (bounds.height - maxSide) / 2.0, width: maxSide, height: maxSide)
if case let .animated(duration, .spring) = transition {
UIView.animate(withDuration: duration, delay: 0.0, usingSpringWithDamping: 500.0, initialSpringVelocity: 0.0, options: .layoutSubviews, animations: {
backdropEffectView.frame = squareBounds
})
} else {
transition.animateView {
backdropEffectView.frame = squareBounds
}
}
}
if let effectView = self.effectView {
if case let .animated(duration, .spring) = transition {
UIView.animate(withDuration: duration, delay: 0.0, usingSpringWithDamping: 500.0, initialSpringVelocity: 0.0, options: .layoutSubviews, animations: {

View File

@ -13,6 +13,7 @@ import TelegramPresentationData
import DeviceAccess
import UniversalMediaPlayer
import AccountContext
import DeviceProximity
final class PresentationCallToneRenderer {
let queue: Queue
@ -287,6 +288,8 @@ public final class PresentationCallImpl: PresentationCall {
private var useFrontCamera: Bool = true
private var videoCapturer: OngoingCallVideoCapturer?
private var proximityManagerIndex: Int?
init(
account: Account,
audioSession: ManagedAudioSession,
@ -455,6 +458,11 @@ public final class PresentationCallImpl: PresentationCall {
strongSelf.updateIsAudioSessionActive(value)
}
})
if callKitIntegration == nil {
self.proximityManagerIndex = DeviceProximityManager.shared().add { _ in
}
}
}
deinit {
@ -473,6 +481,10 @@ public final class PresentationCallImpl: PresentationCall {
self.callKitIntegration?.dropCall(uuid: self.internalId)
}
}
if let proximityManagerIndex = self.proximityManagerIndex {
DeviceProximityManager.shared().remove(proximityManagerIndex)
}
}
private func updateSessionState(sessionState: CallSession, callContextState: OngoingCallContextState?, reception: Int32?, audioSessionControl: ManagedAudioSessionControl?) {

View File

@ -6,6 +6,7 @@ import SwiftSignalKit
import AccountContext
import TelegramVoip
import AVFoundation
import LibYuvBinding
private func sampleBufferFromPixelBuffer(pixelBuffer: CVPixelBuffer) -> CMSampleBuffer? {
var maybeFormat: CMVideoFormatDescription?
@ -40,6 +41,68 @@ private func sampleBufferFromPixelBuffer(pixelBuffer: CVPixelBuffer) -> CMSample
return sampleBuffer
}
private func copyI420BufferToNV12Buffer(buffer: OngoingGroupCallContext.VideoFrameData.I420Buffer, pixelBuffer: CVPixelBuffer) -> Bool {
guard CVPixelBufferGetPixelFormatType(pixelBuffer) == kCVPixelFormatType_420YpCbCr8BiPlanarFullRange else {
return false
}
guard CVPixelBufferGetWidthOfPlane(pixelBuffer, 0) == buffer.width else {
return false
}
guard CVPixelBufferGetHeightOfPlane(pixelBuffer, 0) == buffer.height else {
return false
}
let cvRet = CVPixelBufferLockBaseAddress(pixelBuffer, [])
if cvRet != kCVReturnSuccess {
return false
}
defer {
CVPixelBufferUnlockBaseAddress(pixelBuffer, [])
}
guard let dstY = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0) else {
return false
}
let dstStrideY = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 0)
guard let dstUV = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 1) else {
return false
}
let dstStrideUV = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 1)
buffer.y.withUnsafeBytes { srcYBuffer in
guard let srcY = srcYBuffer.baseAddress else {
return
}
buffer.u.withUnsafeBytes { srcUBuffer in
guard let srcU = srcUBuffer.baseAddress else {
return
}
buffer.v.withUnsafeBytes { srcVBuffer in
guard let srcV = srcVBuffer.baseAddress else {
return
}
libyuv_I420ToNV12(
srcY.assumingMemoryBound(to: UInt8.self),
Int32(buffer.strideY),
srcU.assumingMemoryBound(to: UInt8.self),
Int32(buffer.strideU),
srcV.assumingMemoryBound(to: UInt8.self),
Int32(buffer.strideV),
dstY.assumingMemoryBound(to: UInt8.self),
Int32(dstStrideY),
dstUV.assumingMemoryBound(to: UInt8.self),
Int32(dstStrideUV),
Int32(buffer.width),
Int32(buffer.height)
)
}
}
}
return true
}
final class SampleBufferVideoRenderingView: UIView, VideoRenderingView {
static override var layerClass: AnyClass {
return AVSampleBufferDisplayLayer.self
@ -111,6 +174,28 @@ final class SampleBufferVideoRenderingView: UIView, VideoRenderingView {
if let sampleBuffer = sampleBufferFromPixelBuffer(pixelBuffer: buffer.pixelBuffer) {
self.sampleBufferLayer.enqueue(sampleBuffer)
}
case let .i420(buffer):
let ioSurfaceProperties = NSMutableDictionary()
let options = NSMutableDictionary()
options.setObject(ioSurfaceProperties, forKey: kCVPixelBufferIOSurfacePropertiesKey as NSString)
var pixelBuffer: CVPixelBuffer?
CVPixelBufferCreate(
kCFAllocatorDefault,
buffer.width,
buffer.height,
kCVPixelFormatType_420YpCbCr8BiPlanarFullRange,
options,
&pixelBuffer
)
if let pixelBuffer = pixelBuffer {
if copyI420BufferToNV12Buffer(buffer: buffer, pixelBuffer: pixelBuffer) {
if let sampleBuffer = sampleBufferFromPixelBuffer(pixelBuffer: pixelBuffer) {
self.sampleBufferLayer.enqueue(sampleBuffer)
}
}
}
default:
break
}

View File

@ -35,11 +35,17 @@ class VideoRenderingContext {
func makeView(input: Signal<OngoingGroupCallContext.VideoFrameData, NoError>, blur: Bool) -> VideoRenderingView? {
#if targetEnvironment(simulator)
if blur {
return nil
}
return SampleBufferVideoRenderingView(input: input)
#else
if #available(iOS 13.0, *) {
return MetalVideoRenderingView(renderingContext: self.metalContext, input: input, blur: blur)
} else {
if blur {
return nil
}
return SampleBufferVideoRenderingView(input: input)
}
#endif

View File

@ -15,23 +15,32 @@ import ReplayKit
private let accentColor: UIColor = UIColor(rgb: 0x007aff)
protocol PreviewVideoNode: ASDisplayNode {
var ready: Signal<Bool, NoError> { get }
func flip(withBackground: Bool)
func updateIsBlurred(isBlurred: Bool, light: Bool, animated: Bool)
func updateLayout(size: CGSize, layoutMode: VideoNodeLayoutMode, transition: ContainedViewLayoutTransition)
}
final class VoiceChatCameraPreviewController: ViewController {
private var controllerNode: VoiceChatCameraPreviewControllerNode {
return self.displayNode as! VoiceChatCameraPreviewControllerNode
}
private let context: AccountContext
private let sharedContext: SharedAccountContext
private var animatedIn = false
private let cameraNode: GroupVideoNode
private let cameraNode: PreviewVideoNode
private let shareCamera: (ASDisplayNode, Bool) -> Void
private let switchCamera: () -> Void
private var presentationDataDisposable: Disposable?
init(context: AccountContext, cameraNode: GroupVideoNode, shareCamera: @escaping (ASDisplayNode, Bool) -> Void, switchCamera: @escaping () -> Void) {
self.context = context
init(sharedContext: SharedAccountContext, cameraNode: PreviewVideoNode, shareCamera: @escaping (ASDisplayNode, Bool) -> Void, switchCamera: @escaping () -> Void) {
self.sharedContext = sharedContext
self.cameraNode = cameraNode
self.shareCamera = shareCamera
self.switchCamera = switchCamera
@ -42,7 +51,7 @@ final class VoiceChatCameraPreviewController: ViewController {
self.blocksBackgroundWhenInOverlay = true
self.presentationDataDisposable = (context.sharedContext.presentationData
self.presentationDataDisposable = (sharedContext.presentationData
|> deliverOnMainQueue).start(next: { [weak self] presentationData in
if let strongSelf = self {
strongSelf.controllerNode.updatePresentationData(presentationData)
@ -61,7 +70,7 @@ final class VoiceChatCameraPreviewController: ViewController {
}
override public func loadDisplayNode() {
self.displayNode = VoiceChatCameraPreviewControllerNode(controller: self, context: self.context, cameraNode: self.cameraNode)
self.displayNode = VoiceChatCameraPreviewControllerNode(controller: self, sharedContext: self.sharedContext, cameraNode: self.cameraNode)
self.controllerNode.shareCamera = { [weak self] unmuted in
if let strongSelf = self {
strongSelf.shareCamera(strongSelf.cameraNode, unmuted)
@ -106,10 +115,10 @@ final class VoiceChatCameraPreviewController: ViewController {
private class VoiceChatCameraPreviewControllerNode: ViewControllerTracingNode, UIScrollViewDelegate {
private weak var controller: VoiceChatCameraPreviewController?
private let context: AccountContext
private let sharedContext: SharedAccountContext
private var presentationData: PresentationData
private let cameraNode: GroupVideoNode
private let cameraNode: PreviewVideoNode
private let dimNode: ASDisplayNode
private let wrappingScrollNode: ASScrollNode
private let contentContainerNode: ASDisplayNode
@ -119,8 +128,7 @@ private class VoiceChatCameraPreviewControllerNode: ViewControllerTracingNode, U
private let titleNode: ASTextNode
private let previewContainerNode: ASDisplayNode
private let shimmerNode: ShimmerEffectForegroundNode
private let cameraButton: SolidRoundedButtonNode
private let screenButton: SolidRoundedButtonNode
private let doneButton: SolidRoundedButtonNode
private var broadcastPickerView: UIView?
private let cancelButton: SolidRoundedButtonNode
@ -128,9 +136,11 @@ private class VoiceChatCameraPreviewControllerNode: ViewControllerTracingNode, U
private let microphoneEffectView: UIVisualEffectView
private let microphoneIconNode: VoiceChatMicrophoneNode
private let switchCameraButton: HighlightTrackingButtonNode
private let switchCameraEffectView: UIVisualEffectView
private let switchCameraIconNode: ASImageNode
private let placeholderTextNode: ImmediateTextNode
private let placeholderIconNode: ASImageNode
private let tabsNode: TabsSegmentedControlNode
private var selectedTabIndex: Int = 0
private var containerLayout: (ContainerViewLayout, CGFloat)?
@ -145,10 +155,10 @@ private class VoiceChatCameraPreviewControllerNode: ViewControllerTracingNode, U
var dismiss: (() -> Void)?
var cancel: (() -> Void)?
init(controller: VoiceChatCameraPreviewController, context: AccountContext, cameraNode: GroupVideoNode) {
init(controller: VoiceChatCameraPreviewController, sharedContext: SharedAccountContext, cameraNode: PreviewVideoNode) {
self.controller = controller
self.context = context
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.sharedContext = sharedContext
self.presentationData = sharedContext.currentPresentationData.with { $0 }
self.cameraNode = cameraNode
@ -185,16 +195,14 @@ private class VoiceChatCameraPreviewControllerNode: ViewControllerTracingNode, U
self.titleNode = ASTextNode()
self.titleNode.attributedText = NSAttributedString(string: title, font: Font.bold(17.0), textColor: textColor)
self.cameraButton = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(backgroundColor: accentColor, foregroundColor: .white), font: .bold, height: 52.0, cornerRadius: 11.0, gloss: false)
self.cameraButton.title = self.presentationData.strings.VoiceChat_VideoPreviewShareCamera
self.screenButton = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(backgroundColor: buttonColor, foregroundColor: buttonTextColor), font: .bold, height: 52.0, cornerRadius: 11.0, gloss: false)
self.screenButton.title = self.presentationData.strings.VoiceChat_VideoPreviewShareScreen
self.doneButton = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(backgroundColor: accentColor, foregroundColor: .white), font: .bold, height: 52.0, cornerRadius: 11.0, gloss: false)
self.doneButton.title = self.presentationData.strings.VoiceChat_VideoPreviewContinue
if #available(iOS 12.0, *) {
let broadcastPickerView = RPSystemBroadcastPickerView(frame: CGRect(x: 0, y: 0, width: 50, height: 52.0))
broadcastPickerView.alpha = 0.02
broadcastPickerView.preferredExtension = "\(self.context.sharedContext.applicationBindings.appBundleId).BroadcastUpload"
broadcastPickerView.isHidden = true
broadcastPickerView.preferredExtension = "\(self.sharedContext.applicationBindings.appBundleId).BroadcastUpload"
broadcastPickerView.showsMicrophoneButton = false
self.broadcastPickerView = broadcastPickerView
}
@ -212,24 +220,26 @@ private class VoiceChatCameraPreviewControllerNode: ViewControllerTracingNode, U
self.microphoneButton = HighlightTrackingButtonNode()
self.microphoneButton.isSelected = true
self.microphoneEffectView = UIVisualEffectView(effect: UIBlurEffect(style: .dark))
self.microphoneEffectView = UIVisualEffectView(effect: UIBlurEffect(style: .light))
self.microphoneEffectView.clipsToBounds = true
self.microphoneEffectView.layer.cornerRadius = 24.0
self.microphoneEffectView.isUserInteractionEnabled = false
self.microphoneIconNode = VoiceChatMicrophoneNode()
// self.microphoneIconNode.alpha = 0.75
self.microphoneIconNode.update(state: .init(muted: false, filled: true, color: .white), animated: false)
self.switchCameraButton = HighlightTrackingButtonNode()
self.switchCameraEffectView = UIVisualEffectView(effect: UIBlurEffect(style: .dark))
self.switchCameraEffectView.clipsToBounds = true
self.switchCameraEffectView.layer.cornerRadius = 24.0
self.switchCameraEffectView.isUserInteractionEnabled = false
self.tabsNode = TabsSegmentedControlNode(items: [TabsSegmentedControlNode.Item(title: "Front Camera"), TabsSegmentedControlNode.Item(title: "Back Camera"), TabsSegmentedControlNode.Item(title: "Share Screen")], selectedIndex: 0)
self.switchCameraIconNode = ASImageNode()
self.switchCameraIconNode.displaysAsynchronously = false
self.switchCameraIconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Call/SwitchCameraIcon"), color: .white)
self.switchCameraIconNode.contentMode = .center
self.placeholderTextNode = ImmediateTextNode()
self.placeholderTextNode.alpha = 0.0
self.placeholderTextNode.maximumNumberOfLines = 3
self.placeholderTextNode.textAlignment = .center
self.placeholderIconNode = ASImageNode()
self.placeholderIconNode.alpha = 0.0
self.placeholderIconNode.contentMode = .scaleAspectFit
self.placeholderIconNode.displaysAsynchronously = false
super.init()
@ -248,8 +258,7 @@ private class VoiceChatCameraPreviewControllerNode: ViewControllerTracingNode, U
self.backgroundNode.addSubnode(self.effectNode)
self.backgroundNode.addSubnode(self.contentBackgroundNode)
self.contentContainerNode.addSubnode(self.titleNode)
self.contentContainerNode.addSubnode(self.cameraButton)
self.contentContainerNode.addSubnode(self.screenButton)
self.contentContainerNode.addSubnode(self.doneButton)
if let broadcastPickerView = self.broadcastPickerView {
self.contentContainerNode.view.addSubview(broadcastPickerView)
}
@ -258,14 +267,40 @@ private class VoiceChatCameraPreviewControllerNode: ViewControllerTracingNode, U
self.contentContainerNode.addSubnode(self.previewContainerNode)
self.previewContainerNode.addSubnode(self.cameraNode)
self.previewContainerNode.addSubnode(self.microphoneButton)
self.microphoneButton.view.addSubview(self.microphoneEffectView)
self.microphoneButton.addSubnode(self.microphoneIconNode)
self.previewContainerNode.addSubnode(self.switchCameraButton)
self.switchCameraButton.view.addSubview(self.switchCameraEffectView)
self.switchCameraButton.addSubnode(self.switchCameraIconNode)
self.cameraButton.pressed = { [weak self] in
self.previewContainerNode.addSubnode(self.placeholderIconNode)
self.previewContainerNode.addSubnode(self.placeholderTextNode)
if self.cameraNode is GroupVideoNode {
self.previewContainerNode.addSubnode(self.microphoneButton)
self.microphoneButton.view.addSubview(self.microphoneEffectView)
self.microphoneButton.addSubnode(self.microphoneIconNode)
}
self.previewContainerNode.addSubnode(self.tabsNode)
self.tabsNode.selectedIndexChanged = { [weak self] index in
if let strongSelf = self {
if (index == 0 && strongSelf.selectedTabIndex == 1) || (index == 1 && strongSelf.selectedTabIndex == 0) {
strongSelf.switchCamera?()
}
if index == 2 && [0, 1].contains(strongSelf.selectedTabIndex) {
strongSelf.broadcastPickerView?.isHidden = false
strongSelf.cameraNode.updateIsBlurred(isBlurred: true, light: false, animated: true)
let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .easeInOut)
transition.updateAlpha(node: strongSelf.placeholderTextNode, alpha: 1.0)
transition.updateAlpha(node: strongSelf.placeholderIconNode, alpha: 1.0)
} else if [0, 1].contains(index) && strongSelf.selectedTabIndex == 2 {
strongSelf.broadcastPickerView?.isHidden = true
strongSelf.cameraNode.updateIsBlurred(isBlurred: false, light: false, animated: true)
let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .easeInOut)
transition.updateAlpha(node: strongSelf.placeholderTextNode, alpha: 0.0)
transition.updateAlpha(node: strongSelf.placeholderIconNode, alpha: 0.0)
}
strongSelf.selectedTabIndex = index
}
}
self.doneButton.pressed = { [weak self] in
if let strongSelf = self {
strongSelf.shareCamera?(strongSelf.microphoneButton.isSelected)
}
@ -289,19 +324,6 @@ private class VoiceChatCameraPreviewControllerNode: ViewControllerTracingNode, U
}
}
self.switchCameraButton.addTarget(self, action: #selector(self.switchCameraPressed), forControlEvents: .touchUpInside)
self.switchCameraButton.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
let transition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: .spring)
transition.updateSublayerTransformScale(node: strongSelf.switchCameraButton, scale: 0.9)
} else {
let transition: ContainedViewLayoutTransition = .animated(duration: 0.5, curve: .spring)
transition.updateSublayerTransformScale(node: strongSelf.switchCameraButton, scale: 1.0)
}
}
}
self.readyDisposable.set(self.cameraNode.ready.start(next: { [weak self] ready in
if let strongSelf = self, ready {
Queue.mainQueue().after(0.07) {
@ -323,18 +345,6 @@ private class VoiceChatCameraPreviewControllerNode: ViewControllerTracingNode, U
self.microphoneIconNode.update(state: .init(muted: !self.microphoneButton.isSelected, filled: true, color: .white), animated: true)
}
@objc private func switchCameraPressed() {
self.hapticFeedback.impact(.light)
self.switchCamera?()
let springDuration: Double = 0.7
let springDamping: CGFloat = 100.0
self.switchCameraButton.isUserInteractionEnabled = false
self.switchCameraIconNode.layer.animateSpring(from: 0.0 as NSNumber, to: CGFloat.pi as NSNumber, keyPath: "transform.rotation.z", duration: springDuration, damping: springDamping, completion: { [weak self] _ in
self?.switchCameraButton.isUserInteractionEnabled = true
})
}
func updatePresentationData(_ presentationData: PresentationData) {
self.presentationData = presentationData
}
@ -368,7 +378,7 @@ private class VoiceChatCameraPreviewControllerNode: ViewControllerTracingNode, U
self.dimNode.position = dimPosition
})
self.applicationStateDisposable = (self.context.sharedContext.applicationBindings.applicationIsActive
self.applicationStateDisposable = (self.sharedContext.applicationBindings.applicationIsActive
|> filter { !$0 }
|> take(1)
|> deliverOnMainQueue).start(next: { [weak self] _ in
@ -443,9 +453,6 @@ private class VoiceChatCameraPreviewControllerNode: ViewControllerTracingNode, U
insets.top = max(10.0, insets.top)
var buttonOffset: CGFloat = 60.0
if let _ = self.broadcastPickerView {
buttonOffset *= 2.0
}
let bottomInset: CGFloat = isTablet ? 31.0 : 10.0 + cleanInsets.bottom
let titleHeight: CGFloat = 54.0
var contentHeight = titleHeight + bottomInset + 52.0 + 17.0
@ -522,24 +529,27 @@ private class VoiceChatCameraPreviewControllerNode: ViewControllerTracingNode, U
self.cameraNode.frame = CGRect(origin: CGPoint(), size: previewSize)
self.cameraNode.updateLayout(size: previewSize, layoutMode: isLandscape ? .fillHorizontal : .fillVertical, transition: .immediate)
let microphoneFrame = CGRect(x: 16.0, y: previewSize.height - 48.0 - 16.0, width: 48.0, height: 48.0)
let microphoneFrame = CGRect(x: 8.0, y: previewSize.height - 48.0 - 8.0 - 48.0, width: 48.0, height: 48.0)
transition.updateFrame(node: self.microphoneButton, frame: microphoneFrame)
transition.updateFrame(view: self.microphoneEffectView, frame: CGRect(origin: CGPoint(), size: microphoneFrame.size))
transition.updateFrameAsPositionAndBounds(node: self.microphoneIconNode, frame: CGRect(origin: CGPoint(x: 1.0, y: 0.0), size: microphoneFrame.size).insetBy(dx: 6.0, dy: 6.0))
self.microphoneIconNode.transform = CATransform3DMakeScale(1.2, 1.2, 1.0)
let switchCameraFrame = CGRect(x: previewSize.width - 48.0 - 16.0, y: previewSize.height - 48.0 - 16.0, width: 48.0, height: 48.0)
transition.updateFrame(node: self.switchCameraButton, frame: switchCameraFrame)
transition.updateFrame(view: self.switchCameraEffectView, frame: CGRect(origin: CGPoint(), size: switchCameraFrame.size))
transition.updateFrame(node: self.switchCameraIconNode, frame: CGRect(origin: CGPoint(), size: switchCameraFrame.size))
let tabsFrame = CGRect(x: 8.0, y: previewSize.height - 40.0 - 8.0, width: previewSize.width - 16.0, height: 40.0)
self.tabsNode.updateLayout(size: tabsFrame.size, transition: transition)
transition.updateFrame(node: self.tabsNode, frame: tabsFrame)
self.placeholderTextNode.attributedText = NSAttributedString(string: presentationData.strings.VoiceChat_VideoPreviewShareScreenInfo, font: Font.semibold(14.0), textColor: .white)
self.placeholderIconNode.image = generateTintedImage(image: UIImage(bundleImageName: isTablet ? "Call/ScreenShareTablet" : "Call/ScreenSharePhone"), color: .white)
let placeholderTextSize = self.placeholderTextNode.updateLayout(CGSize(width: previewSize.width - 80.0, height: 100.0))
transition.updateFrame(node: self.placeholderTextNode, frame: CGRect(origin: CGPoint(x: floor((previewSize.width - placeholderTextSize.width) / 2.0), y: floorToScreenPixels(previewSize.height / 2.0) + 10.0), size: placeholderTextSize))
if let imageSize = self.placeholderIconNode.image?.size {
transition.updateFrame(node: self.placeholderIconNode, frame: CGRect(origin: CGPoint(x: floor((previewSize.width - imageSize.width) / 2.0), y: floorToScreenPixels(previewSize.height / 2.0) - imageSize.height - 8.0), size: imageSize))
}
if isLandscape {
var buttonsCount: Int = 2
if let _ = self.broadcastPickerView {
buttonsCount += 1
} else {
self.screenButton.isHidden = true
}
let buttonInset: CGFloat = 6.0
var leftButtonInset = buttonInset
@ -552,32 +562,18 @@ private class VoiceChatCameraPreviewControllerNode: ViewControllerTracingNode, U
}
let buttonWidth = floorToScreenPixels((availableWidth - CGFloat(buttonsCount + 1) * buttonInset) / CGFloat(buttonsCount))
let cameraButtonHeight = self.cameraButton.updateLayout(width: buttonWidth, transition: transition)
let screenButtonHeight = self.screenButton.updateLayout(width: buttonWidth, transition: transition)
let cameraButtonHeight = self.doneButton.updateLayout(width: buttonWidth, transition: transition)
let cancelButtonHeight = self.cancelButton.updateLayout(width: buttonWidth, transition: transition)
transition.updateFrame(node: self.cancelButton, frame: CGRect(x: layout.safeInsets.left + leftButtonInset, y: previewFrame.maxY + 10.0, width: buttonWidth, height: cancelButtonHeight))
if let broadcastPickerView = self.broadcastPickerView {
transition.updateFrame(node: self.screenButton, frame: CGRect(x: layout.safeInsets.left + leftButtonInset + buttonWidth + buttonInset, y: previewFrame.maxY + 10.0, width: buttonWidth, height: screenButtonHeight))
broadcastPickerView.frame = CGRect(x: layout.safeInsets.left + leftButtonInset + buttonWidth + buttonInset, y: previewFrame.maxY + 10.0, width: buttonWidth, height: screenButtonHeight)
transition.updateFrame(node: self.cameraButton, frame: CGRect(x: layout.safeInsets.left + leftButtonInset + buttonWidth + buttonInset + buttonWidth + buttonInset, y: previewFrame.maxY + 10.0, width: buttonWidth, height: cameraButtonHeight))
} else {
transition.updateFrame(node: self.cameraButton, frame: CGRect(x: layout.safeInsets.left + leftButtonInset + buttonWidth + buttonInset, y: previewFrame.maxY + 10.0, width: buttonWidth, height: cameraButtonHeight))
}
transition.updateFrame(node: self.doneButton, frame: CGRect(x: layout.safeInsets.left + leftButtonInset + buttonWidth + buttonInset, y: previewFrame.maxY + 10.0, width: buttonWidth, height: cameraButtonHeight))
self.broadcastPickerView?.frame = self.doneButton.frame
} else {
let bottomInset = isTablet ? 21.0 : insets.bottom + 16.0
let buttonInset: CGFloat = 16.0
let cameraButtonHeight = self.cameraButton.updateLayout(width: contentFrame.width - buttonInset * 2.0, transition: transition)
transition.updateFrame(node: self.cameraButton, frame: CGRect(x: buttonInset, y: contentHeight - cameraButtonHeight - bottomInset - buttonOffset, width: contentFrame.width, height: cameraButtonHeight))
let screenButtonHeight = self.screenButton.updateLayout(width: contentFrame.width - buttonInset * 2.0, transition: transition)
transition.updateFrame(node: self.screenButton, frame: CGRect(x: buttonInset, y: contentHeight - cameraButtonHeight - 8.0 - screenButtonHeight - bottomInset, width: contentFrame.width, height: screenButtonHeight))
if let broadcastPickerView = self.broadcastPickerView {
broadcastPickerView.frame = CGRect(x: buttonInset, y: contentHeight - cameraButtonHeight - 8.0 - screenButtonHeight - bottomInset, width: contentFrame.width + 1000.0, height: screenButtonHeight)
} else {
self.screenButton.isHidden = true
}
let cameraButtonHeight = self.doneButton.updateLayout(width: contentFrame.width - buttonInset * 2.0, transition: transition)
transition.updateFrame(node: self.doneButton, frame: CGRect(x: buttonInset, y: contentHeight - cameraButtonHeight - bottomInset - buttonOffset, width: contentFrame.width, height: cameraButtonHeight))
self.broadcastPickerView?.frame = self.doneButton.frame
let cancelButtonHeight = self.cancelButton.updateLayout(width: contentFrame.width - buttonInset * 2.0, transition: transition)
transition.updateFrame(node: self.cancelButton, frame: CGRect(x: buttonInset, y: contentHeight - cancelButtonHeight - bottomInset, width: contentFrame.width, height: cancelButtonHeight))
@ -586,3 +582,303 @@ private class VoiceChatCameraPreviewControllerNode: ViewControllerTracingNode, U
transition.updateFrame(node: self.contentContainerNode, frame: contentFrame)
}
}
private let textFont = Font.medium(14.0)
class TabsSegmentedControlNode: ASDisplayNode, UIGestureRecognizerDelegate {
struct Item: Equatable {
public let title: String
public init(title: String) {
self.title = title
}
}
private var blurEffectView: UIVisualEffectView?
private var vibrancyEffectView: UIVisualEffectView?
private let selectionNode: ASDisplayNode
private var itemNodes: [HighlightTrackingButtonNode]
private var highlightedItemNodes: [HighlightTrackingButtonNode]
private var validLayout: CGSize?
private var _items: [Item]
private var _selectedIndex: Int = 0
public var selectedIndex: Int {
get {
return self._selectedIndex
}
set {
guard newValue != self._selectedIndex else {
return
}
self._selectedIndex = newValue
if let size = self.validLayout {
self.updateLayout(size: size, transition: .immediate)
}
}
}
public func setSelectedIndex(_ index: Int, animated: Bool) {
guard index != self._selectedIndex else {
return
}
self._selectedIndex = index
if let size = self.validLayout {
self.updateLayout(size: size, transition: .animated(duration: 0.2, curve: .easeInOut))
}
}
public var selectedIndexChanged: (Int) -> Void = { _ in }
private var gestureRecognizer: UIPanGestureRecognizer?
private var gestureSelectedIndex: Int?
public init(items: [Item], selectedIndex: Int) {
self._items = items
self._selectedIndex = selectedIndex
self.selectionNode = ASDisplayNode()
self.selectionNode.clipsToBounds = true
self.selectionNode.backgroundColor = .black
self.selectionNode.alpha = 0.75
self.itemNodes = items.map { item in
let itemNode = HighlightTrackingButtonNode()
itemNode.contentEdgeInsets = UIEdgeInsets(top: 0.0, left: 8.0, bottom: 0.0, right: 8.0)
itemNode.imageNode.isHidden = true
itemNode.titleNode.maximumNumberOfLines = 1
itemNode.titleNode.truncationMode = .byTruncatingTail
itemNode.titleNode.alpha = 0.75
itemNode.accessibilityLabel = item.title
itemNode.accessibilityTraits = [.button]
itemNode.setTitle(item.title, with: textFont, with: .black, for: .normal)
return itemNode
}
self.highlightedItemNodes = items.map { item in
let itemNode = HighlightTrackingButtonNode()
itemNode.isUserInteractionEnabled = false
itemNode.isHidden = true
itemNode.contentEdgeInsets = UIEdgeInsets(top: 0.0, left: 8.0, bottom: 0.0, right: 8.0)
itemNode.imageNode.isHidden = true
itemNode.titleNode.maximumNumberOfLines = 1
itemNode.titleNode.truncationMode = .byTruncatingTail
itemNode.setTitle(item.title, with: textFont, with: .white, for: .normal)
return itemNode
}
super.init()
self.clipsToBounds = true
if #available(iOS 13.0, *) {
self.layer.cornerCurve = .continuous
self.selectionNode.layer.cornerCurve = .continuous
}
self.setupButtons()
}
override func didLoad() {
super.didLoad()
self.view.disablesInteractiveTransitionGestureRecognizer = true
let gestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:)))
gestureRecognizer.delegate = self
self.view.addGestureRecognizer(gestureRecognizer)
self.gestureRecognizer = gestureRecognizer
let blurEffect = UIBlurEffect(style: .light)
let blurEffectView = UIVisualEffectView(effect: blurEffect)
self.blurEffectView = blurEffectView
self.view.addSubview(blurEffectView)
let vibrancyEffect: UIVibrancyEffect
if #available(iOS 13.0, *) {
vibrancyEffect = UIVibrancyEffect(blurEffect: blurEffect, style: .label)
} else {
vibrancyEffect = UIVibrancyEffect(blurEffect: blurEffect)
}
let vibrancyEffectView = UIVisualEffectView(effect: vibrancyEffect)
self.vibrancyEffectView = vibrancyEffectView
blurEffectView.contentView.addSubview(vibrancyEffectView)
self.itemNodes.forEach(vibrancyEffectView.contentView.addSubnode(_:))
vibrancyEffectView.contentView.addSubnode(self.selectionNode)
self.highlightedItemNodes.forEach(self.addSubnode(_:))
}
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
self.validLayout = size
let bounds = CGRect(origin: CGPoint(), size: size)
self.cornerRadius = size.height / 2.0
if let blurEffectView = self.blurEffectView {
transition.updateFrame(view: blurEffectView, frame: bounds)
}
if let vibrancyEffectView = self.vibrancyEffectView {
transition.updateFrame(view: vibrancyEffectView, frame: bounds)
}
let selectedIndex: Int
if let gestureSelectedIndex = self.gestureSelectedIndex {
selectedIndex = gestureSelectedIndex
} else {
selectedIndex = self.selectedIndex
}
if !self.itemNodes.isEmpty {
let itemSize = CGSize(width: floorToScreenPixels(size.width / CGFloat(self.itemNodes.count)), height: size.height)
let selectionFrame = CGRect(origin: CGPoint(x: itemSize.width * CGFloat(selectedIndex), y: 0.0), size: itemSize).insetBy(dx: 4.0, dy: 4.0)
transition.updateFrameAsPositionAndBounds(node: self.selectionNode, frame: selectionFrame)
self.selectionNode.cornerRadius = selectionFrame.height / 2.0
for i in 0 ..< self.itemNodes.count {
let itemNode = self.itemNodes[i]
let highlightedItemNode = self.highlightedItemNodes[i]
let _ = itemNode.measure(itemSize)
transition.updateFrame(node: itemNode, frame: CGRect(origin: CGPoint(x: itemSize.width * CGFloat(i), y: (size.height - itemSize.height) / 2.0), size: itemSize))
transition.updateFrame(node: highlightedItemNode, frame: CGRect(origin: CGPoint(x: itemSize.width * CGFloat(i), y: (size.height - itemSize.height) / 2.0), size: itemSize))
let isSelected = selectedIndex == i
if itemNode.isSelected != isSelected {
if case .animated = transition {
UIView.transition(with: itemNode.view, duration: 0.2, options: .transitionCrossDissolve, animations: {
itemNode.isSelected = isSelected
highlightedItemNode.isHidden = !isSelected
}, completion: nil)
} else {
itemNode.isSelected = isSelected
highlightedItemNode.isHidden = !isSelected
}
if isSelected {
itemNode.accessibilityTraits.insert(.selected)
} else {
itemNode.accessibilityTraits.remove(.selected)
}
}
}
}
}
private func setupButtons() {
for i in 0 ..< self.itemNodes.count {
let itemNode = self.itemNodes[i]
itemNode.addTarget(self, action: #selector(self.buttonPressed(_:)), forControlEvents: .touchUpInside)
itemNode.highligthedChanged = { [weak self, weak itemNode] highlighted in
if let strongSelf = self, let itemNode = itemNode {
let transition = ContainedViewLayoutTransition.animated(duration: 0.25, curve: .easeInOut)
if strongSelf.selectedIndex == i {
if let gestureRecognizer = strongSelf.gestureRecognizer, case .began = gestureRecognizer.state {
} else {
strongSelf.updateButtonsHighlights(highlightedIndex: highlighted ? i : nil, gestureSelectedIndex: strongSelf.gestureSelectedIndex)
}
} else if highlighted {
transition.updateAlpha(node: itemNode, alpha: 0.4)
}
if !highlighted {
transition.updateAlpha(node: itemNode, alpha: 1.0)
}
}
}
}
}
private func updateButtonsHighlights(highlightedIndex: Int?, gestureSelectedIndex: Int?) {
let transition = ContainedViewLayoutTransition.animated(duration: 0.25, curve: .easeInOut)
if highlightedIndex == nil && gestureSelectedIndex == nil {
transition.updateTransformScale(node: self.selectionNode, scale: 1.0)
} else {
transition.updateTransformScale(node: self.selectionNode, scale: 0.96)
}
for i in 0 ..< self.itemNodes.count {
let itemNode = self.itemNodes[i]
let highlightedItemNode = self.highlightedItemNodes[i]
if i == highlightedIndex || i == gestureSelectedIndex {
transition.updateTransformScale(node: itemNode, scale: 0.96)
transition.updateTransformScale(node: highlightedItemNode, scale: 0.96)
} else {
transition.updateTransformScale(node: itemNode, scale: 1.0)
transition.updateTransformScale(node: highlightedItemNode, scale: 1.0)
}
}
}
private func updateButtonsHighlights() {
let transition = ContainedViewLayoutTransition.animated(duration: 0.25, curve: .easeInOut)
if let gestureSelectedIndex = self.gestureSelectedIndex {
for i in 0 ..< self.itemNodes.count {
let itemNode = self.itemNodes[i]
let highlightedItemNode = self.highlightedItemNodes[i]
transition.updateTransformScale(node: itemNode, scale: i == gestureSelectedIndex ? 0.96 : 1.0)
transition.updateTransformScale(node: highlightedItemNode, scale: i == gestureSelectedIndex ? 0.96 : 1.0)
}
} else {
for itemNode in self.itemNodes {
transition.updateTransformScale(node: itemNode, scale: 1.0)
}
for itemNode in self.highlightedItemNodes {
transition.updateTransformScale(node: itemNode, scale: 1.0)
}
}
}
@objc private func buttonPressed(_ button: HighlightTrackingButtonNode) {
guard let index = self.itemNodes.firstIndex(of: button) else {
return
}
self._selectedIndex = index
self.selectedIndexChanged(index)
if let size = self.validLayout {
self.updateLayout(size: size, transition: .animated(duration: 0.2, curve: .slide))
}
}
public override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
return self.selectionNode.frame.contains(gestureRecognizer.location(in: self.view))
}
@objc private func panGesture(_ recognizer: UIPanGestureRecognizer) {
let location = recognizer.location(in: self.view)
switch recognizer.state {
case .changed:
if !self.selectionNode.frame.contains(location) {
let point = CGPoint(x: max(0.0, min(self.bounds.width, location.x)), y: 1.0)
for i in 0 ..< self.itemNodes.count {
let itemNode = self.itemNodes[i]
if itemNode.frame.contains(point) {
if i != self.gestureSelectedIndex {
self.gestureSelectedIndex = i
self.updateButtonsHighlights(highlightedIndex: nil, gestureSelectedIndex: i)
if let size = self.validLayout {
self.updateLayout(size: size, transition: .animated(duration: 0.35, curve: .slide))
}
}
break
}
}
}
case .ended:
if let gestureSelectedIndex = self.gestureSelectedIndex {
if gestureSelectedIndex != self.selectedIndex {
self._selectedIndex = gestureSelectedIndex
self.selectedIndexChanged(gestureSelectedIndex)
}
self.gestureSelectedIndex = nil
}
self.updateButtonsHighlights(highlightedIndex: nil, gestureSelectedIndex: nil)
default:
break
}
}
}

View File

@ -3495,7 +3495,7 @@ public final class VoiceChatController: ViewController {
let input = videoCapturer.video()
if let videoView = strongSelf.videoRenderingContext.makeView(input: input, blur: false) {
let cameraNode = GroupVideoNode(videoView: videoView, backdropVideoView: nil)
let controller = VoiceChatCameraPreviewController(context: strongSelf.context, cameraNode: cameraNode, shareCamera: { [weak self] _, unmuted in
let controller = VoiceChatCameraPreviewController(sharedContext: strongSelf.context.sharedContext, cameraNode: cameraNode, shareCamera: { [weak self] _, unmuted in
if let strongSelf = self {
strongSelf.call.setIsMuted(action: unmuted ? .unmuted : .muted(isPushToTalkActive: false))
(strongSelf.call as! PresentationGroupCallImpl).requestVideo(capturer: videoCapturer)
@ -3512,29 +3512,6 @@ public final class VoiceChatController: ViewController {
})
strongSelf.controller?.present(controller, in: .window(.root))
}
/*strongSelf.call.makeOutgoingVideoView(requestClone: false, completion: { [weak self] view, _ in
guard let strongSelf = self, let view = view else {
return
}
let cameraNode = GroupVideoNode(videoView: view, backdropVideoView: nil)
let controller = VoiceChatCameraPreviewController(context: strongSelf.context, cameraNode: cameraNode, shareCamera: { [weak self] videoNode, unmuted in
if let strongSelf = self {
strongSelf.call.setIsMuted(action: unmuted ? .unmuted : .muted(isPushToTalkActive: false))
strongSelf.call.requestVideo()
if let (layout, navigationHeight) = strongSelf.validLayout {
strongSelf.animatingButtonsSwap = true
strongSelf.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.4, curve: .spring))
}
}
}, switchCamera: { [weak self] in
Queue.mainQueue().after(0.1) {
self?.call.switchVideoCamera()
}
})
strongSelf.controller?.present(controller, in: .window(.root))
})*/
})
}
}

View File

@ -1090,7 +1090,7 @@ final class VoiceChatMainStageNode: ASDisplayNode {
let initialBottomInset = bottomInset
var bottomInset = bottomInset
let layoutMode: GroupVideoNode.LayoutMode
let layoutMode: VideoNodeLayoutMode
if case .immediate = transition, self.animatingIn {
layoutMode = .fillOrFitToSquare
bottomInset = 0.0

View File

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

View File

@ -0,0 +1,161 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 5.334991 5.334999 cm
0.000000 0.000000 0.000000 scn
6.665000 19.330002 m
7.032269 19.330002 7.330000 19.032270 7.330000 18.665001 c
7.330000 15.865002 l
7.330000 15.837518 l
7.330000 15.837514 l
7.330000 15.837511 l
7.330009 15.300820 7.330016 14.857977 7.300543 14.497252 c
7.269935 14.122624 7.204253 13.778399 7.039532 13.455116 c
6.784030 12.953665 6.376337 12.545971 5.874885 12.290468 c
5.551602 12.125748 5.207376 12.060066 4.832748 12.029457 c
4.472024 11.999985 4.029181 11.999992 3.492490 12.000001 c
3.492487 12.000001 l
3.492483 12.000001 l
3.465000 12.000001 l
0.665000 12.000001 l
0.297731 12.000001 0.000000 12.297731 0.000000 12.665001 c
0.000000 13.032270 0.297731 13.330001 0.665000 13.330001 c
3.465000 13.330001 l
4.036026 13.330001 4.424301 13.330519 4.724444 13.355041 c
5.016824 13.378929 5.166537 13.422241 5.271077 13.475508 c
5.522274 13.603498 5.726503 13.807728 5.854494 14.058924 c
5.907760 14.163464 5.951072 14.313177 5.974960 14.605556 c
5.999483 14.905701 6.000000 15.293976 6.000000 15.865002 c
6.000000 18.665001 l
6.000000 19.032270 6.297730 19.330002 6.665000 19.330002 c
h
13.330000 18.665001 m
13.330000 19.032270 13.032269 19.330002 12.665000 19.330002 c
12.297730 19.330002 12.000000 19.032270 12.000000 18.665001 c
12.000000 15.865002 l
12.000000 15.837526 l
12.000000 15.837497 l
11.999991 15.300814 11.999985 14.857974 12.029457 14.497252 c
12.060065 14.122624 12.125747 13.778399 12.290467 13.455116 c
12.545970 12.953665 12.953663 12.545971 13.455115 12.290468 c
13.778399 12.125748 14.122623 12.060066 14.497252 12.029457 c
14.857978 11.999985 15.300823 11.999992 15.837517 12.000001 c
15.865000 12.000001 l
18.665001 12.000001 l
19.032270 12.000001 19.330002 12.297731 19.330002 12.665001 c
19.330002 13.032270 19.032270 13.330001 18.665001 13.330001 c
15.865000 13.330001 l
15.293975 13.330001 14.905700 13.330519 14.605556 13.355041 c
14.313176 13.378929 14.163464 13.422241 14.058923 13.475508 c
13.807726 13.603498 13.603498 13.807728 13.475507 14.058924 c
13.422240 14.163464 13.378928 14.313177 13.355040 14.605556 c
13.330517 14.905701 13.330000 15.293976 13.330000 15.865002 c
13.330000 18.665001 l
h
15.865000 7.330001 m
15.837525 7.330001 l
15.837497 7.330001 l
15.300811 7.330009 14.857973 7.330016 14.497252 7.300544 c
14.122623 7.269936 13.778399 7.204254 13.455115 7.039534 c
12.953663 6.784031 12.545970 6.376338 12.290467 5.874886 c
12.125747 5.551602 12.060065 5.207377 12.029457 4.832749 c
11.999985 4.472028 11.999991 4.029190 12.000000 3.492504 c
12.000000 3.492476 l
12.000000 3.465000 l
12.000000 0.665001 l
12.000000 0.297731 12.297730 0.000000 12.665000 0.000000 c
13.032269 0.000000 13.330000 0.297731 13.330000 0.665001 c
13.330000 3.465000 l
13.330000 4.036026 13.330517 4.424301 13.355040 4.724444 c
13.378928 5.016825 13.422240 5.166537 13.475507 5.271078 c
13.603498 5.522275 13.807726 5.726503 14.058923 5.854494 c
14.163464 5.907761 14.313176 5.951073 14.605556 5.974961 c
14.905700 5.999484 15.293975 6.000001 15.865000 6.000001 c
18.665001 6.000001 l
19.032270 6.000001 19.330002 6.297731 19.330002 6.665001 c
19.330002 7.032270 19.032270 7.330001 18.665001 7.330001 c
15.865000 7.330001 l
h
3.465000 6.000001 m
4.036026 6.000001 4.424301 5.999484 4.724444 5.974961 c
5.016824 5.951073 5.166537 5.907761 5.271077 5.854494 c
5.522274 5.726503 5.726503 5.522275 5.854494 5.271078 c
5.907760 5.166537 5.951072 5.016825 5.974960 4.724444 c
5.999483 4.424301 6.000000 4.036026 6.000000 3.465000 c
6.000000 0.665001 l
6.000000 0.297731 6.297730 0.000000 6.665000 0.000000 c
7.032269 0.000000 7.330000 0.297731 7.330000 0.665001 c
7.330000 3.465000 l
7.330000 3.492484 l
7.330009 4.029178 7.330016 4.472023 7.300543 4.832749 c
7.269935 5.207377 7.204253 5.551602 7.039532 5.874886 c
6.784030 6.376338 6.376337 6.784031 5.874885 7.039534 c
5.551602 7.204254 5.207376 7.269936 4.832748 7.300544 c
4.472027 7.330016 4.029188 7.330009 3.492504 7.330001 c
3.492474 7.330001 l
3.465000 7.330001 l
0.665000 7.330001 l
0.297731 7.330001 0.000000 7.032270 0.000000 6.665001 c
0.000000 6.297731 0.297731 6.000001 0.665000 6.000001 c
3.465000 6.000001 l
h
f*
n
Q
endstream
endobj
3 0 obj
4194
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 30.000000 30.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Type /Catalog
/Pages 5 0 R
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000004284 00000 n
0000004307 00000 n
0000004480 00000 n
0000004554 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
4613
%%EOF

View File

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

View File

@ -0,0 +1,92 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 3.334999 3.334999 cm
0.000000 0.000000 0.000000 scn
8.665000 16.000002 m
4.613991 16.000002 1.330000 12.716011 1.330000 8.665002 c
1.330000 4.613994 4.613991 1.330002 8.665000 1.330002 c
12.716008 1.330002 16.000000 4.613994 16.000000 8.665002 c
16.000000 12.716011 12.716008 16.000002 8.665000 16.000002 c
h
0.000000 8.665002 m
0.000000 13.450549 3.879453 17.330002 8.665000 17.330002 c
13.450547 17.330002 17.330002 13.450549 17.330002 8.665002 c
17.330002 3.879455 13.450547 0.000000 8.665000 0.000000 c
3.879453 0.000000 0.000000 3.879455 0.000000 8.665002 c
h
8.665000 12.830002 m
9.032269 12.830002 9.330000 12.532271 9.330000 12.165002 c
9.330000 6.770453 l
11.194775 8.635228 l
11.454473 8.894926 11.875527 8.894926 12.135225 8.635228 c
12.394924 8.375529 12.394924 7.954474 12.135225 7.694776 c
9.135226 4.694776 l
8.875527 4.435078 8.454473 4.435078 8.194774 4.694776 c
5.194774 7.694776 l
4.935075 7.954474 4.935075 8.375529 5.194774 8.635228 c
5.454473 8.894926 5.875527 8.894926 6.135226 8.635228 c
8.000000 6.770453 l
8.000000 12.165002 l
8.000000 12.532271 8.297730 12.830002 8.665000 12.830002 c
h
f*
n
Q
endstream
endobj
3 0 obj
1188
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 24.000000 24.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Type /Catalog
/Pages 5 0 R
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000001278 00000 n
0000001301 00000 n
0000001474 00000 n
0000001548 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
1607
%%EOF

View File

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

View File

@ -0,0 +1,161 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 5.334991 5.334970 cm
0.000000 0.000000 0.000000 scn
15.465001 18.000031 m
16.036026 18.000031 16.424301 17.999512 16.724445 17.974989 c
17.016825 17.951101 17.166538 17.907789 17.271078 17.854523 c
17.522274 17.726532 17.726503 17.522303 17.854494 17.271107 c
17.907761 17.166567 17.951073 17.016853 17.974960 16.724474 c
17.999483 16.424330 18.000000 16.036055 18.000000 15.465029 c
18.000000 12.665030 l
18.000000 12.297760 18.297731 12.000029 18.665001 12.000029 c
19.032270 12.000029 19.330002 12.297760 19.330002 12.665030 c
19.330002 15.465029 l
19.330002 15.492513 l
19.330002 15.492586 l
19.330009 16.029245 19.330015 16.472069 19.300545 16.832777 c
19.269936 17.207407 19.204254 17.551632 19.039534 17.874914 c
18.784031 18.376366 18.376337 18.784060 17.874886 19.039562 c
17.551603 19.204283 17.207378 19.269964 16.832748 19.300573 c
16.472025 19.330046 16.029184 19.330038 15.492496 19.330030 c
15.492476 19.330030 l
15.465001 19.330030 l
12.665001 19.330030 l
12.297731 19.330030 12.000001 19.032299 12.000001 18.665030 c
12.000001 18.297760 12.297731 18.000031 12.665001 18.000031 c
15.465001 18.000031 l
h
3.865001 19.330030 m
3.837527 19.330030 l
3.837508 19.330030 l
3.300819 19.330038 2.857976 19.330046 2.497253 19.300573 c
2.122624 19.269964 1.778399 19.204283 1.455116 19.039562 c
0.953664 18.784060 0.545971 18.376366 0.290469 17.874914 c
0.125748 17.551632 0.060066 17.207407 0.029458 16.832777 c
-0.000015 16.472054 -0.000008 16.029211 0.000000 15.492522 c
0.000000 15.492504 l
0.000000 15.465029 l
0.000000 12.665030 l
0.000000 12.297760 0.297732 12.000029 0.665001 12.000029 c
1.032270 12.000029 1.330001 12.297760 1.330001 12.665030 c
1.330001 15.465029 l
1.330001 16.036055 1.330518 16.424330 1.355041 16.724474 c
1.378929 17.016853 1.422241 17.166567 1.475507 17.271107 c
1.603498 17.522303 1.807727 17.726532 2.058923 17.854523 c
2.163464 17.907789 2.313177 17.951101 2.605557 17.974989 c
2.905700 17.999512 3.293975 18.000031 3.865001 18.000031 c
6.665001 18.000031 l
7.032271 18.000031 7.330001 18.297760 7.330001 18.665030 c
7.330001 19.032299 7.032271 19.330030 6.665001 19.330030 c
3.865001 19.330030 l
h
1.330001 6.665030 m
1.330001 7.032299 1.032270 7.330029 0.665001 7.330029 c
0.297732 7.330029 0.000000 7.032299 0.000000 6.665030 c
0.000000 3.865030 l
0.000000 3.837555 l
0.000000 3.837535 l
-0.000008 3.300846 -0.000015 2.858006 0.029458 2.497282 c
0.060066 2.122652 0.125748 1.778427 0.290469 1.455145 c
0.545971 0.953693 0.953664 0.546000 1.455116 0.290497 c
1.778399 0.125776 2.122624 0.060095 2.497253 0.029486 c
2.857963 0.000015 3.300784 0.000021 3.837445 0.000029 c
3.837518 0.000029 l
3.865001 0.000029 l
6.665001 0.000029 l
7.032271 0.000029 7.330001 0.297760 7.330001 0.665030 c
7.330001 1.032299 7.032271 1.330030 6.665001 1.330030 c
3.865001 1.330030 l
3.293975 1.330030 2.905700 1.330547 2.605557 1.355070 c
2.313177 1.378958 2.163464 1.422270 2.058923 1.475536 c
1.807727 1.603527 1.603498 1.807756 1.475507 2.058952 c
1.422241 2.163492 1.378929 2.313206 1.355041 2.605585 c
1.330518 2.905729 1.330001 3.294004 1.330001 3.865030 c
1.330001 6.665030 l
h
18.665001 7.330029 m
19.032270 7.330029 19.330002 7.032299 19.330002 6.665030 c
19.330002 3.865030 l
19.330002 3.837546 l
19.330002 3.837475 l
19.330009 3.300812 19.330015 2.857990 19.300545 2.497282 c
19.269936 2.122652 19.204254 1.778427 19.039534 1.455145 c
18.784031 0.953693 18.376337 0.546000 17.874886 0.290497 c
17.551603 0.125776 17.207378 0.060095 16.832748 0.029486 c
16.472040 0.000015 16.029217 0.000021 15.492556 0.000029 c
15.492484 0.000029 l
15.465001 0.000029 l
12.665001 0.000029 l
12.297731 0.000029 12.000001 0.297760 12.000001 0.665030 c
12.000001 1.032299 12.297731 1.330030 12.665001 1.330030 c
15.465001 1.330030 l
16.036026 1.330030 16.424301 1.330547 16.724445 1.355070 c
17.016825 1.378958 17.166538 1.422270 17.271078 1.475536 c
17.522274 1.603527 17.726503 1.807756 17.854494 2.058952 c
17.907761 2.163492 17.951073 2.313206 17.974960 2.605585 c
17.999483 2.905729 18.000000 3.294004 18.000000 3.865030 c
18.000000 6.665030 l
18.000000 7.032299 18.297731 7.330029 18.665001 7.330029 c
h
f*
n
Q
endstream
endobj
3 0 obj
4198
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 30.000000 30.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Type /Catalog
/Pages 5 0 R
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000004288 00000 n
0000004311 00000 n
0000004484 00000 n
0000004558 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
4617
%%EOF

View File

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

View File

@ -0,0 +1,113 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 3.335007 3.334999 cm
0.000000 0.000000 0.000000 scn
0.000007 16.665001 m
0.000007 17.032270 0.297737 17.330002 0.665007 17.330002 c
1.165007 17.330002 l
1.532276 17.330002 1.830007 17.032270 1.830007 16.665001 c
1.830007 16.297733 1.532276 16.000002 1.165007 16.000002 c
0.665007 16.000002 l
0.297737 16.000002 0.000007 16.297733 0.000007 16.665001 c
h
9.665000 17.330002 m
9.297730 17.330002 9.000000 17.032270 9.000000 16.665001 c
9.000000 16.297731 9.297730 16.000000 9.665000 16.000000 c
16.665001 16.000000 l
17.032270 16.000000 17.330002 16.297731 17.330002 16.665001 c
17.330002 17.032270 17.032270 17.330002 16.665001 17.330002 c
9.665000 17.330002 l
h
9.665000 1.330002 m
9.297730 1.330002 9.000000 1.032270 9.000000 0.665001 c
9.000000 0.297731 9.297730 0.000000 9.665000 0.000000 c
16.665001 0.000000 l
17.032270 0.000000 17.330002 0.297731 17.330002 0.665001 c
17.330002 1.032270 17.032270 1.330002 16.665001 1.330002 c
9.665000 1.330002 l
h
0.665000 1.330002 m
0.297731 1.330002 0.000000 1.032270 0.000000 0.665001 c
0.000000 0.297731 0.297731 0.000000 0.665000 0.000000 c
1.165000 0.000000 l
1.532269 0.000000 1.830000 0.297731 1.830000 0.665001 c
1.830000 1.032270 1.532269 1.330002 1.165000 1.330002 c
0.665000 1.330002 l
h
3.000000 16.665001 m
3.000000 17.032270 3.297731 17.330002 3.665000 17.330002 c
7.165000 17.330002 l
7.532269 17.330002 7.830000 17.032270 7.830000 16.665001 c
7.830000 16.297731 7.532269 16.000000 7.165000 16.000000 c
3.665000 16.000000 l
3.297731 16.000000 3.000000 16.297731 3.000000 16.665001 c
h
3.665000 1.330002 m
3.297731 1.330002 3.000000 1.032270 3.000000 0.665001 c
3.000000 0.297731 3.297731 0.000000 3.665000 0.000000 c
7.165000 0.000000 l
7.532269 0.000000 7.830000 0.297731 7.830000 0.665001 c
7.830000 1.032270 7.532269 1.330002 7.165000 1.330002 c
3.665000 1.330002 l
h
f*
n
Q
endstream
endobj
3 0 obj
1901
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 24.000000 24.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Type /Catalog
/Pages 5 0 R
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000001991 00000 n
0000002014 00000 n
0000002187 00000 n
0000002261 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
2320
%%EOF

View File

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

View File

@ -0,0 +1,113 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 5.335007 5.334999 cm
0.000000 0.000000 0.000000 scn
0.000000 18.665001 m
0.000000 19.032270 0.297731 19.330002 0.665000 19.330002 c
1.665000 19.330002 l
2.032269 19.330002 2.330000 19.032270 2.330000 18.665001 c
2.330000 18.297733 2.032269 18.000002 1.665000 18.000002 c
0.665000 18.000002 l
0.297731 18.000002 0.000000 18.297733 0.000000 18.665001 c
h
11.665000 19.330002 m
11.297730 19.330002 11.000000 19.032270 11.000000 18.665001 c
11.000000 18.297733 11.297730 18.000002 11.665000 18.000002 c
18.665001 18.000002 l
19.032269 18.000002 19.330000 18.297733 19.330000 18.665001 c
19.330000 19.032270 19.032269 19.330002 18.665001 19.330002 c
11.665000 19.330002 l
h
11.665000 1.330002 m
11.297730 1.330002 11.000000 1.032270 11.000000 0.665001 c
11.000000 0.297731 11.297730 0.000000 11.665000 0.000000 c
18.665001 0.000000 l
19.032269 0.000000 19.330000 0.297731 19.330000 0.665001 c
19.330000 1.032270 19.032269 1.330002 18.665001 1.330002 c
11.665000 1.330002 l
h
0.665000 1.330002 m
0.297731 1.330002 0.000000 1.032270 0.000000 0.665001 c
0.000000 0.297731 0.297731 0.000000 0.665000 0.000000 c
1.665000 0.000000 l
2.032269 0.000000 2.330000 0.297731 2.330000 0.665001 c
2.330000 1.032270 2.032269 1.330002 1.665000 1.330002 c
0.665000 1.330002 l
h
4.000000 18.665001 m
4.000000 19.032270 4.297730 19.330002 4.665000 19.330002 c
8.665000 19.330002 l
9.032269 19.330002 9.330000 19.032270 9.330000 18.665001 c
9.330000 18.297733 9.032269 18.000002 8.665000 18.000002 c
4.665000 18.000002 l
4.297730 18.000002 4.000000 18.297733 4.000000 18.665001 c
h
4.665000 1.330002 m
4.297730 1.330002 4.000000 1.032270 4.000000 0.665001 c
4.000000 0.297731 4.297730 0.000000 4.665000 0.000000 c
8.665000 0.000000 l
9.032269 0.000000 9.330000 0.297731 9.330000 0.665001 c
9.330000 1.032270 9.032269 1.330002 8.665000 1.330002 c
4.665000 1.330002 l
h
f*
n
Q
endstream
endobj
3 0 obj
1917
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 30.000000 30.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Type /Catalog
/Pages 5 0 R
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000002007 00000 n
0000002030 00000 n
0000002203 00000 n
0000002277 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
2336
%%EOF

Some files were not shown because too many files have changed in this diff Show More