Initial document scan implementation

This commit is contained in:
Ilya Laktyushin 2020-08-03 21:04:52 +03:00
parent bdd0ef6285
commit 1e6e92c3de
44 changed files with 1846 additions and 1130 deletions

View File

@ -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";

View File

@ -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

View File

@ -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<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (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)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -11,6 +11,7 @@
+ (UIColor *)transparentPanelBackgroundColor;
+ (UIColor *)transparentOverlayBackgroundColor;
+ (UIFont *)normalFontOfSize:(CGFloat)size;
+ (UIFont *)regularFontOfSize:(CGFloat)size;
+ (UIFont *)boldFontOfSize:(CGFloat)size;
@end

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -19,6 +19,7 @@
#import <LegacyComponents/TGCameraMainPhoneView.h>
#import <LegacyComponents/TGCameraMainTabletView.h>
#import "TGCameraFocusCrosshairsControl.h"
#import "TGCameraRectangleView.h"
#import <LegacyComponents/TGFullscreenContainerView.h>
#import <LegacyComponents/TGPhotoEditorController.h>
@ -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;
@ -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<TGMediaEditableItem, TGMediaSelectableItem>)editableItemValue completion:(void (^)(void))completion
{
__block id<TGMediaEditableItem, TGMediaSelectableItem> editableItem = editableItemValue;
UIViewController *(^begin)(id<LegacyComponentsContext>) = ^(id<LegacyComponentsContext> windowContext) {
[self _createContextsIfNeeded];
TGMediaEditingContext *editingContext = _editingContext;
if (editingContext == nil)
{
editingContext = [[TGMediaEditingContext alloc] init];
if (self.forcedCaption != nil)
[editingContext setForcedCaption:self.forcedCaption entities:self.forcedEntities];
_editingContext = editingContext;
_interfaceView.editingContext = editingContext;
}
TGMediaSelectionContext *selectionContext = _selectionContext;
if (selectionContext == nil)
{
selectionContext = [[TGMediaSelectionContext alloc] initWithGroupingAllowed:self.allowGrouping selectionLimit:100];
if (self.allowGrouping)
selectionContext.grouping = true;
_selectionContext = selectionContext;
}
if (editableItem == nil)
editableItem = _items.lastObject;
@ -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<TGMediaEditableItem> item in selectedItems) {
if ([item isKindOfClass:[TGCameraCapturedPhoto class]] && ((TGCameraCapturedPhoto *)item).rectangle != nil) {
isScan = true;
break;
}
}
if (storeAssets && !isScan) {
NSMutableArray *fullSizeSignals = [[NSMutableArray alloc] init];
for (id<TGMediaEditableItem> item in selectedItems)
{
if ([item isKindOfClass:[TGCameraCapturedPhoto class]] && ((TGCameraCapturedPhoto *)item).rectangle != nil) {
isScan = true;
}
if ([editingContext timerForItem:item] == nil)
{
SSignal *saveMedia = [SSignal defer:^SSignal *
@ -2566,8 +2765,14 @@ static CGPoint TGCameraControllerClampPointToScreenSize(__unused id self, __unus
else if (groupedId != nil && !hasAnyTimers)
dict[@"groupedId"] = groupedId;
id generatedItem = descriptionGenerator(dict, caption, entities, nil);
return generatedItem;
if (isScan) {
if (caption != nil)
dict[@"caption"] = caption;
return dict;
} else {
id generatedItem = descriptionGenerator(dict, caption, entities, nil);
return generatedItem;
}
}];
SSignal *assetSignal = inlineSignal;
@ -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;
}

View File

@ -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

View File

@ -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
{

View File

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

View File

@ -1,4 +1,5 @@
#import "TGCameraInterfaceAssets.h"
#import <CoreText/CoreText.h>
#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

View File

@ -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);

View File

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

View File

@ -26,10 +26,7 @@ const CGFloat TGCameraModeControlVerticalInteritemSpace = 29.0f;
self = [super initWithFrame:frame];
if (self != nil)
{
if (frame.size.width > frame.size.height)
_kerning = 3.5f;
else
_kerning = 2.0f;
_kerning = 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;

View File

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

View File

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

View File

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

View File

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

View File

@ -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];

View File

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

View File

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

View File

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

View File

@ -185,8 +185,6 @@
- (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];

View File

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

View File

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

View File

@ -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];
}
}
}
};

View File

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

View File

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

View File

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

View File

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

View File

@ -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

View File

@ -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<PresentationData>
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<ChannelParticipantsState>
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<Int64> = 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<Bool>()
// override public var ready: Promise<Bool> {
// 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<Never, AddChannelMemberError> 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)
}
}
}
}