From 1e6e92c3dee4440ac7e54b2d2aa5b54956ba41b2 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Mon, 3 Aug 2020 21:04:52 +0300 Subject: [PATCH] Initial document scan implementation --- .../Telegram-iOS/en.lproj/Localizable.strings | 2 + .../Sources/ChatListSearchItemHeader.swift | 5 + .../Sources/ContactListActionItem.swift | 14 +- .../LegacyComponents/LegacyComponents.h | 1 - .../PublicHeaders/LegacyComponents/PGCamera.h | 6 +- .../LegacyComponents/PGCameraCaptureSession.h | 2 + .../LegacyComponents/PGCameraShotMetadata.h | 3 + .../LegacyComponents/PGPhotoEditorValues.h | 7 + .../LegacyComponents/TGCameraCapturedPhoto.h | 4 + .../TGCameraInterfaceAssets.h | 3 +- .../LegacyComponents/TGCameraMainView.h | 8 +- .../LegacyComponents/TGCameraSegmentsView.h | 20 - .../LegacyComponents/Sources/PGCamera.m | 10 + .../Sources/PGCameraCaptureSession.m | 25 +- .../Sources/PGPhotoEditorValues.m | 24 +- .../Sources/PGRectangleDetector.h | 24 + .../Sources/PGRectangleDetector.m | 362 +++++++++ .../Sources/TGCameraCapturedPhoto.m | 22 + .../Sources/TGCameraController.m | 352 +++++++-- .../Sources/TGCameraFlashControl.m | 10 +- .../Sources/TGCameraFlipButton.m | 19 +- .../Sources/TGCameraFocusCrosshairsControl.m | 4 +- .../Sources/TGCameraInterfaceAssets.m | 54 +- .../Sources/TGCameraMainPhoneView.m | 16 +- .../Sources/TGCameraMainView.m | 10 +- .../Sources/TGCameraModeControl.m | 25 +- .../Sources/TGCameraRectangleView.h | 14 + .../Sources/TGCameraRectangleView.m | 102 +++ .../Sources/TGCameraSegmentsView.m | 260 ------- .../Sources/TGCameraShutterButton.m | 11 +- .../Sources/TGCameraTimeCodeView.m | 2 +- .../Sources/TGCameraToastView.h | 7 + .../Sources/TGCameraToastView.m | 70 ++ submodules/LegacyComponents/Sources/TGFont.mm | 2 +- .../Sources/TGMediaEditingContext.m | 4 +- .../Sources/TGPhotoAvatarCropController.h | 26 - .../Sources/TGPhotoAvatarCropController.m | 707 ------------------ .../Sources/TGPhotoPaintController.m | 6 +- .../Sources/TGPhotoRectangleCropController.h | 6 + .../Sources/TGPhotoRectangleCropController.m | 5 + .../LegacyComponents/Sources/TGWarpedView.h | 7 + .../LegacyComponents/Sources/TGWarpedView.m | 92 +++ .../Sources/LegacyMediaPickers.swift | 13 +- .../Sources/ChannelParticipantsScreen.swift | 610 +++++++++++++++ 44 files changed, 1846 insertions(+), 1130 deletions(-) delete mode 100644 submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGCameraSegmentsView.h create mode 100644 submodules/LegacyComponents/Sources/PGRectangleDetector.h create mode 100644 submodules/LegacyComponents/Sources/PGRectangleDetector.m create mode 100644 submodules/LegacyComponents/Sources/TGCameraRectangleView.h create mode 100644 submodules/LegacyComponents/Sources/TGCameraRectangleView.m delete mode 100644 submodules/LegacyComponents/Sources/TGCameraSegmentsView.m create mode 100644 submodules/LegacyComponents/Sources/TGCameraToastView.h create mode 100644 submodules/LegacyComponents/Sources/TGCameraToastView.m delete mode 100644 submodules/LegacyComponents/Sources/TGPhotoAvatarCropController.h delete mode 100644 submodules/LegacyComponents/Sources/TGPhotoAvatarCropController.m create mode 100644 submodules/LegacyComponents/Sources/TGPhotoRectangleCropController.h create mode 100644 submodules/LegacyComponents/Sources/TGPhotoRectangleCropController.m create mode 100644 submodules/LegacyComponents/Sources/TGWarpedView.h create mode 100644 submodules/LegacyComponents/Sources/TGWarpedView.m create mode 100644 submodules/TelegramUI/Sources/ChannelParticipantsScreen.swift diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 0f31ec04a2..90dcc3fcb4 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -5715,3 +5715,5 @@ Any member of this group will be able to see messages in the channel."; "Stats.MessageOverview" = "Overview"; "Stats.MessageInteractionsTitle" = "Interactions"; "Stats.MessagePublicForwardsTitle" = "Public Shares"; + +"Camera.ScanMode" = "SCAN"; diff --git a/submodules/ChatListSearchItemHeader/Sources/ChatListSearchItemHeader.swift b/submodules/ChatListSearchItemHeader/Sources/ChatListSearchItemHeader.swift index 1cb70fd606..be2929a5ab 100644 --- a/submodules/ChatListSearchItemHeader/Sources/ChatListSearchItemHeader.swift +++ b/submodules/ChatListSearchItemHeader/Sources/ChatListSearchItemHeader.swift @@ -22,6 +22,7 @@ public enum ChatListSearchItemHeaderType: Int32 { case chats case chatTypes case faq + case otherSubscribers } public final class ChatListSearchItemHeader: ListViewItemHeader { @@ -110,6 +111,8 @@ public final class ChatListSearchItemHeaderNode: ListViewItemHeaderNode { self.sectionHeaderNode.title = strings.ChatList_ChatTypesSection.uppercased() case .faq: self.sectionHeaderNode.title = strings.Settings_FrequentlyAskedQuestions.uppercased() + case .otherSubscribers: + self.sectionHeaderNode.title = "OTHER SUBSCRIBERS" } self.sectionHeaderNode.action = actionTitle @@ -162,6 +165,8 @@ public final class ChatListSearchItemHeaderNode: ListViewItemHeaderNode { self.sectionHeaderNode.title = strings.ChatList_ChatTypesSection.uppercased() case .faq: self.sectionHeaderNode.title = strings.Settings_FrequentlyAskedQuestions.uppercased() + case .otherSubscribers: + self.sectionHeaderNode.title = "OTHER SUBSCRIBERS" } self.sectionHeaderNode.action = actionTitle diff --git a/submodules/ContactListUI/Sources/ContactListActionItem.swift b/submodules/ContactListUI/Sources/ContactListActionItem.swift index 34e838e162..f0e708ef16 100644 --- a/submodules/ContactListUI/Sources/ContactListActionItem.swift +++ b/submodules/ContactListUI/Sources/ContactListActionItem.swift @@ -14,16 +14,16 @@ public enum ContactListActionItemHighlight { case alpha } -class ContactListActionItem: ListViewItem, ListViewItemWithHeader { +public class ContactListActionItem: ListViewItem, ListViewItemWithHeader { let presentationData: ItemListPresentationData let title: String let icon: ContactListActionItemIcon let highlight: ContactListActionItemHighlight let clearHighlightAutomatically: Bool let action: () -> Void - let header: ListViewItemHeader? + public let header: ListViewItemHeader? - init(presentationData: ItemListPresentationData, title: String, icon: ContactListActionItemIcon, highlight: ContactListActionItemHighlight = .cell, clearHighlightAutomatically: Bool = true, header: ListViewItemHeader?, action: @escaping () -> Void) { + public init(presentationData: ItemListPresentationData, title: String, icon: ContactListActionItemIcon, highlight: ContactListActionItemHighlight = .cell, clearHighlightAutomatically: Bool = true, header: ListViewItemHeader?, action: @escaping () -> Void) { self.presentationData = presentationData self.title = title self.icon = icon @@ -33,7 +33,7 @@ class ContactListActionItem: ListViewItem, ListViewItemWithHeader { self.action = action } - func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { + public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { async { let node = ContactListActionItemNode() let (_, last, firstWithHeader) = ContactListActionItem.mergeType(item: self, previousItem: previousItem, nextItem: nextItem) @@ -50,7 +50,7 @@ class ContactListActionItem: ListViewItem, ListViewItemWithHeader { } } - func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { + public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { Queue.mainQueue().async { if let nodeValue = node() as? ContactListActionItemNode { let makeLayout = nodeValue.asyncLayout() @@ -68,9 +68,9 @@ class ContactListActionItem: ListViewItem, ListViewItemWithHeader { } } - var selectable: Bool = true + public var selectable: Bool = true - func selected(listView: ListView){ + public func selected(listView: ListView){ self.action() if self.clearHighlightAutomatically { listView.clearHighlightAnimated(true) diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/LegacyComponents.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/LegacyComponents.h index 18f24fd501..2d6e44c048 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/LegacyComponents.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/LegacyComponents.h @@ -80,7 +80,6 @@ #import #import #import -#import #import #import #import diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/PGCamera.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/PGCamera.h index 23ac166b8d..b6465ca803 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/PGCamera.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/PGCamera.h @@ -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 diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/PGCameraCaptureSession.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/PGCameraCaptureSession.h index 92c90b3592..9269470d96 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/PGCameraCaptureSession.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/PGCameraCaptureSession.h @@ -3,6 +3,7 @@ #import @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; diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/PGCameraShotMetadata.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/PGCameraShotMetadata.h index 59a07a7051..7243b67cf3 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/PGCameraShotMetadata.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/PGCameraShotMetadata.h @@ -1,9 +1,12 @@ #import #import +@class PGRectangle; + @interface PGCameraShotMetadata : NSObject @property (nonatomic, assign) CGFloat deviceAngle; +@property (nonatomic, strong) PGRectangle *rectangle; + (CGFloat)relativeDeviceAngleFromAngle:(CGFloat)angle orientation:(UIInterfaceOrientation)orientation; diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/PGPhotoEditorValues.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/PGPhotoEditorValues.h index 3e01cb40f6..12a0296d78 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/PGPhotoEditorValues.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/PGPhotoEditorValues.h @@ -1,9 +1,16 @@ #import @class TGPaintingData; +@class PGRectangle; @interface PGPhotoEditorValues : NSObject +@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 diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGCameraCapturedPhoto.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGCameraCapturedPhoto.h index 251ff85651..0eb043274a 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGCameraCapturedPhoto.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGCameraCapturedPhoto.h @@ -3,15 +3,19 @@ #import @class PGCameraShotMetadata; +@class PGRectangle; @interface TGCameraCapturedPhoto : NSObject @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 diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGCameraInterfaceAssets.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGCameraInterfaceAssets.h index ab8c131569..3ac9cc00a0 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGCameraInterfaceAssets.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGCameraInterfaceAssets.h @@ -11,6 +11,7 @@ + (UIColor *)transparentPanelBackgroundColor; + (UIColor *)transparentOverlayBackgroundColor; -+ (UIFont *)normalFontOfSize:(CGFloat)size; ++ (UIFont *)regularFontOfSize:(CGFloat)size; ++ (UIFont *)boldFontOfSize:(CGFloat)size; @end diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGCameraMainView.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGCameraMainView.h index ab2a225b1e..c116134d7d 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGCameraMainView.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGCameraMainView.h @@ -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; diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGCameraSegmentsView.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGCameraSegmentsView.h deleted file mode 100644 index b2f0c2ba73..0000000000 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGCameraSegmentsView.h +++ /dev/null @@ -1,20 +0,0 @@ -#import - -@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 diff --git a/submodules/LegacyComponents/Sources/PGCamera.m b/submodules/LegacyComponents/Sources/PGCamera.m index 6431e15773..d8088a1124 100644 --- a/submodules/LegacyComponents/Sources/PGCamera.m +++ b/submodules/LegacyComponents/Sources/PGCamera.m @@ -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 diff --git a/submodules/LegacyComponents/Sources/PGCameraCaptureSession.m b/submodules/LegacyComponents/Sources/PGCameraCaptureSession.m index e651da1ae3..ade29d5063 100644 --- a/submodules/LegacyComponents/Sources/PGCameraCaptureSession.m +++ b/submodules/LegacyComponents/Sources/PGCameraCaptureSession.m @@ -1,5 +1,6 @@ #import "PGCameraCaptureSession.h" #import "PGCameraMovieWriter.h" +#import "PGRectangleDetector.h" #import #import @@ -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; diff --git a/submodules/LegacyComponents/Sources/PGPhotoEditorValues.m b/submodules/LegacyComponents/Sources/PGPhotoEditorValues.m index ffb2aa828c..3257dd4b08 100644 --- a/submodules/LegacyComponents/Sources/PGPhotoEditorValues.m +++ b/submodules/LegacyComponents/Sources/PGPhotoEditorValues.m @@ -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; } diff --git a/submodules/LegacyComponents/Sources/PGRectangleDetector.h b/submodules/LegacyComponents/Sources/PGRectangleDetector.h new file mode 100644 index 0000000000..1c68d5938f --- /dev/null +++ b/submodules/LegacyComponents/Sources/PGRectangleDetector.h @@ -0,0 +1,24 @@ +#import + +@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 + diff --git a/submodules/LegacyComponents/Sources/PGRectangleDetector.m b/submodules/LegacyComponents/Sources/PGRectangleDetector.m new file mode 100644 index 0000000000..aa41ae4b90 --- /dev/null +++ b/submodules/LegacyComponents/Sources/PGRectangleDetector.m @@ -0,0 +1,362 @@ +#import "PGRectangleDetector.h" +#import "LegacyComponentsInternal.h" + +#import +#import + +#import + +@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 diff --git a/submodules/LegacyComponents/Sources/TGCameraCapturedPhoto.m b/submodules/LegacyComponents/Sources/TGCameraCapturedPhoto.m index 88cb73db87..445a794977 100644 --- a/submodules/LegacyComponents/Sources/TGCameraCapturedPhoto.m +++ b/submodules/LegacyComponents/Sources/TGCameraCapturedPhoto.m @@ -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]]; diff --git a/submodules/LegacyComponents/Sources/TGCameraController.m b/submodules/LegacyComponents/Sources/TGCameraController.m index 8482896730..5730f99828 100644 --- a/submodules/LegacyComponents/Sources/TGCameraController.m +++ b/submodules/LegacyComponents/Sources/TGCameraController.m @@ -19,6 +19,7 @@ #import #import #import "TGCameraFocusCrosshairsControl.h" +#import "TGCameraRectangleView.h" #import #import @@ -52,6 +53,8 @@ #import "TGCameraCapturedVideo.h" #import "PGPhotoEditor.h" +#import "PGRectangleDetector.h" +#import "TGWarpedView.h" #import "TGAnimationUtils.h" @@ -108,9 +111,7 @@ static CGPoint TGCameraControllerClampPointToScreenSize(__unused id self, __unus TGCameraMainView *_interfaceView; UIView *_overlayView; TGCameraFocusCrosshairsControl *_focusControl; - - TGModernGalleryVideoView *_segmentPreviewView; - bool _previewingSegment; + TGCameraRectangleView *_rectangleView; UISwipeGestureRecognizer *_photoSwipeGestureRecognizer; UISwipeGestureRecognizer *_videoSwipeGestureRecognizer; @@ -277,6 +278,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:)]; @@ -356,7 +362,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) @@ -474,6 +480,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; @@ -510,9 +535,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]; } @@ -566,6 +593,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]; + } + }); + }; + } } }); }; @@ -870,9 +912,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) { @@ -952,7 +992,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; @@ -969,7 +1009,7 @@ static CGPoint TGCameraControllerClampPointToScreenSize(__unused id self, __unus _buttonHandler.enabled = false; [_buttonHandler ignoreEventsFor:1.5f andDisable:true]; } - + [_camera takePhotoWithCompletion:^(UIImage *result, PGCameraShotMetadata *metadata) { __strong TGCameraController *strongSelf = weakSelf; @@ -1042,6 +1082,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) @@ -1164,27 +1338,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)editableItemValue completion:(void (^)(void))completion { __block id editableItem = editableItemValue; UIViewController *(^begin)(id) = ^(id 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; @@ -1971,6 +2152,7 @@ static CGPoint TGCameraControllerClampPointToScreenSize(__unused id self, __unus self.view.userInteractionEnabled = false; _focusControl.active = false; + _rectangleView.hidden = true; [UIView animateWithDuration:0.3f animations:^ { @@ -2238,21 +2420,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]; } } @@ -2441,11 +2629,22 @@ static CGPoint TGCameraControllerClampPointToScreenSize(__unused id self, __unus if (selectedItems.count == 0 && currentItem != nil) [selectedItems addObject:currentItem]; - if (storeAssets) - { + bool isScan = false; + for (id 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 item in selectedItems) { + if ([item isKindOfClass:[TGCameraCapturedPhoto class]] && ((TGCameraCapturedPhoto *)item).rectangle != nil) { + isScan = true; + } + if ([editingContext timerForItem:item] == nil) { SSignal *saveMedia = [SSignal defer:^SSignal * @@ -2565,9 +2764,15 @@ static CGPoint TGCameraControllerClampPointToScreenSize(__unused id self, __unus dict[@"timer"] = timer; 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; @@ -2593,12 +2798,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"; @@ -2645,11 +2851,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++; @@ -2732,6 +2946,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; } diff --git a/submodules/LegacyComponents/Sources/TGCameraFlashControl.m b/submodules/LegacyComponents/Sources/TGCameraFlashControl.m index 4962d217f6..54687872ad 100644 --- a/submodules/LegacyComponents/Sources/TGCameraFlashControl.m +++ b/submodules/LegacyComponents/Sources/TGCameraFlashControl.m @@ -63,7 +63,7 @@ const CGFloat TGCameraFlashControlHeight = 44.0f; _autoButton.exclusiveTouch = true; _autoButton.hitTestEdgeInsets = UIEdgeInsetsMake(-10, -15, -10, -15); _autoButton.tag = PGCameraFlashModeAuto; - _autoButton.titleLabel.font = [TGCameraInterfaceAssets normalFontOfSize:13]; + _autoButton.titleLabel.font = [TGCameraInterfaceAssets regularFontOfSize: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]; @@ -78,7 +78,7 @@ const CGFloat TGCameraFlashControlHeight = 44.0f; _onButton.exclusiveTouch = true; _onButton.hitTestEdgeInsets = UIEdgeInsetsMake(-10, -15, -10, -15); _onButton.tag = PGCameraFlashModeOn; - _onButton.titleLabel.font = [TGCameraInterfaceAssets normalFontOfSize:13]; + _onButton.titleLabel.font = [TGCameraInterfaceAssets regularFontOfSize: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]; @@ -93,7 +93,7 @@ const CGFloat TGCameraFlashControlHeight = 44.0f; _offButton.exclusiveTouch = true; _offButton.hitTestEdgeInsets = UIEdgeInsetsMake(-10, -15, -10, -15); _offButton.tag = PGCameraFlashModeOff; - _offButton.titleLabel.font = [TGCameraInterfaceAssets normalFontOfSize:13]; + _offButton.titleLabel.font = [TGCameraInterfaceAssets regularFontOfSize: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]; @@ -605,9 +605,7 @@ const CGFloat TGCameraFlashControlHeight = 44.0f; { CGSize size = title.size; CGFloat width = CGCeil(size.width); - if (iosMajorVersion() < 7) - width += 2; - return CGSizeMake(width, 20); + return CGSizeMake(width + 2.0, 20); } @end diff --git a/submodules/LegacyComponents/Sources/TGCameraFlipButton.m b/submodules/LegacyComponents/Sources/TGCameraFlipButton.m index 0f204e71ad..141fc479b9 100644 --- a/submodules/LegacyComponents/Sources/TGCameraFlipButton.m +++ b/submodules/LegacyComponents/Sources/TGCameraFlipButton.m @@ -30,17 +30,16 @@ 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 { diff --git a/submodules/LegacyComponents/Sources/TGCameraFocusCrosshairsControl.m b/submodules/LegacyComponents/Sources/TGCameraFocusCrosshairsControl.m index 6b8407fcc8..ef6413d067 100644 --- a/submodules/LegacyComponents/Sources/TGCameraFocusCrosshairsControl.m +++ b/submodules/LegacyComponents/Sources/TGCameraFocusCrosshairsControl.m @@ -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: diff --git a/submodules/LegacyComponents/Sources/TGCameraInterfaceAssets.m b/submodules/LegacyComponents/Sources/TGCameraInterfaceAssets.m index 68b4fc84d3..01382fa4cd 100644 --- a/submodules/LegacyComponents/Sources/TGCameraInterfaceAssets.m +++ b/submodules/LegacyComponents/Sources/TGCameraInterfaceAssets.m @@ -1,4 +1,5 @@ #import "TGCameraInterfaceAssets.h" +#import #import "LegacyComponentsInternal.h" @@ -11,12 +12,12 @@ + (UIColor *)accentColor { - return UIColorRGB(0xffcc00); + return UIColorRGB(0xf8d74a); } + (UIColor *)redColor { - return UIColorRGB(0xf53333); + return UIColorRGB(0xea4e3d); } + (UIColor *)panelBackgroundColor @@ -34,9 +35,54 @@ return [UIColor colorWithWhite:0.0f alpha:0.7]; } -+ (UIFont *)normalFontOfSize:(CGFloat)size ++ (UIFont *)regularFontOfSize:(CGFloat)size { - return [UIFont fontWithName:@"DINAlternate-Bold" size:size]; + if (@available(iOSApplicationExtension 13.0, iOS 13.0, *)) { + UIFontDescriptor *descriptor = [UIFont systemFontOfSize:size].fontDescriptor; + descriptor = [descriptor fontDescriptorWithSymbolicTraits:UIFontDescriptorTraitCondensed]; + + NSMutableArray *features = [[NSMutableArray alloc] init]; + [features addObject:@{ + UIFontFeatureTypeIdentifierKey : @(kStylisticAlternativesType), + UIFontFeatureSelectorIdentifierKey : @(kStylisticAltThreeOnSelector) + }]; + [features addObject:@{ + UIFontFeatureTypeIdentifierKey : @(kNumberSpacingType), + UIFontFeatureSelectorIdentifierKey : @(kMonospacedNumbersSelector) + }]; + + NSMutableDictionary *traits = [[NSMutableDictionary alloc] init]; + traits[UIFontWidthTrait] = @(UIFontWeightMedium); + + descriptor = [descriptor fontDescriptorByAddingAttributes:@{ UIFontDescriptorFeatureSettingsAttribute: features}]; + + return [UIFont fontWithDescriptor:descriptor size:size]; + } else { + return [UIFont fontWithName:@"DINAlternate-Bold" size:size]; + } +} + ++ (UIFont *)boldFontOfSize:(CGFloat)size +{ + if (@available(iOSApplicationExtension 13.0, iOS 13.0, *)) { + UIFontDescriptor *descriptor = [UIFont systemFontOfSize:size weight:UIFontWeightSemibold].fontDescriptor; + descriptor = [descriptor fontDescriptorWithSymbolicTraits:UIFontDescriptorTraitCondensed]; + + NSMutableArray *features = [[NSMutableArray alloc] init]; + [features addObject:@{ + UIFontFeatureTypeIdentifierKey : @(kStylisticAlternativesType), + UIFontFeatureSelectorIdentifierKey : @(kStylisticAltThreeOnSelector) + }]; + [features addObject:@{ + UIFontFeatureTypeIdentifierKey : @(kNumberSpacingType), + UIFontFeatureSelectorIdentifierKey : @(kMonospacedNumbersSelector) + }]; + + descriptor = [descriptor fontDescriptorByAddingAttributes:@{ UIFontDescriptorFeatureSettingsAttribute: features}]; + return [UIFont fontWithDescriptor:descriptor size:size]; + } else { + return [UIFont fontWithName:@"DINAlternate-Bold" size:size]; + } } @end diff --git a/submodules/LegacyComponents/Sources/TGCameraMainPhoneView.m b/submodules/LegacyComponents/Sources/TGCameraMainPhoneView.m index 5f0233b629..9679b6bfed 100644 --- a/submodules/LegacyComponents/Sources/TGCameraMainPhoneView.m +++ b/submodules/LegacyComponents/Sources/TGCameraMainPhoneView.m @@ -19,7 +19,7 @@ #import "TGCameraFlipButton.h" #import "TGCameraTimeCodeView.h" #import "TGCameraZoomView.h" -#import "TGCameraSegmentsView.h" +#import "TGCameraToastView.h" #import "TGMenuView.h" @@ -116,8 +116,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; } @@ -253,6 +253,9 @@ _flashActiveView = [[TGCameraFlashActiveView alloc] initWithFrame:CGRectMake((frame.size.width - 40) / 2, frame.size.height - _bottomPanelHeight - 37, 40, 21)]; [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) { @@ -364,9 +367,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; + } } } @@ -850,6 +856,8 @@ _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); diff --git a/submodules/LegacyComponents/Sources/TGCameraMainView.m b/submodules/LegacyComponents/Sources/TGCameraMainView.m index 4254f41c16..79bc985526 100644 --- a/submodules/LegacyComponents/Sources/TGCameraMainView.m +++ b/submodules/LegacyComponents/Sources/TGCameraMainView.m @@ -5,10 +5,11 @@ #import #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; diff --git a/submodules/LegacyComponents/Sources/TGCameraModeControl.m b/submodules/LegacyComponents/Sources/TGCameraModeControl.m index fd6b3cd2ac..9feff38c65 100644 --- a/submodules/LegacyComponents/Sources/TGCameraModeControl.m +++ b/submodules/LegacyComponents/Sources/TGCameraModeControl.m @@ -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 = 1.4f; _maskView = [[UIView alloc] initWithFrame:self.bounds]; _maskView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; @@ -52,7 +49,8 @@ const CGFloat TGCameraModeControlVerticalInteritemSpace = 29.0f; _buttons = @ [ [self _createButtonForMode:PGCameraModeVideo title:TGLocalized(@"Camera.VideoMode")], - [self _createButtonForMode:PGCameraModePhoto title:TGLocalized(@"Camera.PhotoMode")] + [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,11 @@ 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:13] }] forState:UIControlStateNormal]; + [button setAttributedTitle:[[NSAttributedString alloc] initWithString:title attributes:@{ NSForegroundColorAttributeName: [TGCameraInterfaceAssets accentColor], NSKernAttributeName: @(_kerning), NSFontAttributeName: [TGCameraInterfaceAssets boldFontOfSize:13] }] forState:UIControlStateSelected]; [button setAttributedTitle:[button attributedTitleForState:UIControlStateSelected] forState:UIControlStateHighlighted | UIControlStateSelected]; [button sizeToFit]; + 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; diff --git a/submodules/LegacyComponents/Sources/TGCameraRectangleView.h b/submodules/LegacyComponents/Sources/TGCameraRectangleView.h new file mode 100644 index 0000000000..3560be793e --- /dev/null +++ b/submodules/LegacyComponents/Sources/TGCameraRectangleView.h @@ -0,0 +1,14 @@ +#import + +@class TGCameraPreviewView; +@class PGRectangle; + +@interface TGCameraRectangleView : UIView + +@property (nonatomic, weak) TGCameraPreviewView *previewView; +@property (nonatomic, assign) bool enabled; + +- (void)drawRectangle:(PGRectangle *)rectangle; + +@end + diff --git a/submodules/LegacyComponents/Sources/TGCameraRectangleView.m b/submodules/LegacyComponents/Sources/TGCameraRectangleView.m new file mode 100644 index 0000000000..bfe77c9928 --- /dev/null +++ b/submodules/LegacyComponents/Sources/TGCameraRectangleView.m @@ -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 diff --git a/submodules/LegacyComponents/Sources/TGCameraSegmentsView.m b/submodules/LegacyComponents/Sources/TGCameraSegmentsView.m deleted file mode 100644 index 6f5ed063cf..0000000000 --- a/submodules/LegacyComponents/Sources/TGCameraSegmentsView.m +++ /dev/null @@ -1,260 +0,0 @@ -#import "TGCameraSegmentsView.h" - -#import "LegacyComponentsInternal.h" -#import "TGImageUtils.h" - -#import "TGCameraInterfaceAssets.h" - -#import - -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 diff --git a/submodules/LegacyComponents/Sources/TGCameraShutterButton.m b/submodules/LegacyComponents/Sources/TGCameraShutterButton.m index 158bbc6718..4bdf177f9c 100644 --- a/submodules/LegacyComponents/Sources/TGCameraShutterButton.m +++ b/submodules/LegacyComponents/Sources/TGCameraShutterButton.m @@ -1,4 +1,5 @@ #import "TGCameraShutterButton.h" +#import "TGImageUtils.h" #import @@ -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 diff --git a/submodules/LegacyComponents/Sources/TGCameraTimeCodeView.m b/submodules/LegacyComponents/Sources/TGCameraTimeCodeView.m index 8a6f5ad331..265b76424f 100644 --- a/submodules/LegacyComponents/Sources/TGCameraTimeCodeView.m +++ b/submodules/LegacyComponents/Sources/TGCameraTimeCodeView.m @@ -43,7 +43,7 @@ _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.font = [TGCameraInterfaceAssets regularFontOfSize:21]; _timeLabel.text = @"00:00:00"; _timeLabel.textAlignment = NSTextAlignmentCenter; _timeLabel.textColor = [TGCameraInterfaceAssets normalColor]; diff --git a/submodules/LegacyComponents/Sources/TGCameraToastView.h b/submodules/LegacyComponents/Sources/TGCameraToastView.h new file mode 100644 index 0000000000..d5870d620e --- /dev/null +++ b/submodules/LegacyComponents/Sources/TGCameraToastView.h @@ -0,0 +1,7 @@ +#import + +@interface TGCameraToastView : UIView + +- (void)setText:(NSString *)text animated:(bool)animated; + +@end diff --git a/submodules/LegacyComponents/Sources/TGCameraToastView.m b/submodules/LegacyComponents/Sources/TGCameraToastView.m new file mode 100644 index 0000000000..6bb98b597e --- /dev/null +++ b/submodules/LegacyComponents/Sources/TGCameraToastView.m @@ -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 diff --git a/submodules/LegacyComponents/Sources/TGFont.mm b/submodules/LegacyComponents/Sources/TGFont.mm index 9c66d82e67..5d1ccc575e 100644 --- a/submodules/LegacyComponents/Sources/TGFont.mm +++ b/submodules/LegacyComponents/Sources/TGFont.mm @@ -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 { diff --git a/submodules/LegacyComponents/Sources/TGMediaEditingContext.m b/submodules/LegacyComponents/Sources/TGMediaEditingContext.m index 97970434ef..a2c3bb84de 100644 --- a/submodules/LegacyComponents/Sources/TGMediaEditingContext.m +++ b/submodules/LegacyComponents/Sources/TGMediaEditingContext.m @@ -184,9 +184,7 @@ } - (void)cleanup -{ - [_diskCache cleanup]; - +{ [[NSFileManager defaultManager] removeItemAtPath:_fullSizeResultsUrl.path error:nil]; [[NSFileManager defaultManager] removeItemAtPath:_paintingImagesUrl.path error:nil]; [[NSFileManager defaultManager] removeItemAtPath:_paintingDatasUrl.path error:nil]; diff --git a/submodules/LegacyComponents/Sources/TGPhotoAvatarCropController.h b/submodules/LegacyComponents/Sources/TGPhotoAvatarCropController.h deleted file mode 100644 index 50036f64bf..0000000000 --- a/submodules/LegacyComponents/Sources/TGPhotoAvatarCropController.h +++ /dev/null @@ -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)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 diff --git a/submodules/LegacyComponents/Sources/TGPhotoAvatarCropController.m b/submodules/LegacyComponents/Sources/TGPhotoAvatarCropController.m deleted file mode 100644 index c1e91bcd7d..0000000000 --- a/submodules/LegacyComponents/Sources/TGPhotoAvatarCropController.m +++ /dev/null @@ -1,707 +0,0 @@ -#import "TGPhotoAvatarCropController.h" - -#import "LegacyComponentsInternal.h" - -#import "TGPhotoEditorInterfaceAssets.h" -#import - -#import -#import - -#import "PGPhotoEditor.h" -#import "TGPhotoEditorPreviewView.h" - -#import "TGPhotoAvatarCropView.h" -#import - -#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)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 diff --git a/submodules/LegacyComponents/Sources/TGPhotoPaintController.m b/submodules/LegacyComponents/Sources/TGPhotoPaintController.m index abf25402a4..9e5cf723f3 100644 --- a/submodules/LegacyComponents/Sources/TGPhotoPaintController.m +++ b/submodules/LegacyComponents/Sources/TGPhotoPaintController.m @@ -1223,8 +1223,10 @@ const CGFloat TGPhotoPaintStickerKeyboardSize = 260.0f; NSTimeInterval currentTime = editorController.currentTime; __strong TGPhotoStickerEntityView *strongStickerView = weakStickerView; if (strongStickerView != nil) { - [strongStickerView seekTo:currentTime]; - [strongStickerView play]; + if(!isnan(currentTime)) { + [strongStickerView seekTo:currentTime]; + [strongStickerView play]; + } } } }; diff --git a/submodules/LegacyComponents/Sources/TGPhotoRectangleCropController.h b/submodules/LegacyComponents/Sources/TGPhotoRectangleCropController.h new file mode 100644 index 0000000000..3df2af9dd5 --- /dev/null +++ b/submodules/LegacyComponents/Sources/TGPhotoRectangleCropController.h @@ -0,0 +1,6 @@ +#import "TGPhotoEditorTabController.h" + +@interface TGPhotoRectangleCropController : TGPhotoEditorTabController + +@end + diff --git a/submodules/LegacyComponents/Sources/TGPhotoRectangleCropController.m b/submodules/LegacyComponents/Sources/TGPhotoRectangleCropController.m new file mode 100644 index 0000000000..3794d8603d --- /dev/null +++ b/submodules/LegacyComponents/Sources/TGPhotoRectangleCropController.m @@ -0,0 +1,5 @@ +#import "TGPhotoRectangleCropController.h" + +@implementation TGPhotoRectangleCropController + +@end diff --git a/submodules/LegacyComponents/Sources/TGWarpedView.h b/submodules/LegacyComponents/Sources/TGWarpedView.h new file mode 100644 index 0000000000..0d5a7b1bdb --- /dev/null +++ b/submodules/LegacyComponents/Sources/TGWarpedView.h @@ -0,0 +1,7 @@ +#import + +@interface TGWarpedView : UIImageView + +- (void)transformToFitQuadTopLeft:(CGPoint)tl topRight:(CGPoint)tr bottomLeft:(CGPoint)bl bottomRight:(CGPoint)br; + +@end diff --git a/submodules/LegacyComponents/Sources/TGWarpedView.m b/submodules/LegacyComponents/Sources/TGWarpedView.m new file mode 100644 index 0000000000..975df28fd2 --- /dev/null +++ b/submodules/LegacyComponents/Sources/TGWarpedView.m @@ -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 diff --git a/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift b/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift index f6eb986ea0..f9767f303c 100644 --- a/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift +++ b/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift @@ -372,10 +372,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: arc4random64()) + 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: [])) + } + } + 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(.message(text: caption ?? "", attributes: [], mediaReference: .standalone(media: media), replyToMessageId: nil, localGroupingKey: item.groupedId)) case let .asset(asset): var randomId: Int64 = 0 diff --git a/submodules/TelegramUI/Sources/ChannelParticipantsScreen.swift b/submodules/TelegramUI/Sources/ChannelParticipantsScreen.swift new file mode 100644 index 0000000000..accd1e80fd --- /dev/null +++ b/submodules/TelegramUI/Sources/ChannelParticipantsScreen.swift @@ -0,0 +1,610 @@ +import Foundation +import UIKit +import SwiftSignalKit +import Display +import TelegramCore +import SyncCore +import Postbox +import TelegramPresentationData +import TelegramStringFormatting +import AccountContext +import MergeLists +import SearchBarNode +import SearchUI +import ContactListUI +import ContactsPeerItem +import ItemListUI +import ChatListSearchItemHeader +import PresentationDataUtils + +class ChannelParticipantsInteraction { + let addMember: () -> Void + let openPeer: (PeerId) -> Void + + init(addMember: @escaping () -> Void, openPeer: @escaping (PeerId) -> Void) { + self.addMember = addMember + self.openPeer = openPeer + } +} + +private struct ChannelParticipantsTransaction { + let deletions: [ListViewDeleteItem] + let insertions: [ListViewInsertItem] + let updates: [ListViewUpdateItem] + let isLoading: Bool + let isEmpty: Bool + let crossFade: Bool +} + +private enum ChannelParticipantsEntryId: Hashable { + case action(Int) + case peer(PeerId) +} + +private enum ChannelParticipantsEntry: Comparable, Identifiable { + case action(Int, PresentationTheme, ContactListAdditionalOption) + case peer(Int, PresentationTheme, PresentationStrings, RenderedChannelParticipant, ListViewItemHeader?, Bool) + + var stableId: ChannelParticipantsEntryId { + switch self { + case let .action(index, _, _): + return .action(index) + case let .peer(_, _ , _, participant, _, _): + return .peer(participant.peer.id) + } + } + + static func ==(lhs: ChannelParticipantsEntry, rhs: ChannelParticipantsEntry) -> Bool { + switch lhs { + case let .action(lhsIndex, lhsTheme, lhsOption): + if case let .action(rhsIndex, rhsTheme, rhsOption) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsOption == rhsOption { + return true + } else { + return false + } + case let .peer(lhsIndex, lhsTheme, lhsStrings, lhsParticipant, lhsHeader, lhsExpanded): + if case let .peer(rhsIndex, rhsTheme, rhsStrings, rhsParticipant, rhsHeader, rhsExpanded) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsParticipant == rhsParticipant, lhsHeader?.id == rhsHeader?.id, lhsExpanded == rhsExpanded { + return true + } else { + return false + } + } + } + + static func <(lhs: ChannelParticipantsEntry, rhs: ChannelParticipantsEntry) -> Bool { + switch lhs { + case let .action(lhsIndex, _, _): + switch rhs { + case let .action(rhsIndex, _, _): + return lhsIndex < rhsIndex + case .peer: + return true + } + case let .peer(lhsIndex, _, _, _, _, _): + switch rhs { + case .action: + return false + case let .peer(rhsIndex, _, _, _, _, _): + return lhsIndex < rhsIndex + } + } + } + + func item(context: AccountContext, presentationData: PresentationData, interaction: ChannelParticipantsInteraction?) -> ListViewItem { + switch self { + case let .action(_, _, option): + return ContactListActionItem(presentationData: ItemListPresentationData(presentationData), title: option.title, icon: option.icon, clearHighlightAutomatically: false, header: nil, action: option.action) + case let .peer(_, theme, strings, participant, header, expanded): + var status: String + if case let .member(_, invitedAt, _, _, _) = participant.participant { + status = "joined \(stringForFullDate(timestamp: invitedAt, strings: strings, dateTimeFormat: presentationData.dateTimeFormat))" + } else { + status = "owner" + } + return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: presentationData.nameSortOrder, displayOrder: presentationData.nameDisplayOrder, context: context, peerMode: .peer, peer: .peer(peer: participant.peer, chatPeer: nil), status: .custom(string: status, multiline: false), enabled: true, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: header, action: { _ in + interaction?.openPeer(participant.peer.id) + }, itemHighlighting: ContactItemHighlighting(), contextAction: nil) + } + } +} + +private func preparedTransaction(from fromEntries: [ChannelParticipantsEntry], to toEntries: [ChannelParticipantsEntry], isLoading: Bool, isEmpty: Bool, crossFade: Bool, context: AccountContext, presentationData: PresentationData, interaction: ChannelParticipantsInteraction?) -> ChannelParticipantsTransaction { + let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) + + let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, interaction: interaction), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, interaction: interaction), directionHint: nil) } + + return ChannelParticipantsTransaction(deletions: deletions, insertions: insertions, updates: updates, isLoading: isLoading, isEmpty: isEmpty, crossFade: crossFade) +} + +private struct ChannelParticipantsState { + var editing: Bool + + init() { + self.editing = false + } +} + + +private class ChannelParticipantsScreenNode: ViewControllerTracingNode { + private let context: AccountContext + private let peerId: PeerId + private var presentationData: PresentationData + private var presentationDataPromise: Promise + private let membersContext: PeerInfoMembersContext + private var interaction: ChannelParticipantsInteraction? + + private let listNode: ListView + + var navigationBar: NavigationBar? + private var searchDisplayController: SearchDisplayController? + + var requestActivateSearch: (() -> Void)? + var requestDeactivateSearch: (() -> Void)? + var requestAddMember: (() -> Void)? + var requestOpenPeer: ((PeerId) -> Void)? + + var contentOffsetChanged: ((ListViewVisibleContentOffset) -> Void)? + var contentScrollingEnded: ((ListView) -> Bool)? + + private var disposable: Disposable? + private var state: ChannelParticipantsState + private let statePromise: Promise + + private var currentEntries: [ChannelParticipantsEntry] = [] + private var enqueuedTransactions: [ChannelParticipantsTransaction] = [] + + private var containerLayout: (ContainerViewLayout, CGFloat, CGFloat)? + + init(context: AccountContext, peerId: PeerId) { + self.context = context + self.peerId = peerId + self.presentationData = context.sharedContext.currentPresentationData.with { $0 } + self.presentationDataPromise = Promise(presentationData) + self.membersContext = PeerInfoMembersContext(context: context, peerId: peerId) + + self.state = ChannelParticipantsState() + self.statePromise = Promise(self.state) + + self.listNode = ListView() + self.listNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor + self.listNode.verticalScrollIndicatorColor = UIColor(white: 0.0, alpha: 0.3) + self.listNode.verticalScrollIndicatorFollowsOverscroll = true + + super.init() + + self.interaction = ChannelParticipantsInteraction(addMember: { [weak self] in + self?.requestAddMember?() + }, openPeer: { [weak self] peerId in + self?.requestOpenPeer?(peerId) + }) + + self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor + + self.addSubnode(self.listNode) + + self.disposable = (combineLatest(self.presentationDataPromise.get(), self.statePromise.get(), self.membersContext.state, context.account.postbox.combinedView(keys: [.basicPeer(peerId)])) + |> deliverOnMainQueue).start(next: { [weak self] presentationData, state, members, combinedView in + guard let strongSelf = self, let basicPeerView = combinedView.views[.basicPeer(peerId)] as? BasicPeerView, let enclosingPeer = basicPeerView.peer else { + return + } + + strongSelf.updateState(enclosingPeer: enclosingPeer, members: members, presentationData: presentationData) + }) + } + + private func updateState(enclosingPeer: Peer, members: PeerInfoMembersState, presentationData: PresentationData) { + var entries: [ChannelParticipantsEntry] = [] + + entries.append(.action(0, presentationData.theme, ContactListAdditionalOption(title: "Add Subscribers", icon: .generic(UIImage(bundleImageName: "Contact List/AddMemberIcon")!), action: { [weak self] in +// self?.interaction? + }))) + + let contacts = ChatListSearchItemHeader(type: .contacts, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil) + let otherSubscribers = ChatListSearchItemHeader(type: .otherSubscribers, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil) + + var known: [RenderedChannelParticipant] = [] + var other: [RenderedChannelParticipant] = [] + +// Ilya Laktyushin 572439 +// Nikolay Kudashov 552564 +// Alexander Stepanov 215491 +// Michael Filimonov 438078 +// Peter Iakovlev 903523 +// Вася Бабич 763171 +// Denis Prokopov 949693 +// **Dmitrybot** 230212 +// Dmitry Moskovsky 659864346 +// Nick Kudashov jjrjrtest 76745538 +// В 102439374 +// Michael Filimonov 264037907 +// example 3735744 +// California Kai ☀️ 12549969 +// Pushtest 640083077 + + let unknown: Set = Set([102439374, 76745538, 264037907, 3735744, 12549969, 640083077]) + + for member in members.members { + if case let .channelMember(participant) = member { + if case .member = participant.participant { + if unknown.contains(participant.peer.id.toInt64()) { + other.append(participant) + } else { + known.append(participant) + } +// entries.append(.peer(entries.count, presentationData.theme, presentationData.strings, participant, nil, false)) +// print(participant.peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + " " + "\(participant.peer.id.toInt64())") + } + } + } + + for participant in known.sorted(by: { lhs, rhs in + if case let .member(_, lhsInvitedAt, _, _, _) = lhs.participant, case let .member(_, rhsInvitedAt, _, _, _) = rhs.participant { + return lhsInvitedAt > rhsInvitedAt + } else { + return false + } + }) { + entries.append(.peer(entries.count, presentationData.theme, presentationData.strings, participant, contacts, false)) + } + + for participant in other.sorted(by: { lhs, rhs in + if case let .member(_, lhsInvitedAt, _, _, _) = lhs.participant, case let .member(_, rhsInvitedAt, _, _, _) = rhs.participant { + return lhsInvitedAt > rhsInvitedAt + } else { + return false + } + + }) { + entries.append(.peer(entries.count, presentationData.theme, presentationData.strings, participant, otherSubscribers, false)) + } + + + let transaction = preparedTransaction(from: self.currentEntries, to: entries, isLoading: false, isEmpty: false, crossFade: false, context: self.context, presentationData: presentationData, interaction: self.interaction) +// let transaction = preparedTransition(from: self.currentEntries, to: entries, context: self.context, presentationData: presentationData, enclosingPeer: enclosingPeer, action: { [weak self] member, action in +//// self?.action(member, action) +// }) +// self.enclosingPeer = enclosingPeer + self.currentEntries = entries + self.enqueueTransaction(transaction) + } + + func activateSearch(placeholderNode: SearchBarPlaceholderNode) { + guard let (containerLayout, navigationBarHeight, _) = self.containerLayout, let navigationBar = self.navigationBar else { + return + } + +// self.searchDisplayController = SearchDisplayController(presentationData: self.presentationData, contentNode: ChatListSearchContainerNode(context: self.context, filter: self.filter, groupId: .root, openPeer: { [weak self] peer, _ in +// if let requestOpenPeerFromSearch = self?.requestOpenPeerFromSearch { +// requestOpenPeerFromSearch(peer) +// } +// }, openDisabledPeer: { [weak self] peer in +// self?.requestOpenDisabledPeer?(peer) +// }, openRecentPeerOptions: { _ in +// }, openMessage: { [weak self] peer, messageId in +// if let requestOpenMessageFromSearch = self?.requestOpenMessageFromSearch { +// requestOpenMessageFromSearch(peer, messageId) +// } +// }, addContact: nil, peerContextAction: nil, present: { _ in +// }), cancel: { [weak self] in +// if let requestDeactivateSearch = self?.requestDeactivateSearch { +// requestDeactivateSearch() +// } +// }) +// +// self.searchDisplayController?.containerLayoutUpdated(containerLayout, navigationBarHeight: navigationBarHeight, transition: .immediate) +// self.searchDisplayController?.activate(insertSubnode: { [weak self, weak placeholderNode] subnode, isSearchBar in +// if let strongSelf = self, let strongPlaceholderNode = placeholderNode { +// if isSearchBar { +// strongPlaceholderNode.supernode?.insertSubnode(subnode, aboveSubnode: strongPlaceholderNode) +// } else { +// strongSelf.insertSubnode(subnode, belowSubnode: navigationBar) +// } +// } +// }, placeholder: placeholderNode) + } + + func deactivateSearch(placeholderNode: SearchBarPlaceholderNode) { + if let searchDisplayController = self.searchDisplayController { + searchDisplayController.deactivate(placeholder: placeholderNode) + self.searchDisplayController = nil + } + } + + func scrollToTop() { + + } + + private func enqueueTransaction(_ transaction: ChannelParticipantsTransaction) { + self.enqueuedTransactions.append(transaction) + + if let _ = self.containerLayout { + while !self.enqueuedTransactions.isEmpty { + self.dequeueTransaction() + } + } + } + + private func dequeueTransaction() { + guard let layout = self.containerLayout, let transition = self.enqueuedTransactions.first else { + return + } + self.enqueuedTransactions.remove(at: 0) + + var options = ListViewDeleteAndInsertOptions() + if transition.crossFade { + options.insert(.AnimateCrossfade) + } + + self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { [weak self] _ in + if let strongSelf = self { + // strongSelf.activityIndicator.isHidden = !transition.isLoading + // strongSelf.emptyResultsTextNode.isHidden = transition.isLoading || !transition.isEmpty + // + // strongSelf.emptyResultsTextNode.attributedText = NSAttributedString(string: strongSelf.presentationData.strings.Map_NoPlacesNearby, font: Font.regular(15.0), textColor: strongSelf.presentationData.theme.list.freeTextColor) + // + // strongSelf.layoutActivityIndicator(transition: .immediate) + } + }) + } + + func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, actualNavigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { + let isFirstLayout = self.containerLayout == nil + self.containerLayout = (layout, navigationBarHeight, actualNavigationBarHeight) + + var insets = layout.insets(options: [.input]) + insets.top += max(navigationBarHeight, layout.insets(options: [.statusBar]).top) + insets.left += layout.safeInsets.left + insets.right += layout.safeInsets.right + + let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) + let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, duration: duration, curve: curve) + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + + self.listNode.bounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height) + self.listNode.position = CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0) + + if isFirstLayout { + while !self.enqueuedTransactions.isEmpty { + self.dequeueTransaction() + } + } + } +} + +public class ChannelParticipantsScreen: ViewController { + private let context: AccountContext + private let peerId: PeerId + private var presentationData: PresentationData + private var presentationDataDisposable: Disposable? + + private var controllerNode: ChannelParticipantsScreenNode { + return self.displayNode as! ChannelParticipantsScreenNode + } + + let addMembersDisposable = MetaDisposable() + +// private let _ready = Promise() +// override public var ready: Promise { +// return self._ready +// } + + private var searchContentNode: NavigationBarSearchContentNode? + + public init(context: AccountContext, peerId: PeerId) { + self.context = context + self.peerId = peerId + self.presentationData = context.sharedContext.currentPresentationData.with { $0 } + + super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData)) + + self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style + + self.title = "Subscribers" + + self.scrollToTop = { [weak self] in + if let strongSelf = self { + if let searchContentNode = strongSelf.searchContentNode { + searchContentNode.updateExpansionProgress(1.0, animated: true) + } + strongSelf.controllerNode.scrollToTop() + } + } + + self.presentationDataDisposable = (self.context.sharedContext.presentationData + |> deliverOnMainQueue).start(next: { [weak self] presentationData in + if let strongSelf = self { + let previousTheme = strongSelf.presentationData.theme + let previousStrings = strongSelf.presentationData.strings + + strongSelf.presentationData = presentationData + + if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings { +// strongSelf.updateThemeAndStrings() + } + } + }) + + self.searchContentNode = NavigationBarSearchContentNode(theme: self.presentationData.theme, placeholder: self.presentationData.strings.Common_Search, activate: { [weak self] in + self?.activateSearch() + }) + self.navigationBar?.setContentNode(self.searchContentNode, animated: false) + } + + required init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.presentationDataDisposable?.dispose() + self.addMembersDisposable.dispose() + } + + override public func loadDisplayNode() { + self.displayNode = ChannelParticipantsScreenNode(context: self.context, peerId: self.peerId) + + self.controllerNode.navigationBar = self.navigationBar + + self.controllerNode.requestDeactivateSearch = { [weak self] in + self?.deactivateSearch() + } + + self.controllerNode.requestActivateSearch = { [weak self] in + self?.activateSearch() + } + + self.controllerNode.requestAddMember = { [weak self] in + guard let strongSelf = self else { + return + } +// let disabledIds = members?.compactMap({$0.peer.id}) ?? [] + + let context = strongSelf.context + let peerId = strongSelf.peerId + let presentationData = strongSelf.presentationData + + let contactsController = context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: context, mode: .peerSelection(searchChatList: false, searchGroups: false, searchChannels: false), options: [], filters: [.excludeSelf, .disable([])])) + contactsController.navigationPresentation = .modal + + strongSelf.addMembersDisposable.set((contactsController.result + |> deliverOnMainQueue + |> castError(AddChannelMemberError.self) + |> mapToSignal { [weak contactsController] result -> Signal in + contactsController?.displayProgress = true + + var contacts: [ContactListPeerId] = [] + if case let .result(peerIdsValue, _) = result { + contacts = peerIdsValue + } + + let signal = context.peerChannelMemberCategoriesContextsManager.addMembers(account: context.account, peerId: peerId, memberIds: contacts.compactMap({ contact -> PeerId? in + switch contact { + case let .peer(contactId): + return contactId + default: + return nil + } + })) + + return signal + |> ignoreValues + |> deliverOnMainQueue + |> afterCompleted { + contactsController?.dismiss() + } + }).start(error: { [weak self, weak contactsController] error in + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let text: String + switch error { + case .limitExceeded: + text = presentationData.strings.Channel_ErrorAddTooMuch + case .tooMuchJoined: + text = presentationData.strings.Invite_ChannelsTooMuch + case .generic: + text = presentationData.strings.Login_UnknownError + case .restricted: + text = presentationData.strings.Channel_ErrorAddBlocked + case .notMutualContact: + text = presentationData.strings.GroupInfo_AddUserLeftError + case let .bot(memberId): + let _ = (context.account.postbox.transaction { transaction in + return transaction.getPeer(peerId) + } + |> deliverOnMainQueue).start(next: { peer in + guard let peer = peer as? TelegramChannel else { + self?.present(textAlertController(context: context, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + contactsController?.dismiss() + + return + } + + if peer.hasPermission(.addAdmins) { + contactsController?.displayProgress = false + self?.present(textAlertController(context: context, title: nil, text: presentationData.strings.Channel_AddBotErrorHaveRights, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Channel_AddBotAsAdmin, action: { + contactsController?.dismiss() + +// pushControllerImpl?(channelAdminController(context: context, peerId: peerId, adminId: memberId, initialParticipant: nil, updated: { _ in +// }, upgradedToSupergroup: { _, f in f () }, transferedOwnership: { _ in })) + })]), in: .window(.root)) + } else { + self?.present(textAlertController(context: context, title: nil, text: presentationData.strings.Channel_AddBotErrorHaveRights, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + } + + contactsController?.dismiss() + }) + return + case .botDoesntSupportGroups: + text = presentationData.strings.Channel_BotDoesntSupportGroups + case .tooMuchBots: + text = presentationData.strings.Channel_TooMuchBots + } + self?.present(textAlertController(context: context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + contactsController?.dismiss() + })) + + strongSelf.push(contactsController) + } + + self.controllerNode.requestOpenPeer = { [weak self] peerId in +// if let strongSelf = self, let peerSelected = strongSelf.peerSelected { +// peerSelected(peerId) +// } + } + + var isProcessingContentOffsetChanged = false + self.controllerNode.contentOffsetChanged = { [weak self] offset in + if isProcessingContentOffsetChanged { + return + } + isProcessingContentOffsetChanged = true + if let strongSelf = self, let searchContentNode = strongSelf.searchContentNode { + searchContentNode.updateListVisibleContentOffset(offset) + isProcessingContentOffsetChanged = false + } + } + + self.controllerNode.contentScrollingEnded = { [weak self] listView in + if let strongSelf = self, let searchContentNode = strongSelf.searchContentNode { + return fixNavigationSearchableListNodeScrolling(listView, searchNode: searchContentNode) + } else { + return false + } + } + + self.displayNodeDidLoad() + +// self._ready.set(self.controllerNode.ready) + } + + private func updatePresentationData() { + self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationInsetHeight, actualNavigationBarHeight: self.navigationHeight, transition: transition) + } + + private func activateSearch() { + if self.displayNavigationBar { + if let scrollToTop = self.scrollToTop { + scrollToTop() + } + if let searchContentNode = self.searchContentNode { + self.controllerNode.activateSearch(placeholderNode: searchContentNode.placeholderNode) + } + self.setDisplayNavigationBar(false, transition: .animated(duration: 0.5, curve: .spring)) + } + } + + private func deactivateSearch() { + if !self.displayNavigationBar { + self.setDisplayNavigationBar(true, transition: .animated(duration: 0.5, curve: .spring)) + if let searchContentNode = self.searchContentNode { + self.controllerNode.deactivateSearch(placeholderNode: searchContentNode.placeholderNode) + } + } + } +}