diff --git a/submodules/LegacyComponents/.gitignore b/submodules/LegacyComponents/.gitignore new file mode 100644 index 0000000000..1c2b8dd422 --- /dev/null +++ b/submodules/LegacyComponents/.gitignore @@ -0,0 +1,25 @@ +fastlane/README.md +fastlane/report.xml +fastlane/test_output/* +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata +*.xccheckout +*.xcscmblueprint +*.moved-aside +DerivedData +*.hmap +*.ipa +*.xcuserstate +.DS_Store +*.dSYM +*.dSYM.zip +*.ipa +*/xcuserdata/* +LegacyComponents.xcodeproj/* diff --git a/submodules/LegacyComponents/BUCK b/submodules/LegacyComponents/BUCK new file mode 100644 index 0000000000..7f56907f1d --- /dev/null +++ b/submodules/LegacyComponents/BUCK @@ -0,0 +1,380 @@ +load('//tools:buck_utils.bzl', 'config_with_updated_linker_flags', 'combined_config', 'configs_with_config') +load('//tools:buck_defs.bzl', 'SHARED_CONFIGS', 'EXTENSION_LIB_SPECIFIC_CONFIG') + +apple_resource( + name = "LegacyComponentsResources", + dirs = [ + "LegacyComponents/Resources/LegacyComponentsResources.bundle", + ], +) + +apple_library( + name = 'LegacyComponents', + srcs = glob([ + 'LegacyComponents/*.m', + 'LegacyComponents/*.mm', + 'LegacyComponents/*.c', + 'LegacyComponents/*.cpp', + ]), + headers = glob([ + 'LegacyComponents/*.h', + ]), + header_namespace = 'LegacyComponents', + exported_headers = [ + 'LegacyComponents/LegacyComponentsGlobals.h', + 'LegacyComponents/LegacyComponentsContext.h', + 'LegacyComponents/TGLocalization.h', + 'LegacyComponents/TGPluralization.h', + 'LegacyComponents/TGStringUtils.h', + 'LegacyComponents/TGPhoneUtils.h', + 'LegacyComponents/NSObject+TGLock.h', + 'LegacyComponents/RMPhoneFormat.h', + 'LegacyComponents/NSInputStream+TL.h', + 'LegacyComponents/TGFont.h', + 'LegacyComponents/TGImageUtils.h', + 'LegacyComponents/TGDateUtils.h', + 'LegacyComponents/Freedom.h', + 'LegacyComponents/FreedomUIKit.h', + 'LegacyComponents/TGHacks.h', + 'LegacyComponents/TGImageBlur.h', + 'LegacyComponents/UIDevice+PlatformInfo.h', + 'LegacyComponents/TGObserverProxy.h', + 'LegacyComponents/TGModernCache.h', + 'LegacyComponents/TGMemoryImageCache.h', + 'LegacyComponents/LegacyComponentsAccessChecker.h', + 'LegacyComponents/TGTimerTarget.h', + 'LegacyComponents/TGKeyCommand.h', + 'LegacyComponents/TGKeyCommandController.h', + 'LegacyComponents/TGWeakDelegate.h', + 'LegacyComponents/TGCache.h', + 'LegacyComponents/TGLiveUploadInterface.h', + + 'LegacyComponents/JNWSpringAnimation.h', + 'LegacyComponents/POPAnimationEvent.h', + 'LegacyComponents/POPAnimationTracer.h', + 'LegacyComponents/POPAnimation.h', + 'LegacyComponents/POPBasicAnimation.h', + 'LegacyComponents/POPCustomAnimation.h', + 'LegacyComponents/POPDecayAnimation.h', + 'LegacyComponents/POPPropertyAnimation.h', + 'LegacyComponents/POPSpringAnimation.h', + 'LegacyComponents/POPGeometry.h', + 'LegacyComponents/POPAnimatableProperty.h', + + 'LegacyComponents/lmdb.h', + 'LegacyComponents/PSLMDBTable.h', + 'LegacyComponents/PSLMDBKeyValueStore.h', + 'LegacyComponents/PSLMDBKeyValueReaderWriter.h', + 'LegacyComponents/PSLMDBKeyValueCursor.h', + + 'LegacyComponents/PSCoding.h', + 'LegacyComponents/PSData.h', + 'LegacyComponents/PSKeyValueCoder.h', + 'LegacyComponents/PSKeyValueDecoder.h', + 'LegacyComponents/PSKeyValueEncoder.h', + 'LegacyComponents/PSKeyValueReader.h', + 'LegacyComponents/PSKeyValueStore.h', + 'LegacyComponents/PSKeyValueWriter.h', + + 'LegacyComponents/TGPeerIdAdapter.h', + 'LegacyComponents/TGUser.h', + 'LegacyComponents/TGBotInfo.h', + 'LegacyComponents/TGBotComandInfo.h', + 'LegacyComponents/TGConversation.h', + + 'LegacyComponents/TGModernConversationAssociatedInputPanel.h', + 'LegacyComponents/TGModernConversationHashtagsAssociatedPanel.h', + 'LegacyComponents/TGModernConversationMentionsAssociatedPanel.h', + 'LegacyComponents/TGModernConversationAlphacodeAssociatedPanel.h', + 'LegacyComponents/TGSuggestionContext.h', + 'LegacyComponents/TGAlphacode.h', + + 'LegacyComponents/TGTextCheckingResult.h', + 'LegacyComponents/TGChannelBannedRights.h', + 'LegacyComponents/TGChannelAdminRights.h', + 'LegacyComponents/TGDatabaseMessageDraft.h', + 'LegacyComponents/TGMessageGroup.h', + 'LegacyComponents/TGMessageHole.h', + 'LegacyComponents/TGMessageViewCountContentProperty.h', + 'LegacyComponents/TGAuthorSignatureMediaAttachment.h', + 'LegacyComponents/TGWebDocument.h', + 'LegacyComponents/TGInvoiceMediaAttachment.h', + 'LegacyComponents/TGGameMediaAttachment.h', + 'LegacyComponents/TGViaUserAttachment.h', + 'LegacyComponents/TGBotContextResultAttachment.h', + 'LegacyComponents/TGMessageEntity.h', + 'LegacyComponents/TGMessageEntityBold.h', + 'LegacyComponents/TGMessageEntityBotCommand.h', + 'LegacyComponents/TGMessageEntityCode.h', + 'LegacyComponents/TGMessageEntityEmail.h', + 'LegacyComponents/TGMessageEntityHashtag.h', + 'LegacyComponents/TGMessageEntityItalic.h', + 'LegacyComponents/TGMessageEntityMention.h', + 'LegacyComponents/TGMessageEntityMentionName.h', + 'LegacyComponents/TGMessageEntityPre.h', + 'LegacyComponents/TGMessageEntityTextUrl.h', + 'LegacyComponents/TGMessageEntityUrl.h', + 'LegacyComponents/TGMessageEntitiesAttachment.h', + 'LegacyComponents/TGBotReplyMarkup.h', + 'LegacyComponents/TGBotReplyMarkupButton.h', + 'LegacyComponents/TGBotReplyMarkupRow.h', + 'LegacyComponents/TGReplyMarkupAttachment.h', + 'LegacyComponents/TGInstantPage.h', + 'LegacyComponents/TGWebPageMediaAttachment.h', + 'LegacyComponents/TGAudioMediaAttachment.h', + 'LegacyComponents/TGAudioWaveform.h', + 'LegacyComponents/TGStickerPackReference.h', + 'LegacyComponents/TGDocumentAttributeFilename.h', + 'LegacyComponents/TGDocumentAttributeImageSize.h', + 'LegacyComponents/TGDocumentAttributeSticker.h', + 'LegacyComponents/TGDocumentAttributeVideo.h', + 'LegacyComponents/TGDocumentAttributeAnimated.h', + 'LegacyComponents/TGDocumentAttributeAudio.h', + 'LegacyComponents/TGDocumentMediaAttachment.h', + 'LegacyComponents/TGUnsupportedMediaAttachment.h', + 'LegacyComponents/TGForwardedMessageMediaAttachment.h', + 'LegacyComponents/TGContactMediaAttachment.h', + 'LegacyComponents/TGVideoInfo.h', + 'LegacyComponents/TGVideoMediaAttachment.h', + 'LegacyComponents/TGLocalMessageMetaMediaAttachment.h', + 'LegacyComponents/TGLocationMediaAttachment.h', + 'LegacyComponents/TGImageMediaAttachment.h', + 'LegacyComponents/TGMediaAttachment.h', + 'LegacyComponents/TGImageInfo.h', + 'LegacyComponents/TGMediaOriginInfo.h', + 'LegacyComponents/TGMessage.h', + 'LegacyComponents/TGStickerPack.h', + 'LegacyComponents/TGStickerAssociation.h', + 'LegacyComponents/TGPhotoMaskPosition.h', + + 'LegacyComponents/ActionStage.h', + 'LegacyComponents/ASActor.h', + 'LegacyComponents/ASHandle.h', + 'LegacyComponents/ASQueue.h', + 'LegacyComponents/ASWatcher.h', + 'LegacyComponents/SGraphListNode.h', + 'LegacyComponents/SGraphNode.h', + 'LegacyComponents/SGraphObjectNode.h', + + 'LegacyComponents/TGLabel.h', + 'LegacyComponents/TGToolbarButton.h', + 'LegacyComponents/UIScrollView+TGHacks.h', + 'LegacyComponents/TGAnimationBlockDelegate.h', + 'LegacyComponents/TGBackdropView.h', + 'LegacyComponents/UIImage+TG.h', + 'LegacyComponents/TGStaticBackdropAreaData.h', + 'LegacyComponents/TGStaticBackdropImageData.h', + 'LegacyComponents/TGImageLuminanceMap.h', + 'LegacyComponents/TGFullscreenContainerView.h', + 'LegacyComponents/TGDoubleTapGestureRecognizer.h', + 'LegacyComponents/TGModernButton.h', + 'LegacyComponents/TGModernToolbarButton.h', + 'LegacyComponents/TGModernBackToolbarButton.h', + 'LegacyComponents/UIControl+HitTestEdgeInsets.h', + 'LegacyComponents/TGMenuView.h', + 'LegacyComponents/TGImageView.h', + 'LegacyComponents/UICollectionView+Utils.h', + 'LegacyComponents/TGMessageImageViewOverlayView.h', + 'LegacyComponents/TGLetteredAvatarView.h', + 'LegacyComponents/TGGradientLabel.h', + 'LegacyComponents/TGRemoteImageView.h', + + 'LegacyComponents/TGProgressSpinnerView.h', + 'LegacyComponents/TGProgressWindow.h', + + 'LegacyComponents/TGMenuSheetController.h', + 'LegacyComponents/TGMenuSheetButtonItemView.h', + 'LegacyComponents/TGMenuSheetCollectionView.h', + 'LegacyComponents/TGMenuSheetItemView.h', + 'LegacyComponents/TGMenuSheetTitleItemView.h', + 'LegacyComponents/TGMenuSheetView.h', + + 'LegacyComponents/HPGrowingTextView.h', + 'LegacyComponents/HPTextViewInternal.h', + 'LegacyComponents/TGInputTextTag.h', + + 'LegacyComponents/TGStickerKeyboardTabPanel.h', + + 'LegacyComponents/TGItemPreviewController.h', + 'LegacyComponents/TGItemPreviewView.h', + 'LegacyComponents/TGItemMenuSheetPreviewView.h', + + 'LegacyComponents/TGImageManager.h', + 'LegacyComponents/TGDataResource.h', + 'LegacyComponents/TGImageDataSource.h', + 'LegacyComponents/TGImageManagerTask.h', + + 'LegacyComponents/TGRTLScreenEdgePanGestureRecognizer.h', + 'LegacyComponents/TGPopoverController.h', + 'LegacyComponents/TGNavigationController.h', + 'LegacyComponents/TGNavigationBar.h', + 'LegacyComponents/TGViewController.h', + 'LegacyComponents/TGViewController+TGRecursiveEnumeration.h', + 'LegacyComponents/TGOverlayController.h', + 'LegacyComponents/TGOverlayControllerWindow.h', + + 'LegacyComponents/TGMediaAssetsLibrary.h', + 'LegacyComponents/TGMediaAssetsModernLibrary.h', + 'LegacyComponents/TGMediaAsset.h', + 'LegacyComponents/TGMediaAssetFetchResult.h', + 'LegacyComponents/TGMediaAssetFetchResultChange.h', + 'LegacyComponents/TGMediaAssetGroup.h', + 'LegacyComponents/TGMediaAssetMoment.h', + 'LegacyComponents/TGMediaAssetMomentList.h', + 'LegacyComponents/TGMediaAssetImageSignals.h', + 'LegacyComponents/TGMediaSelectionContext.h', + 'LegacyComponents/TGMediaEditingContext.h', + + 'LegacyComponents/TGModernGalleryZoomableItemViewContent.h', + 'LegacyComponents/TGModernGalleryZoomableScrollView.h', + 'LegacyComponents/TGModernGalleryZoomableScrollViewSwipeGestureRecognizer.h', + 'LegacyComponents/TGModernGalleryVideoView.h', + 'LegacyComponents/TGModernGalleryScrollView.h', + 'LegacyComponents/TGModernGalleryItem.h', + 'LegacyComponents/TGModernGalleryItemView.h', + 'LegacyComponents/TGModernGalleryImageItem.h', + 'LegacyComponents/TGModernGalleryImageItemView.h', + 'LegacyComponents/TGModernGalleryImageItemImageView.h', + 'LegacyComponents/TGModernGalleryEditableItemView.h', + 'LegacyComponents/TGModernGallerySelectableItem.h', + 'LegacyComponents/TGModernGalleryDefaultFooterAccessoryView.h', + 'LegacyComponents/TGModernGalleryDefaultFooterView.h', + 'LegacyComponents/TGModernGalleryDefaultHeaderView.h', + 'LegacyComponents/TGModernGalleryDefaultInterfaceView.h', + 'LegacyComponents/TGModernGalleryInterfaceView.h', + 'LegacyComponents/TGModernGalleryImageItemContainerView.h', + 'LegacyComponents/TGModernGalleryZoomableItemView.h', + 'LegacyComponents/TGModernGalleryModel.h', + 'LegacyComponents/TGModernGalleryTransitionView.h', + 'LegacyComponents/TGModernGalleryView.h', + 'LegacyComponents/TGModernGalleryContainerView.h', + 'LegacyComponents/TGModernGalleryEmbeddedStickersHeaderView.h', + 'LegacyComponents/TGModernGalleryController.h', + + 'LegacyComponents/TGPhotoToolbarView.h', + 'LegacyComponents/TGMediaPickerGalleryModel.h', + 'LegacyComponents/TGMediaPickerGalleryInterfaceView.h', + 'LegacyComponents/TGPhotoEditorController.h', + 'LegacyComponents/TGMediaAvatarEditorTransition.h', + + 'LegacyComponents/TGPhotoEditorUtils.h', + 'LegacyComponents/PGPhotoEditorValues.h', + 'LegacyComponents/TGVideoEditAdjustments.h', + 'LegacyComponents/AVURLAsset+TGMediaItem.h', + 'LegacyComponents/UIImage+TGMediaEditableItem.h', + 'LegacyComponents/TGMediaVideoConverter.h', + 'LegacyComponents/TGGifConverter.h', + + 'LegacyComponents/TGPhotoEditorAnimation.h', + + 'LegacyComponents/TGPaintingData.h', + 'LegacyComponents/TGPaintUtils.h', + 'LegacyComponents/TGPhotoPaintEntity.h', + 'LegacyComponents/TGPhotoPaintStickerEntity.h', + 'LegacyComponents/TGPaintUndoManager.h', + + 'LegacyComponents/PGCamera.h', + 'LegacyComponents/PGCameraCaptureSession.h', + 'LegacyComponents/PGCameraDeviceAngleSampler.h', + 'LegacyComponents/PGCameraMomentSegment.h', + 'LegacyComponents/PGCameraMomentSession.h', + 'LegacyComponents/PGCameraMovieWriter.h', + 'LegacyComponents/PGCameraShotMetadata.h', + 'LegacyComponents/PGCameraVolumeButtonHandler.h', + 'LegacyComponents/TGCameraPreviewView.h', + 'LegacyComponents/TGCameraMainPhoneView.h', + 'LegacyComponents/TGCameraMainTabletView.h', + 'LegacyComponents/TGCameraMainView.h', + 'LegacyComponents/TGCameraFlashActiveView.h', + 'LegacyComponents/TGCameraFlashControl.h', + 'LegacyComponents/TGCameraFlipButton.h', + 'LegacyComponents/TGCameraInterfaceAssets.h', + 'LegacyComponents/TGCameraModeControl.h', + 'LegacyComponents/TGCameraSegmentsView.h', + 'LegacyComponents/TGCameraShutterButton.h', + 'LegacyComponents/TGCameraTimeCodeView.h', + 'LegacyComponents/TGCameraZoomView.h', + 'LegacyComponents/TGCameraPhotoPreviewController.h', + 'LegacyComponents/TGCameraController.h', + 'LegacyComponents/TGCameraCapturedPhoto.h', + 'LegacyComponents/TGCameraCapturedVideo.h', + 'LegacyComponents/TGPhotoVideoEditor.h', + + 'LegacyComponents/TGModernConversationTitleActivityIndicator.h', + 'LegacyComponents/TGEmbedPIPButton.h', + 'LegacyComponents/TGEmbedPIPPullArrowView.h', + 'LegacyComponents/TGEmbedPlayerState.h', + 'LegacyComponents/TGAttachmentCameraView.h', + 'LegacyComponents/TGMediaAvatarMenuMixin.h', + 'LegacyComponents/TGPassportAttachMenu.h', + 'LegacyComponents/TGPassportScanController.h', + 'LegacyComponents/TGPassportOCR.h', + 'LegacyComponents/TGPassportMRZ.h', + 'LegacyComponents/TGPasscodeEntryController.h', + 'LegacyComponents/TGPassportICloud.h', + 'LegacyComponents/TGEmbedPlayerView.h', + 'LegacyComponents/TGWallpaperInfo.h', + 'LegacyComponents/LegacyHTTPRequestOperation.h', + + 'LegacyComponents/TGAttachmentCarouselItemView.h', + 'LegacyComponents/TGMediaAssetsController.h', + + 'LegacyComponents/TGLocationVenue.h', + 'LegacyComponents/TGLocationMapViewController.h', + 'LegacyComponents/TGLocationPickerController.h', + 'LegacyComponents/TGLocationViewController.h', + 'LegacyComponents/TGListsTableView.h', + 'LegacyComponents/TGSearchBar.h', + 'LegacyComponents/TGSearchDisplayMixin.h', + + 'LegacyComponents/TGPhotoEditorSliderView.h', + + 'LegacyComponents/TGClipboardGalleryMixin.h', + 'LegacyComponents/TGClipboardGalleryPhotoItem.h', + 'LegacyComponents/TGVideoMessageCaptureController.h', + 'LegacyComponents/TGModernConversationInputMicButton.h', + + 'LegacyComponents/TGLocationPulseView.h', + 'LegacyComponents/TGLocationWavesView.h', + 'LegacyComponents/TGLocationLiveElapsedView.h', + 'LegacyComponents/TGLocationLiveSessionItemView.h', + + 'LegacyComponents/TGTooltipView.h', + + 'LegacyComponents/TGCheckButtonView.h', + 'LegacyComponents/TGBuiltinWallpaperInfo.h', + 'LegacyComponents/TGRemoteWallpaperInfo.h', + 'LegacyComponents/TGColorWallpaperInfo.h', + 'LegacyComponents/TGCustomImageWallpaperInfo.h', + 'LegacyComponents/TGClipboardMenu.h', + 'LegacyComponents/TGImagePickerController.h', + 'LegacyComponents/TGLegacyCameraController.h', + 'LegacyComponents/TGWallpaperController.h', + 'LegacyComponents/TGProxyWindow.h', + 'LegacyComponents/TGIconSwitchView.h', + + 'LegacyComponents/TGModernGalleryEditableItem.h', + 'LegacyComponents/TGPhotoEditorButton.h', + + 'LegacyComponents/TGActionMediaAttachment.h', + 'LegacyComponents/TGReplyMessageMediaAttachment.h', + 'LegacyComponents/TGMessageEntityPhone.h', + 'LegacyComponents/TGMessageEntityCashtag.h', + 'LegacyComponents/TGPIPAblePlayerView.h', + 'LegacyComponents/TGEmbedPlayerControls.h', + ], + modular = True, + configs = configs_with_config(combined_config([SHARED_CONFIGS, EXTENSION_LIB_SPECIFIC_CONFIG])), + compiler_flags = ['-w'], + preprocessor_flags = ['-fobjc-arc'], + visibility = ['PUBLIC'], + deps = [ + ':LegacyComponentsResources', + '//submodules/SSignalKit:SSignalKit', + ], + frameworks = [ + '$SDKROOT/System/Library/Frameworks/Foundation.framework', + '$SDKROOT/System/Library/Frameworks/UIKit.framework', + '$SDKROOT/System/Library/Frameworks/Vision.framework', + ], +) diff --git a/submodules/LegacyComponents/LegacyComponents/ASActor.h b/submodules/LegacyComponents/LegacyComponents/ASActor.h new file mode 100644 index 0000000000..807d1a10c1 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/ASActor.h @@ -0,0 +1,36 @@ +#import + +#import + +@interface ASActor : NSObject + ++ (void)registerActorClass:(Class)requestBuilderClass; + ++ (ASActor *)requestBuilderForGenericPath:(NSString *)genericPath path:(NSString *)path; + ++ (NSString *)genericPath; + +@property (nonatomic, strong) NSString *path; + +@property (nonatomic, strong) NSString *requestQueueName; +@property (nonatomic, strong) NSDictionary *storedOptions; + +@property (nonatomic) bool requiresAuthorization; + +@property (nonatomic) NSTimeInterval cancelTimeout; +@property (nonatomic, strong) id cancelToken; +@property (nonatomic, strong) NSMutableArray *multipleCancelTokens; +@property (nonatomic) bool cancelled; + +- (id)initWithPath:(NSString *)path; +- (void)prepare:(NSDictionary *)options; +- (void)execute:(NSDictionary *)options; +- (void)cancel; + +- (void)addCancelToken:(id)token; + +- (void)watcherJoined:(ASHandle *)watcherHandle options:(NSDictionary *)options waitingInActorQueue:(bool)waitingInActorQueue; + +- (void)handleRequestProblem; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/ASActor.m b/submodules/LegacyComponents/LegacyComponents/ASActor.m new file mode 100644 index 0000000000..4ee9ad48fa --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/ASActor.m @@ -0,0 +1,121 @@ +#import "ASActor.h" + +#import "ActionStage.h" + +#import "LegacyComponentsInternal.h" + +static NSMutableDictionary *registeredRequestBuilders() +{ + static NSMutableDictionary *dict = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^ + { + dict = [[NSMutableDictionary alloc] init]; + }); + return dict; +} + +@implementation ASActor + ++ (void)registerActorClass:(Class)requestBuilderClass +{ + NSString *genericPath = [requestBuilderClass genericPath]; + if (genericPath == nil || genericPath.length == 0) + { + TGLegacyLog(@"Error: ASActor::registerActorClass: genericPath is nil"); + return; + } + + [registeredRequestBuilders() setObject:requestBuilderClass forKey:genericPath]; +} + ++ (ASActor *)requestBuilderForGenericPath:(NSString *)genericPath path:(NSString *)path +{ + Class builderClass = [registeredRequestBuilders() objectForKey:genericPath]; + if (builderClass != nil) + { + ASActor *builderInstance = [[builderClass alloc] initWithPath:path]; + return builderInstance; + } + return nil; +} + ++ (NSString *)genericPath +{ + TGLegacyLog(@"Error: ASActor::genericPath: no default implementation provided"); + + return nil; +} + +@synthesize path = _path; + +@synthesize requestQueueName = _requestQueueName; +@synthesize storedOptions = _storedOptions; + +@synthesize requiresAuthorization = _requiresAuthorization; + +@synthesize cancelTimeout = _cancelTimeout; +@synthesize cancelToken = _cancelToken; +@synthesize cancelled = _cancelled; + +/*#if TARGET_IPHONE_SIMULATOR +static int instanceCount = 0; +#endif*/ + +- (id)initWithPath:(NSString *)path +{ + self = [super init]; + if (self != nil) + { + _cancelTimeout = 0; + _path = path; + +/*#if TARGET_IPHONE_SIMULATOR + instanceCount++; + + TGLegacyLog(@"%d actors (++)", instanceCount); +#endif*/ + } + return self; +} + +- (void)dealloc +{ +/*#if TARGET_IPHONE_SIMULATOR + instanceCount--; + + TGLegacyLog(@"%d actors (--)", instanceCount); +#endif*/ +} + +- (void)prepare:(NSDictionary *)__unused options +{ +} + +- (void)execute:(NSDictionary *)__unused options +{ + TGLegacyLog(@"Error: ASActor::execute: no default implementation provided"); +} + +- (void)cancel +{ + self.cancelled = true; +} + +- (void)addCancelToken:(id)token +{ + if (_multipleCancelTokens == nil) + _multipleCancelTokens = [[NSMutableArray alloc] init]; + + [_multipleCancelTokens addObject:token]; +} + +- (void)handleRequestProblem +{ +} + +- (void)watcherJoined:(ASHandle *)__unused watcherHandle options:(NSDictionary *)__unused options waitingInActorQueue:(bool)__unused waitingInActorQueue +{ +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/ASHandle.h b/submodules/LegacyComponents/LegacyComponents/ASHandle.h new file mode 100644 index 0000000000..0c0a134df3 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/ASHandle.h @@ -0,0 +1,21 @@ +#import + +@protocol ASWatcher; + +@interface ASHandle : NSObject + +@property (nonatomic, weak) id delegate; +@property (nonatomic) bool releaseOnMainThread; + +- (id)initWithDelegate:(id)delegate; +- (id)initWithDelegate:(id)delegate releaseOnMainThread:(bool)releaseOnMainThread; +- (void)reset; + +- (bool)hasDelegate; + +- (void)requestAction:(NSString *)action options:(id)options; +- (void)receiveActorMessage:(NSString *)path messageType:(NSString *)messageType message:(id)message; +- (void)notifyResourceDispatched:(NSString *)path resource:(id)resource; +- (void)notifyResourceDispatched:(NSString *)path resource:(id)resource arguments:(id)arguments; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/ASHandle.m b/submodules/LegacyComponents/LegacyComponents/ASHandle.m new file mode 100644 index 0000000000..8cfd53620b --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/ASHandle.m @@ -0,0 +1,130 @@ +#import "ASHandle.h" + +#import + +#import "ASWatcher.h" + +@interface ASHandle () +{ + TG_SYNCHRONIZED_DEFINE(_delegate); +} + +@end + +@implementation ASHandle + +@synthesize delegate = _delegate; +@synthesize releaseOnMainThread = _releaseOnMainThread; + +- (id)initWithDelegate:(id)delegate +{ + self = [super init]; + if (self != nil) + { + TG_SYNCHRONIZED_INIT(_delegate); + + _delegate = delegate; + } + return self; +} + +- (id)initWithDelegate:(id)delegate releaseOnMainThread:(bool)releaseOnMainThread +{ + self = [super init]; + if (self != nil) + { + TG_SYNCHRONIZED_INIT(_delegate); + + _delegate = delegate; + _releaseOnMainThread = releaseOnMainThread; + } + return self; +} + +- (void)reset +{ + TG_SYNCHRONIZED_BEGIN(_delegate); + _delegate = nil; + TG_SYNCHRONIZED_END(_delegate); +} + +- (bool)hasDelegate +{ + bool result = false; + + TG_SYNCHRONIZED_BEGIN(_delegate); + result = _delegate != nil; + TG_SYNCHRONIZED_END(_delegate); + + return result; +} + +- (id)delegate +{ + id result = nil; + + TG_SYNCHRONIZED_BEGIN(_delegate); + result = _delegate; + TG_SYNCHRONIZED_END(_delegate); + + return result; +} + +- (void)setDelegate:(id)delegate +{ + TG_SYNCHRONIZED_BEGIN(_delegate); + _delegate = delegate; + TG_SYNCHRONIZED_END(_delegate); +} + +- (void)requestAction:(NSString *)action options:(id)options +{ + __strong id delegate = self.delegate; + if (delegate != nil && [delegate respondsToSelector:@selector(actionStageActionRequested:options:)]) + [delegate actionStageActionRequested:action options:options]; + + if (_releaseOnMainThread && ![NSThread isMainThread]) + { + dispatch_async(dispatch_get_main_queue(), ^ + { + [delegate class]; + }); + } +} + +- (void)receiveActorMessage:(NSString *)path messageType:(NSString *)messageType message:(id)message +{ + __strong id delegate = self.delegate; + if (delegate != nil && [delegate respondsToSelector:@selector(actorMessageReceived:messageType:message:)]) + [delegate actorMessageReceived:path messageType:messageType message:message]; + + if (_releaseOnMainThread && ![NSThread isMainThread]) + { + dispatch_async(dispatch_get_main_queue(), ^ + { + [delegate class]; + }); + } +} + +- (void)notifyResourceDispatched:(NSString *)path resource:(id)resource +{ + [self notifyResourceDispatched:path resource:resource arguments:nil]; +} + +- (void)notifyResourceDispatched:(NSString *)path resource:(id)resource arguments:(id)arguments +{ + __strong id delegate = self.delegate; + if (delegate != nil && [delegate respondsToSelector:@selector(actionStageResourceDispatched:resource:arguments:)]) + [delegate actionStageResourceDispatched:path resource:resource arguments:arguments]; + + if (_releaseOnMainThread && ![NSThread isMainThread]) + { + dispatch_async(dispatch_get_main_queue(), ^ + { + [delegate class]; + }); + } +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/ASQueue.h b/submodules/LegacyComponents/LegacyComponents/ASQueue.h new file mode 100644 index 0000000000..15e95646fa --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/ASQueue.h @@ -0,0 +1,15 @@ +#import + +@interface ASQueue : NSObject + +- (instancetype)initWithName:(const char *)name; + ++ (ASQueue *)mainQueue; + +- (dispatch_queue_t)nativeQueue; + +- (bool)isCurrentQueue; +- (void)dispatchOnQueue:(dispatch_block_t)block; +- (void)dispatchOnQueue:(dispatch_block_t)block synchronous:(bool)synchronous; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/ASQueue.m b/submodules/LegacyComponents/LegacyComponents/ASQueue.m new file mode 100644 index 0000000000..347c6de191 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/ASQueue.m @@ -0,0 +1,100 @@ + + +#import "ASQueue.h" + +@interface ASQueue () +{ + bool _isMainQueue; + dispatch_queue_t _queue; + + const char *_name; +} + +@end + +@implementation ASQueue + +- (instancetype)initWithName:(const char *)name +{ + self = [super init]; + if (self != nil) + { + _name = name; + + _queue = dispatch_queue_create(_name, 0); + dispatch_queue_set_specific(_queue, _name, (void *)_name, NULL); + } + return self; +} + +- (void)dealloc +{ +#if !OS_OBJECT_HAVE_OBJC_SUPPORT + dispatch_release(_queue); +#endif + _queue = nil; +} + ++ (ASQueue *)mainQueue +{ + static ASQueue *queue = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^ + { + queue = [[ASQueue alloc] init]; + queue->_queue = dispatch_get_main_queue(); + queue->_isMainQueue = true; + }); + return queue; +} + +- (dispatch_queue_t)nativeQueue +{ + return _queue; +} + +- (bool)isCurrentQueue +{ + if (_queue == nil) + return false; + + if (_isMainQueue) + return [NSThread isMainThread]; + else + return dispatch_get_specific(_name) == _name; +} + +- (void)dispatchOnQueue:(dispatch_block_t)block +{ + [self dispatchOnQueue:block synchronous:false]; +} + +- (void)dispatchOnQueue:(dispatch_block_t)block synchronous:(bool)synchronous +{ + if (block == nil) + return; + + if (_queue != nil) + { + if (_isMainQueue) + { + if ([NSThread isMainThread]) + block(); + else if (synchronous) + dispatch_sync(_queue, block); + else + dispatch_async(_queue, block); + } + else + { + if (dispatch_get_specific(_name) == _name) + block(); + else if (synchronous) + dispatch_sync(_queue, block); + else + dispatch_async(_queue, block); + } + } +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/ASWatcher.h b/submodules/LegacyComponents/LegacyComponents/ASWatcher.h new file mode 100644 index 0000000000..02870068cb --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/ASWatcher.h @@ -0,0 +1,20 @@ +#import + +#import +#import + +@protocol ASWatcher + +@required + +@property (nonatomic, strong, readonly) ASHandle *actionHandle; + +@optional + +- (void)actorCompleted:(int)status path:(NSString *)path result:(id)result; +- (void)actorReportedProgress:(NSString *)path progress:(float)progress; +- (void)actionStageResourceDispatched:(NSString *)path resource:(id)resource arguments:(id)arguments; +- (void)actionStageActionRequested:(NSString *)action options:(id)options; +- (void)actorMessageReceived:(NSString *)path messageType:(NSString *)messageType message:(id)message; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/AVAsset+CBExtension.h b/submodules/LegacyComponents/LegacyComponents/AVAsset+CBExtension.h new file mode 100755 index 0000000000..eb5cb5c3f4 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/AVAsset+CBExtension.h @@ -0,0 +1,16 @@ +// +// AVAsset+Extension.h +// CoubPlayer +// +// Created by Pavel Tikhonenko on 19/10/14. +// Copyright (c) 2014 Pavel Tikhonenko. All rights reserved. +// + +#import + +@interface AVAsset (CBExtension) + +@property (nonatomic, readonly) AVAssetTrack *anyVideoTrack; +@property (nonatomic, readonly) AVAssetTrack *anyAudioTrack; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/AVAsset+CBExtension.m b/submodules/LegacyComponents/LegacyComponents/AVAsset+CBExtension.m new file mode 100755 index 0000000000..4ecf0fe334 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/AVAsset+CBExtension.m @@ -0,0 +1,25 @@ +// +// AVAsset+Extension.m +// CoubPlayer +// +// Created by Pavel Tikhonenko on 19/10/14. +// Copyright (c) 2014 Pavel Tikhonenko. All rights reserved. +// + +#import "AVAsset+CBExtension.h" + +@implementation AVAsset (CBExtension) + +- (AVAssetTrack *)anyVideoTrack +{ + NSArray *videoTracks = [self tracksWithMediaType:AVMediaTypeVideo]; + return [videoTracks count] ? videoTracks[0] : nil; +} + +- (AVAssetTrack *)anyAudioTrack +{ + NSArray *audioTracks = [self tracksWithMediaType:AVMediaTypeAudio]; + return [audioTracks count] ? audioTracks[0] : nil; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/AVURLAsset+TGMediaItem.h b/submodules/LegacyComponents/LegacyComponents/AVURLAsset+TGMediaItem.h new file mode 100644 index 0000000000..49bd8869b6 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/AVURLAsset+TGMediaItem.h @@ -0,0 +1,7 @@ +#import +#import +#import + +@interface AVURLAsset (TGMediaItem) + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/AVURLAsset+TGMediaItem.m b/submodules/LegacyComponents/LegacyComponents/AVURLAsset+TGMediaItem.m new file mode 100644 index 0000000000..b4f03a5cb8 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/AVURLAsset+TGMediaItem.m @@ -0,0 +1,42 @@ +#import "AVURLAsset+TGMediaItem.h" +#import + +#import + +@implementation AVURLAsset (TGMediaItem) + +- (bool)isVideo +{ + return true; +} + +- (NSString *)uniqueIdentifier +{ + return self.URL.absoluteString; +} + +- (CGSize)originalSize +{ + AVAssetTrack *track = self.tracks.firstObject; + return CGRectApplyAffineTransform((CGRect){ CGPointZero, track.naturalSize }, track.preferredTransform).size; +} + +- (SSignal *)thumbnailImageSignal +{ + CGFloat thumbnailImageSide = TGPhotoThumbnailSizeForCurrentScreen().width; + CGSize size = TGScaleToSize(self.originalSize, CGSizeMake(thumbnailImageSide, thumbnailImageSide)); + + return [TGMediaAssetImageSignals videoThumbnailForAVAsset:self size:size timestamp:kCMTimeZero]; +} + +- (SSignal *)screenImageSignal:(NSTimeInterval)__unused position +{ + return nil; +} + +- (SSignal *)originalImageSignal:(NSTimeInterval)__unused position +{ + return nil; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/ActionStage.h b/submodules/LegacyComponents/LegacyComponents/ActionStage.h new file mode 100644 index 0000000000..73f2adf61f --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/ActionStage.h @@ -0,0 +1,82 @@ +#import + +#import +#import + +#import + +typedef enum { + ASStatusSuccess = 0, + ASStatusFailed = -1 +} ASStatus; + +#ifdef DEBUG +#define dispatchOnStageQueue dispatchOnStageQueueDebug:__FILE__ line:__LINE__ block +#endif + +@class ActionStage; + +#ifdef __cplusplus +extern "C" { +#endif + +ActionStage *ActionStageInstance(); + +#ifdef __cplusplus +} +#endif + +typedef enum { + TGActorRequestChangePriority = 1 +} TGActorRequestFlags; + +@interface ActionStage : NSObject + +- (dispatch_queue_t)globalStageDispatchQueue; +#ifdef DEBUG +- (void)dispatchOnStageQueueDebug:(const char *)function line:(int)line block:(dispatch_block_t)block; +#else +- (void)dispatchOnStageQueue:(dispatch_block_t)block; +#endif + +- (NSFileManager *)globalFileManager; + +- (bool)isCurrentQueueStageQueue; + +- (void)cancelActorTimeout:(NSString *)path; + +- (NSString *)genericStringForParametrizedPath:(NSString *)path; + +- (void)requestActor:(NSString *)path options:(NSDictionary *)options flags:(int)flags watcher:(id)watcher; +- (void)requestActor:(NSString *)path options:(NSDictionary *)options watcher:(id)watcher; +- (void)changeActorPriority:(NSString *)path; + +- (NSArray *)rejoinActionsWithGenericPathNow:(NSString *)genericPath prefix:(NSString *)prefix watcher:(id)watcher; +- (bool)isExecutingActorsWithGenericPath:(NSString *)genericPath; +- (bool)isExecutingActorsWithPathPrefix:(NSString *)pathPrefix; +- (NSArray *)executingActorsWithPathPrefix:(NSString *)pathPrefix; +- (ASActor *)executingActorWithPath:(NSString *)path; + +- (void)watchForPath:(NSString *)path watcher:(id)watcher; +- (void)watchForPaths:(NSArray *)paths watcher:(id)watcher; +- (void)watchForGenericPath:(NSString *)path watcher:(id)watcher; +- (void)watchForMessagesToWatchersAtGenericPath:(NSString *)genericPath watcher:(id)watcher; +- (void)removeWatcherByHandle:(ASHandle *)actionHandle; +- (void)removeWatcher:(id)watcher; +- (void)removeWatcherByHandle:(ASHandle *)actionHandle fromPath:(NSString *)path; +- (void)removeWatcher:(id)watcher fromPath:(NSString *)path; +- (void)removeAllWatchersFromPath:(NSString *)path; + +- (bool)requestActorStateNow:(NSString *)path; + +- (void)dispatchResource:(NSString *)path resource:(id)resource arguments:(id)arguments; +- (void)dispatchResource:(NSString *)path resource:(id)resource; + +- (void)actionCompleted:(NSString *)action result:(id)result; +- (void)dispatchMessageToWatchers:(NSString *)path messageType:(NSString *)messageType message:(id)message; +- (void)actionFailed:(NSString *)action reason:(int)reason; +- (void)nodeRetrieved:(NSString *)path node:(SGraphNode *)node; +- (void)nodeRetrieveProgress:(NSString *)path progress:(float)progress; +- (void)nodeRetrieveFailed:(NSString *)path; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/ActionStage.mm b/submodules/LegacyComponents/LegacyComponents/ActionStage.mm new file mode 100644 index 0000000000..2827a5da98 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/ActionStage.mm @@ -0,0 +1,1242 @@ +#import "ActionStage.h" + +#import + +#import "LegacyComponentsInternal.h" + +#import "ASActor.h" + +#import + +#include +#include + +static const char *graphQueueSpecific = "com.telegraph.graphdispatchqueue"; + +static dispatch_queue_t mainGraphQueue = nil; +static dispatch_queue_t globalGraphQueue = nil; +static dispatch_queue_t highPriorityGraphQueue = nil; + +static volatile OSSpinLock removeWatcherRequestsLock = OS_SPINLOCK_INIT; +static volatile OSSpinLock removeWatcherFromPathRequestsLock = OS_SPINLOCK_INIT; + +@interface ActionStage () +{ + std::vector > _removeWatcherFromPathRequests; + std::vector _removeWatcherRequests; +} + +@property (nonatomic, strong) NSMutableDictionary *requestQueues; + +@property (nonatomic, strong) NSMutableDictionary *activeRequests; +@property (nonatomic, strong) NSMutableDictionary *cancelRequestTimers; + +@property (nonatomic, strong) NSMutableDictionary *liveNodeWatchers; +@property (nonatomic, strong) NSMutableDictionary *actorMessagesWatchers; + +@end + +ActionStage *ActionStageInstance() +{ + static ActionStage *singleton = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^ + { + singleton = [[ActionStage alloc] init]; + }); + + return singleton; +} + +@implementation ActionStage + +#pragma mark - Singleton + +#pragma mark - Implemetation + +@synthesize requestQueues = _requestQueues; + +@synthesize activeRequests = _activeRequests; +@synthesize cancelRequestTimers = _cancelRequestTimers; + +@synthesize liveNodeWatchers = _liveNodeWatchers; +@synthesize actorMessagesWatchers = _actorMessagesWatchers; + +- (id)init +{ + self = [super init]; + if (self != nil) + { + _requestQueues = [[NSMutableDictionary alloc] init]; + + _activeRequests = [[NSMutableDictionary alloc] init]; + _cancelRequestTimers = [[NSMutableDictionary alloc] init]; + + _liveNodeWatchers = [[NSMutableDictionary alloc] init]; + _actorMessagesWatchers = [[NSMutableDictionary alloc] init]; + } + return self; +} + +- (dispatch_queue_t)globalStageDispatchQueue +{ + if (mainGraphQueue == NULL) + { + mainGraphQueue = dispatch_queue_create("com.telegraph.graphdispatchqueue", 0); + + globalGraphQueue = dispatch_queue_create("com.telegraph.graphdispatchqueue-global", 0); + dispatch_set_target_queue(globalGraphQueue, mainGraphQueue); + + highPriorityGraphQueue = dispatch_queue_create("com.telegraph.graphdispatchqueue-high", 0); + dispatch_set_target_queue(highPriorityGraphQueue, mainGraphQueue); + + dispatch_queue_set_specific(mainGraphQueue, graphQueueSpecific, (void *)graphQueueSpecific, NULL); + dispatch_queue_set_specific(globalGraphQueue, graphQueueSpecific, (void *)graphQueueSpecific, NULL); + dispatch_queue_set_specific(highPriorityGraphQueue, graphQueueSpecific, (void *)graphQueueSpecific, NULL); + } + return globalGraphQueue; +} + +- (bool)isCurrentQueueStageQueue +{ + return dispatch_get_specific(graphQueueSpecific) != NULL; +} + +#ifdef DEBUG +- (void)dispatchOnStageQueueDebug:(const char *)function line:(int)line block:(dispatch_block_t)block +#else +- (void)dispatchOnStageQueue:(dispatch_block_t)block +#endif +{ + bool isGraphQueue = false; + + isGraphQueue = dispatch_get_specific(graphQueueSpecific) != NULL; + + if (isGraphQueue) + { +#ifdef DEBUG + CFAbsoluteTime startTime = CFAbsoluteTimeGetCurrent(); +#endif + + block(); + +#ifdef DEBUG + CFAbsoluteTime executionTime = (CFAbsoluteTimeGetCurrent() - startTime); + if (executionTime > 0.1) + TGLegacyLog(@"***** Dispatch from %s:%d took %f s", function, line, executionTime); +#endif + } + else + { +#ifdef DEBUG + dispatch_async([self globalStageDispatchQueue], ^ + { + CFAbsoluteTime startTime = CFAbsoluteTimeGetCurrent(); + + block(); + + CFAbsoluteTime executionTime = (CFAbsoluteTimeGetCurrent() - startTime); + if (executionTime > 0.1) + TGLegacyLog(@"***** Dispatch from %s:%d took %f s", function, line, executionTime); + }); +#else + dispatch_async([self globalStageDispatchQueue], block); +#endif + } +} + +- (void)dispatchOnHighPriorityQueue:(dispatch_block_t)block +{ + if ([self isCurrentQueueStageQueue]) + block(); + else + { + if (highPriorityGraphQueue == NULL) + [self globalStageDispatchQueue]; + + dispatch_async(highPriorityGraphQueue, block); + } +} + +- (void)dumpGraphState +{ + [self dispatchOnStageQueue:^ + { + TGLegacyLog(@"===== SGraph State ====="); + TGLegacyLog(@"%d live node watchers", _liveNodeWatchers.count); + [_liveNodeWatchers enumerateKeysAndObjectsUsingBlock:^(NSString *path, NSArray *watchers, __unused BOOL *stop) + { + TGLegacyLog(@" %@", path); + for (ASHandle *handle in watchers) + { + id watcher = handle.delegate; + if (watcher != nil) + { + TGLegacyLog(@" %@", [watcher description]); + } + } + }]; + TGLegacyLog(@"%d requests", _activeRequests.count); + [_activeRequests enumerateKeysAndObjectsUsingBlock:^(NSString *path, __unused id obj, __unused BOOL *stop) { + TGLegacyLog(@" %@", path); + }]; + TGLegacyLog(@"========================"); + }]; +} + +- (NSFileManager *)globalFileManager +{ + static NSFileManager *fileManager = nil; + + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^ + { + fileManager = [[NSFileManager alloc] init]; + }); + + return fileManager; +} + +- (NSString *)optionsHash:(NSDictionary *)options +{ + if (options.count == 0) + return @""; + + NSMutableString *string = [[NSMutableString alloc] initWithString:@"#"]; + + Class StringClass = [NSString class]; + NSArray *keys = [[options allKeys] sortedArrayUsingComparator:^NSComparisonResult(id obj1, id obj2) + { + if ([obj1 isKindOfClass:StringClass] && [obj2 isKindOfClass:StringClass]) + { + return [(NSString *)obj1 compare:(NSString *)obj2]; + } + else + return NSOrderedSame; + }]; + + bool first = true; + for (NSString *key in keys) + { + if (![key isKindOfClass:StringClass]) + { + TGLegacyLog(@"Warning: optionsHash: key is not a string"); + continue; + } + + if (first) + { + [string appendString:@","]; + first = false; + } + + NSObject *value = [options objectForKey:key]; + if ([value respondsToSelector:@selector(stringValue)]) + [string appendFormat:@"%@=%@", key, value]; + } + + return string; +} + +- (NSString *)genericStringForParametrizedPath:(NSString *)path +{ + if (path == nil) + return @""; + + int length = (int)path.length; + unichar newPath[path.length]; + int newLength = 0; + + SEL sel = @selector(characterAtIndex:); + unichar (*characterAtIndexImp)(id, SEL, NSUInteger) = (unichar (*)(id, SEL, NSUInteger))[path methodForSelector:sel]; + + bool skipCharacters = false; + bool skippedCharacters = false; + + for (int i = 0; i < length; i++) + { + unichar c = characterAtIndexImp(path, sel, i); + if (c == '(') + { + skipCharacters = true; + skippedCharacters = true; + newPath[newLength++] = '@'; + } + else if (c == ')') + { + skipCharacters = false; + } + else if (!skipCharacters) + { + newPath[newLength++] = c; + } + } + + if (!skippedCharacters) + return path; + + NSString *genericPath = [[NSString alloc] initWithCharacters:newPath length:newLength]; + return genericPath; +} + +- (void)_requestGeneric:(bool)joinOnly inCurrentQueue:(bool)inCurrentQueue path:(NSString *)path options:(NSDictionary *)options flags:(int)flags watcher:(id)watcher +{ + ASHandle *actionHandle = watcher.actionHandle; + dispatch_block_t requestBlock = ^ + { + if (![actionHandle hasDelegate]) + { + TGLegacyLog(@"Error: %s:%d: actionHandle.delegate is nil", __PRETTY_FUNCTION__, __LINE__); + return; + } + + NSMutableDictionary *activeRequests = _activeRequests; + NSMutableDictionary *cancelTimers = _cancelRequestTimers; + + NSString *genericPath = [self genericStringForParametrizedPath:path]; + + NSMutableDictionary *requestInfo = nil; + + NSMutableDictionary *cancelRequestInfo = [cancelTimers objectForKey:path]; + if (cancelRequestInfo != nil) + { + STimer *timer = [cancelRequestInfo objectForKey:@"timer"]; + [timer invalidate]; + timer = nil; + requestInfo = [cancelRequestInfo objectForKey:@"requestInfo"]; + [activeRequests setObject:requestInfo forKey:path]; + [cancelTimers removeObjectForKey:path]; + TGLegacyLog(@"Resuming request to \"%@\"", path); + } + + if (requestInfo == nil) + requestInfo = [activeRequests objectForKey:path]; + + if (joinOnly && requestInfo == nil) + return; + + if (requestInfo == nil) + { + ASActor *requestBuilder = [ASActor requestBuilderForGenericPath:genericPath path:path]; + if (requestBuilder != nil) + { + NSMutableArray *watchers = [[NSMutableArray alloc] initWithObjects:actionHandle, nil]; + + requestInfo = [[NSMutableDictionary alloc] initWithObjectsAndKeys: + requestBuilder, @"requestBuilder", + watchers, @"watchers", + nil]; + + [activeRequests setObject:requestInfo forKey:path]; + + [requestBuilder prepare:options]; + + bool executeNow = true; + if (requestBuilder.requestQueueName != nil) + { + NSMutableArray *requestQueue = [_requestQueues objectForKey:requestBuilder.requestQueueName]; + if (requestQueue == nil) + { + requestQueue = [[NSMutableArray alloc] initWithObjects:requestBuilder, nil]; + [_requestQueues setObject:requestQueue forKey:requestBuilder.requestQueueName]; + } + else + { + [requestQueue addObject:requestBuilder]; + if ([requestQueue count] > 1) + { + executeNow = false; + TGLegacyLog(@"Adding request %@ to request queue \"%@\"", requestBuilder, requestBuilder.requestQueueName); + + if (flags & TGActorRequestChangePriority) + { + if (requestQueue.count > 2) + { + [requestQueue removeLastObject]; + [requestQueue insertObject:requestBuilder atIndex:1]; + + TGLegacyLog(@"(Inserted actor with high priority (next in queue)"); + } + } + } + } + } + + if (executeNow) + [requestBuilder execute:options]; + else + requestBuilder.storedOptions = options; + } + else + { + TGLegacyLog(@"Error: request builder not found for \"%@\"", path); + } + } + else + { + NSMutableArray *watchers = [requestInfo objectForKey:@"watchers"]; + if (![watchers containsObject:actionHandle]) + { + TGLegacyLog(@"Joining watcher to the watchers of \"%@\"", path); + [watchers addObject:actionHandle]; + } + else + { + TGLegacyLog(@"Continue to watch for actor \"%@\"", path); + } + + ASActor *actor = [requestInfo objectForKey:@"requestBuilder"]; + if (actor.requestQueueName == nil) + [actor watcherJoined:actionHandle options:options waitingInActorQueue:false]; + else + { + NSMutableArray *requestQueue = [_requestQueues objectForKey:actor.requestQueueName]; + if (requestQueue == nil || requestQueue.count == 0) + { + [actor watcherJoined:actionHandle options:options waitingInActorQueue:false]; + } + else + { + [actor watcherJoined:actionHandle options:options waitingInActorQueue:[requestQueue objectAtIndex:0] != actor]; + + if (flags & TGActorRequestChangePriority) + [self changeActorPriority:path]; + } + } + } + }; + + if (inCurrentQueue) + requestBlock(); + else + [self dispatchOnStageQueue:requestBlock]; +} + +- (void)changeActorPriority:(NSString *)path +{ + [ActionStageInstance() dispatchOnStageQueue:^ + { + NSDictionary *requestInfo = [_activeRequests objectForKey:path]; + if (requestInfo != nil) + { + ASActor *actor = [requestInfo objectForKey:@"requestBuilder"]; + if (actor.requestQueueName != nil) + { + NSMutableArray *requestQueue = [_requestQueues objectForKey:actor.requestQueueName]; + if (requestQueue != nil && requestQueue.count != 0) + { + NSUInteger index = [requestQueue indexOfObject:actor]; + if (index != NSNotFound && index != 0 && index != 1) + { + [requestQueue removeObjectAtIndex:index]; + [requestQueue insertObject:actor atIndex:1]; + + TGLegacyLog(@"Changed actor %@ priority (next in %@)", path, actor.requestQueueName); + } + } + } + } + }]; +} + +- (NSArray *)rejoinActionsWithGenericPathNow:(NSString *)genericPath prefix:(NSString *)prefix watcher:(id)watcher +{ + NSMutableDictionary *activeRequests = _activeRequests; + NSMutableDictionary *cancelTimers = _cancelRequestTimers; + + NSMutableArray *rejoinPaths = [[NSMutableArray alloc] init]; + + for (NSString *path in activeRequests.allKeys) + { + if ([path isEqualToString:genericPath] || ([[self genericStringForParametrizedPath:path] isEqualToString:genericPath] && (prefix.length == 0 || [path hasPrefix:prefix]))) + { + [rejoinPaths addObject:path]; + } + } + + for (NSString *path in cancelTimers.allKeys) + { + if ([[self genericStringForParametrizedPath:path] isEqualToString:genericPath] && [path hasPrefix:prefix]) + { + [rejoinPaths addObject:path]; + } + } + + for (NSString *path in rejoinPaths) + { + [self _requestGeneric:true inCurrentQueue:true path:path options:nil flags:0 watcher:watcher]; + } + + return rejoinPaths; +} + +- (bool)isExecutingActorsWithGenericPath:(NSString *)genericPath +{ + if (![self isCurrentQueueStageQueue]) + { + TGLegacyLog(@"%s should be called from graph queue", __PRETTY_FUNCTION__); + + return nil; + } + + __block bool result = false; + + [_activeRequests enumerateKeysAndObjectsUsingBlock:^(__unused NSString *path, NSDictionary *actionInfo, BOOL *stop) + { + ASActor *actor = [actionInfo objectForKey:@"requestBuilder"]; + + if ([genericPath isEqualToString:[actor.class genericPath]]) + { + result = true; + if (stop != NULL) + *stop = true; + } + }]; + + if (!result) + { + [_cancelRequestTimers enumerateKeysAndObjectsUsingBlock:^(__unused NSString *path, NSDictionary *actionInfo, BOOL *stop) + { + ASActor *actor = [actionInfo objectForKey:@"requestBuilder"]; + + if ([genericPath isEqualToString:[actor.class genericPath]]) + { + result = true; + if (stop != NULL) + *stop = true; + } + }]; + } + + return result; +} + +- (bool)isExecutingActorsWithPathPrefix:(NSString *)pathPrefix +{ + if (![self isCurrentQueueStageQueue]) + { + TGLegacyLog(@"%s should be called from graph queue", __PRETTY_FUNCTION__); + + return nil; + } + + __block bool result = false; + + [_activeRequests enumerateKeysAndObjectsUsingBlock:^(NSString *path, __unused id obj, BOOL *stop) + { + if ([path hasPrefix:pathPrefix]) + { + result = true; + if (stop != NULL) + *stop = true; + } + }]; + + if (!result) + { + [_cancelRequestTimers enumerateKeysAndObjectsUsingBlock:^(NSString *path, __unused id obj, BOOL *stop) + { + if ([path hasPrefix:pathPrefix]) + { + result = true; + if (stop != NULL) + *stop = true; + } + }]; + } + + return result; +} + +- (NSArray *)executingActorsWithPathPrefix:(NSString *)pathPrefix +{ + if (![self isCurrentQueueStageQueue]) + { + TGLegacyLog(@"%s should be called from graph queue", __PRETTY_FUNCTION__); + + return nil; + } + + NSMutableArray *array = [[NSMutableArray alloc] init]; + + [_activeRequests enumerateKeysAndObjectsUsingBlock:^(NSString *path, NSDictionary *actionInfo, __unused BOOL *stop) + { + if ([path hasPrefix:pathPrefix]) + { + ASActor *actor = [actionInfo objectForKey:@"requestBuilder"]; + if (actor != nil) + [array addObject:actor]; + } + }]; + + [_cancelRequestTimers enumerateKeysAndObjectsUsingBlock:^(NSString *path, NSDictionary *actionInfo, __unused BOOL *stop) + { + if ([path hasPrefix:pathPrefix]) + { + ASActor *actor = [actionInfo objectForKey:@"requestBuilder"]; + if (actor != nil) + [array addObject:actor]; + } + }]; + + return array; +} + +- (ASActor *)executingActorWithPath:(NSString *)path +{ + if (![self isCurrentQueueStageQueue]) + { + TGLegacyLog(@"%s should be called from graph queue", __PRETTY_FUNCTION__); + + return nil; + } + + NSMutableDictionary *requestInfo = [_activeRequests objectForKey:path]; + if (requestInfo != nil) + { + ASActor *requestBuilder = [requestInfo objectForKey:@"requestBuilder"]; + return requestBuilder; + } + + NSMutableDictionary *cancelRequestInfo = [_cancelRequestTimers objectForKey:path]; + if (cancelRequestInfo != nil) + { + ASActor *requestBuilder = [[cancelRequestInfo objectForKey:@"requestInfo"] objectForKey:@"requestBuilder"]; + return requestBuilder; + } + + return nil; +} + +- (void)cancelActorTimeout:(NSString *)path +{ + NSMutableDictionary *cancelRequestInfo = [_cancelRequestTimers objectForKey:path]; + if (cancelRequestInfo != nil) + { + STimer *timer = [cancelRequestInfo objectForKey:@"timer"]; + [timer fireAndInvalidate]; + timer = nil; + + return; + } +} + +- (void)requestActor:(NSString *)action options:(NSDictionary *)options watcher:(id)watcher +{ + [self _requestGeneric:false inCurrentQueue:false path:action options:options flags:0 watcher:watcher]; +} + +- (void)requestActor:(NSString *)path options:(NSDictionary *)options flags:(int)flags watcher:(id)watcher +{ + [self _requestGeneric:false inCurrentQueue:false path:path options:options flags:flags watcher:watcher]; +} + +- (void)watchForPath:(NSString *)path watcher:(id)watcher +{ + ASHandle *actionHandle = watcher.actionHandle; + if (actionHandle == nil) + { + TGLegacyLog(@"***** Warning: actionHandle is nil in %s:%d", __PRETTY_FUNCTION__, __LINE__); + return; + } + + [self dispatchOnStageQueue:^ + { + NSMutableArray *pathWatchers = [_liveNodeWatchers objectForKey:path]; + if (pathWatchers == nil) + { + pathWatchers = [[NSMutableArray alloc] init]; + [_liveNodeWatchers setObject:pathWatchers forKey:path]; + } + + if (![pathWatchers containsObject:actionHandle]) + [pathWatchers addObject:actionHandle]; + }]; +} + +- (void)watchForPaths:(NSArray *)paths watcher:(id)watcher +{ + ASHandle *actionHandle = watcher.actionHandle; + if (actionHandle == nil) + { + TGLegacyLog(@"***** Warning: actionHandle is nil in %s:%d", __PRETTY_FUNCTION__, __LINE__); + return; + } + + [self dispatchOnStageQueue:^ + { + for (NSString *path in paths) + { + NSMutableArray *pathWatchers = [_liveNodeWatchers objectForKey:path]; + if (pathWatchers == nil) + { + pathWatchers = [[NSMutableArray alloc] init]; + [_liveNodeWatchers setObject:pathWatchers forKey:path]; + } + + if (![pathWatchers containsObject:actionHandle]) + [pathWatchers addObject:actionHandle]; + } + }]; +} + +- (void)watchForGenericPath:(NSString *)path watcher:(id)watcher +{ + ASHandle *actionHandle = watcher.actionHandle; + if (actionHandle == nil) + { + TGLegacyLog(@"***** Warning: actionHandle is nil in %s:%d", __PRETTY_FUNCTION__, __LINE__); + return; + } + + [self dispatchOnStageQueue:^ + { + NSString *genericPath = [self genericStringForParametrizedPath:path]; + NSMutableArray *pathWatchers = [_liveNodeWatchers objectForKey:genericPath]; + if (pathWatchers == nil) + { + pathWatchers = [[NSMutableArray alloc] init]; + [_liveNodeWatchers setObject:pathWatchers forKey:genericPath]; + } + + [pathWatchers addObject:actionHandle]; + }]; +} + +- (void)watchForMessagesToWatchersAtGenericPath:(NSString *)genericPath watcher:(id)watcher +{ + ASHandle *actionHandle = watcher.actionHandle; + if (actionHandle == nil) + { + TGLegacyLog(@"***** Warning: actionHandle is nil in %s:%d", __PRETTY_FUNCTION__, __LINE__); + return; + } + + [self dispatchOnStageQueue:^ + { + NSMutableArray *pathWatchers = [_actorMessagesWatchers objectForKey:genericPath]; + if (pathWatchers == nil) + { + pathWatchers = [[NSMutableArray alloc] init]; + [_actorMessagesWatchers setObject:pathWatchers forKey:genericPath]; + } + + [pathWatchers addObject:actionHandle]; + }]; +} + +- (void)removeRequestFromQueueAndProceedIfFirst:(NSString *)name fromRequestBuilder:(ASActor *)requestBuilder +{ + NSMutableArray *requestQueue = [_requestQueues objectForKey:requestBuilder.requestQueueName == nil ? name : requestBuilder.requestQueueName]; + if (requestQueue == nil) + TGLegacyLog(@"Warning: requestQueue is nil"); + else + { + if (requestQueue.count == 0) + { + TGLegacyLog(@"***** Warning ***** request queue \"%@\" is empty.", requestBuilder.requestQueueName); + } + else + { + if ([requestQueue objectAtIndex:0] == requestBuilder) + { + [requestQueue removeObjectAtIndex:0]; + + if (requestQueue.count != 0) + { + ASActor *nextRequest = nil; + id nextRequestOptions = nil; + + nextRequest = [requestQueue objectAtIndex:0]; + nextRequestOptions = nextRequest.storedOptions; + nextRequest.storedOptions = nil; + + if (nextRequest != nil && !nextRequest.cancelled) + [nextRequest execute:nextRequestOptions]; + } + else + { + //TGLegacyLog(@"Request queue %@ finished", requestBuilder.requestQueueName); + [_requestQueues removeObjectForKey:requestBuilder.requestQueueName]; + } + } + else + { + if ([requestQueue containsObject:requestBuilder]) + { + [requestQueue removeObject:requestBuilder]; + } + else + { + TGLegacyLog(@"***** Warning ***** request queue \"%@\" doesn't contain request to %@", requestBuilder.requestQueueName, requestBuilder.path); + } + } + } + } +} + +- (void)removeWatcher:(id)watcher +{ + [self removeWatcherByHandle:watcher.actionHandle]; +} + +- (void)removeWatcherByHandle:(ASHandle *)actionHandle +{ + ASHandle *watcherGraphHandle = actionHandle; + if (watcherGraphHandle == nil) + { + TGLegacyLog(@"***** Warning: graph handle is nil in removeWatcher"); + return; + } + + bool alreadyExecuting = false; + OSSpinLockLock(&removeWatcherRequestsLock); + if (!_removeWatcherRequests.empty()) + alreadyExecuting = true; + _removeWatcherRequests.push_back(watcherGraphHandle); + OSSpinLockUnlock(&removeWatcherRequestsLock); + + if (alreadyExecuting && ![self isCurrentQueueStageQueue]) + return; + + [self dispatchOnHighPriorityQueue:^ + { + std::vector removeWatchers; + + OSSpinLockLock(&removeWatcherRequestsLock); + removeWatchers.insert(removeWatchers.begin(), _removeWatcherRequests.begin(), _removeWatcherRequests.end()); + _removeWatcherRequests.clear(); + OSSpinLockUnlock(&removeWatcherRequestsLock); + + for (std::vector::iterator it = removeWatchers.begin(); it != removeWatchers.end(); it++) + { + ASHandle *actionHandle = *it; + + for (id key in [_activeRequests allKeys]) + { + NSMutableDictionary *requestInfo = [_activeRequests objectForKey:key]; + NSMutableArray *watchers = [requestInfo objectForKey:@"watchers"]; + [watchers removeObject:actionHandle]; + + if (watchers.count == 0) + { + [self scheduleCancelRequest:(NSString *)key]; + } + } + + { + NSMutableArray *keysToRemove = nil; + for (NSString *key in [_liveNodeWatchers allKeys]) + { + NSMutableArray *watchers = [_liveNodeWatchers objectForKey:key]; + [watchers removeObject:actionHandle]; + + if (watchers.count == 0) + { + if (keysToRemove == nil) + keysToRemove = [[NSMutableArray alloc] init]; + [keysToRemove addObject:key]; + } + } + if (keysToRemove != nil) + [_liveNodeWatchers removeObjectsForKeys:keysToRemove]; + } + + { + NSMutableArray *keysToRemove = nil; + for (NSString *key in [_actorMessagesWatchers allKeys]) + { + NSMutableArray *watchers = [_actorMessagesWatchers objectForKey:key]; + [watchers removeObject:actionHandle]; + + if (watchers.count == 0) + { + if (keysToRemove == nil) + keysToRemove = [[NSMutableArray alloc] init]; + [keysToRemove addObject:key]; + } + } + if (keysToRemove != nil) + [_actorMessagesWatchers removeObjectsForKeys:keysToRemove]; + } + } + }]; +} + +- (void)removeAllWatchersFromPath:(NSString *)path +{ + [self dispatchOnHighPriorityQueue:^ + { + { + NSMutableDictionary *requestInfo = [_activeRequests objectForKey:path]; + if (requestInfo != nil) + { + NSMutableArray *watchers = [requestInfo objectForKey:@"watchers"]; + [watchers removeAllObjects]; + [self scheduleCancelRequest:(NSString *)path]; + } + } + }]; +} + +- (void)removeWatcher:(id)watcher fromPath:(NSString *)path +{ + ASHandle *actionHandle = watcher.actionHandle; + [self removeWatcherByHandle:actionHandle fromPath:path]; +} + +- (void)removeWatcherByHandle:(ASHandle *)watcherGraphHandle fromPath:(NSString *)watcherPath +{ + if (watcherGraphHandle == nil) + { + TGLegacyLog(@"***** Warning: graph handle is nil in removeWatcher:fromPath"); + return; + } + + bool alreadyExecuting = false; + OSSpinLockLock(&removeWatcherFromPathRequestsLock); + if (!_removeWatcherFromPathRequests.empty()) + alreadyExecuting = true; + _removeWatcherFromPathRequests.push_back(std::pair(watcherGraphHandle, watcherPath)); + OSSpinLockUnlock(&removeWatcherFromPathRequestsLock); + + if (alreadyExecuting && ![self isCurrentQueueStageQueue]) + return; + + [self dispatchOnHighPriorityQueue:^ + { + std::vector > removeWatchersFromPath; + + OSSpinLockLock(&removeWatcherFromPathRequestsLock); + removeWatchersFromPath.insert(removeWatchersFromPath.begin(), _removeWatcherFromPathRequests.begin(), _removeWatcherFromPathRequests.end()); + _removeWatcherFromPathRequests.clear(); + OSSpinLockUnlock(&removeWatcherFromPathRequestsLock); + + if (removeWatchersFromPath.size() > 1) + { + TGLegacyLog(@"Cancelled %ld requests at once", removeWatchersFromPath.size()); + } + + for (std::vector >::iterator it = removeWatchersFromPath.begin(); it != removeWatchersFromPath.end(); it++) + { + ASHandle *actionHandle = it->first; + NSString *path = it->second; + if (path == nil) + continue; + + { + NSMutableDictionary *requestInfo = [_activeRequests objectForKey:path]; + if (requestInfo != nil) + { + NSMutableArray *watchers = [requestInfo objectForKey:@"watchers"]; + if ([watchers containsObject:actionHandle]) + { + [watchers removeObject:actionHandle]; + } + if (watchers.count == 0) + { + [self scheduleCancelRequest:(NSString *)path]; + } + } + } + { + NSMutableArray *watchers = [_liveNodeWatchers objectForKey:path]; + if ([watchers containsObject:actionHandle]) + { + [watchers removeObject:actionHandle]; + } + if (watchers.count == 0) + { + [_liveNodeWatchers removeObjectForKey:path]; + } + } + { + NSMutableArray *watchers = [_actorMessagesWatchers objectForKey:path]; + if ([watchers containsObject:actionHandle]) + { + [watchers removeObject:actionHandle]; + } + if (watchers.count == 0) + { + [_actorMessagesWatchers removeObjectForKey:path]; + } + } + } + }]; +} + +- (bool)requestActorStateNow:(NSString *)path +{ + if ([_activeRequests objectForKey:path] != nil) + return true; + return false; +} + +- (void)dispatchResource:(NSString *)path resource:(id)resource +{ + [self dispatchResource:path resource:resource arguments:nil]; +} + +- (void)dispatchResource:(NSString *)path resource:(id)resource arguments:(id)arguments +{ + [self dispatchOnStageQueue:^ + { + NSString *genericPath = [self genericStringForParametrizedPath:path]; + { + NSArray *watchers = [[_liveNodeWatchers objectForKey:path] copy]; + if (watchers != nil) + { + for (ASHandle *handle in watchers) + { + id watcher = handle.delegate; + if (watcher != nil) + { + if ([watcher respondsToSelector:@selector(actionStageResourceDispatched:resource:arguments:)]) + [watcher actionStageResourceDispatched:path resource:resource arguments:arguments]; + + if (handle.releaseOnMainThread) + dispatch_async(dispatch_get_main_queue(), ^ { [watcher class]; }); + watcher = nil; + } + } + } + } + if (![genericPath isEqualToString:path]) + { + NSArray *watchers = [_liveNodeWatchers objectForKey:genericPath]; + if (watchers != nil) + { + for (ASHandle *handle in watchers) + { + id watcher = handle.delegate; + if (watcher != nil) + { + if ([watcher respondsToSelector:@selector(actionStageResourceDispatched:resource:arguments:)]) + [watcher actionStageResourceDispatched:path resource:resource arguments:arguments]; + + if (handle.releaseOnMainThread) + dispatch_async(dispatch_get_main_queue(), ^ { [watcher class]; }); + watcher = nil; + } + } + } + } + }]; +} + +- (void)actionCompleted:(NSString *)action result:(id)result +{ + [self dispatchOnStageQueue:^ + { + NSMutableDictionary *requestInfo = [_activeRequests objectForKey:action]; + if (requestInfo != nil) + { + ASActor *requestBuilder = [requestInfo objectForKey:@"requestBuilder"]; + + NSMutableArray *actionWatchers = [requestInfo objectForKey:@"watchers"]; + [_activeRequests removeObjectForKey:action]; + for (ASHandle *handle in actionWatchers) + { + id watcher = handle.delegate; + if (watcher != nil) + { + if ([watcher respondsToSelector:@selector(actorCompleted:path:result:)]) + [watcher actorCompleted:ASStatusSuccess path:action result:result]; + + if (handle.releaseOnMainThread) + dispatch_async(dispatch_get_main_queue(), ^ { [watcher class]; }); + watcher = nil; + } + } + [actionWatchers removeAllObjects]; + + if (requestBuilder == nil) + TGLegacyLog(@"***** Warning ***** requestBuilder is nil"); + else if (requestBuilder.requestQueueName != nil) + { + [self removeRequestFromQueueAndProceedIfFirst:requestBuilder.requestQueueName fromRequestBuilder:requestBuilder]; + } + } + }]; +} + +- (void)dispatchMessageToWatchers:(NSString *)path messageType:(NSString *)messageType message:(id)message +{ + [self dispatchOnStageQueue:^ + { + NSMutableDictionary *requestInfo = [_activeRequests objectForKey:path]; + if (requestInfo != nil) + { + NSArray *actionWatchersCopy = [[requestInfo objectForKey:@"watchers"] copy]; + for (ASHandle *handle in actionWatchersCopy) + { + [handle receiveActorMessage:path messageType:messageType message:message]; + } + } + + if (_actorMessagesWatchers.count != 0) + { + NSString *genericPath = [self genericStringForParametrizedPath:path]; + NSArray *messagesWatchersCopy = [[_actorMessagesWatchers objectForKey:genericPath] copy]; + + if (messagesWatchersCopy != nil) + { + for (ASHandle *handle in messagesWatchersCopy) + { + [handle receiveActorMessage:path messageType:messageType message:message]; + } + } + } + }]; +} + +- (void)actionFailed:(NSString *)action reason:(int)reason +{ + [self dispatchOnStageQueue:^ + { + NSMutableDictionary *requestInfo = [_activeRequests objectForKey:action]; + if (requestInfo != nil) + { + ASActor *requestBuilder = [requestInfo objectForKey:@"requestBuilder"]; + + NSMutableArray *actionWatchers = [requestInfo objectForKey:@"watchers"]; + [_activeRequests removeObjectForKey:action]; + for (ASHandle *handle in actionWatchers) + { + id watcher = handle.delegate; + if (watcher != nil) + { + if ([watcher respondsToSelector:@selector(actorCompleted:path:result:)]) + [watcher actorCompleted:reason path:action result:nil]; + + if (handle.releaseOnMainThread) + dispatch_async(dispatch_get_main_queue(), ^ { [watcher class]; }); + watcher = nil; + } + } + [actionWatchers removeAllObjects]; + + if (requestBuilder == nil) + TGLegacyLog(@"***** Warning ***** requestBuilder is nil"); + else if (requestBuilder.requestQueueName != nil) + { + [self removeRequestFromQueueAndProceedIfFirst:requestBuilder.requestQueueName fromRequestBuilder:requestBuilder]; + } + } + }]; +} + +- (void)nodeRetrieved:(NSString *)path node:(SGraphNode *)node +{ + [self actionCompleted:path result:node]; +} + +- (void)nodeRetrieveProgress:(NSString *)path progress:(float)progress +{ + [self dispatchOnStageQueue:^ + { + NSMutableDictionary *requestInfo = [_activeRequests objectForKey:path]; + if (requestInfo == nil) + requestInfo = [_activeRequests objectForKey:path]; + + if (requestInfo != nil) + { + NSMutableArray *watchers = [requestInfo objectForKey:@"watchers"]; + for (ASHandle *handle in watchers) + { + id watcher = handle.delegate; + if (watcher != nil) + { + if ([watcher respondsToSelector:@selector(actorReportedProgress:progress:)]) + [watcher actorReportedProgress:path progress:progress]; + + if (handle.releaseOnMainThread) + dispatch_async(dispatch_get_main_queue(), ^ { [watcher class]; }); + watcher = nil; + } + } + } + }]; +} + +- (void)nodeRetrieveFailed:(NSString *)path +{ + [self actionFailed:path reason:-1]; +} + +- (void)scheduleCancelRequest:(NSString *)path +{ + NSMutableDictionary *activeRequests = _activeRequests; + NSMutableDictionary *cancelTimers = _cancelRequestTimers; + + NSMutableDictionary *requestInfo = [activeRequests objectForKey:path]; + NSMutableDictionary *cancelRequestInfo = [cancelTimers objectForKey:path]; + if (requestInfo != nil && cancelRequestInfo == nil) + { + ASActor *requestBuilder = [requestInfo objectForKey:@"requestBuilder"]; + NSTimeInterval cancelTimeout = requestBuilder.cancelTimeout; + + if (cancelTimeout <= DBL_EPSILON) + { + [activeRequests removeObjectForKey:path]; + + [requestBuilder cancel]; + TGLegacyLog(@"Cancelled request to \"%@\"", path); + if (requestBuilder.requestQueueName != nil) + [self removeRequestFromQueueAndProceedIfFirst:requestBuilder.requestQueueName fromRequestBuilder:requestBuilder]; + } + else + { + TGLegacyLog(@"Will cancel request to \"%@\" in %f s", path, cancelTimeout); + NSDictionary *cancelDict = [NSDictionary dictionaryWithObjectsAndKeys:path, @"path", [NSNumber numberWithInt:0], @"type", nil]; + STimer *timer = [[STimer alloc] initWithTimeout:cancelTimeout repeat:false completion:^ + { + [self performCancelRequest:cancelDict]; + } nativeQueue:[ActionStageInstance() globalStageDispatchQueue]]; + + cancelRequestInfo = [[NSMutableDictionary alloc] initWithObjectsAndKeys:requestInfo, @"requestInfo", nil]; + [cancelRequestInfo setObject:timer forKey:@"timer"]; + [cancelTimers setObject:cancelRequestInfo forKey:path]; + [activeRequests removeObjectForKey:path]; + + [timer start]; + } + } + else if (cancelRequestInfo == nil) + { + TGLegacyLog(@"Warning: cannot cancel request to \"%@\": no active request found", path); + } +} + +- (void)performCancelRequest:(NSDictionary *)cancelDict +{ + NSString *path = [cancelDict objectForKey:@"path"]; + + [self dispatchOnStageQueue:^ + { + NSMutableDictionary *cancelTimers = _cancelRequestTimers; + + NSMutableDictionary *cancelRequestInfo = [cancelTimers objectForKey:path]; + if (cancelRequestInfo == nil) + { + TGLegacyLog(@"Warning: cancelNodeRequestTimerEvent: \"%@\": no cancel info found", path); + return; + } + NSDictionary *requestInfo = [cancelRequestInfo objectForKey:@"requestInfo"]; + ASActor *requestBuilder = [requestInfo objectForKey:@"requestBuilder"]; + if (requestBuilder == nil) + { + TGLegacyLog(@"Warning: active request builder for \"%@\" not fond, cannot cancel request", path); + } + else + { + [requestBuilder cancel]; + TGLegacyLog(@"Cancelled request to \"%@\"", path); + if (requestBuilder.requestQueueName != nil) + [self removeRequestFromQueueAndProceedIfFirst:requestBuilder.requestQueueName fromRequestBuilder:requestBuilder]; + } + [cancelTimers removeObjectForKey:path]; + }]; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/CBAssetDownloadManager.h b/submodules/LegacyComponents/LegacyComponents/CBAssetDownloadManager.h new file mode 100755 index 0000000000..5b91844ea5 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/CBAssetDownloadManager.h @@ -0,0 +1,66 @@ +// +// Created by Tikhonenko Pavel on 23/05/2014. +// Copyright (c) 2014 Coub. All rights reserved. +// + +#import +#import "CBCoubAsset.h" + +@protocol CBDownloadOperationDelegate; + +typedef NS_ENUM(NSInteger, CBDownloadProcessType) +{ + CBDownloadProcessTypeCoub, + CBDownloadProcessTypeChunks, +}; + +@interface CBAssetDownloadManager : NSObject + +- (void)downloadCoubWithoutChunks:(id)coub + tag:(NSInteger)tag + withDelegate:(id)delegate + downloadSucces:(void (^)(id coub, NSInteger tag))success downloadFailure:(void (^)(id coub, NSInteger tag, NSError *error))failure; + +- (void)downloadCoub:(id)coub + tag:(NSInteger)tag + withDelegate:(id)delegate + withChunks:(BOOL)withChunks + downloadSucces:(void (^)(id coub, NSInteger tag))success downloadFailure:(void (^)(id coub, NSInteger tag, NSError *error))failure; + +#pragma mark - +#pragma mark Downloading video and first chunk + +//- (void)downloadCoub:(id)coub +// tag:(NSInteger)tag +// withDelegate:(id)delegate +// downloadSucces:(void (^)(id coub, NSInteger tag))success downloadFailure:(void (^)(id coub, NSInteger tag, NSError *error))failure; + +- (void)downloadNextCoub:(id)coub + tag:(NSInteger)tag + downloadSucces:(void (^)(id coub, NSInteger tag))success downloadFailure:(void (^)(id coub, NSInteger tag, NSError *error))failure; + +- (void)cancelDownloadingForCoub:(id)coub; + +#pragma mark - +#pragma mark Downloading remaining chunks + +- (void)downloadChunkWithCoub:(id)coub + tag:(NSInteger)tag + chunkIdx:(NSInteger)chunkIdx + downloadSucces:(void (^)(id coub, NSInteger tag))success + downloadFailure:(void (^)(id coub, NSInteger tag, NSError *error))failure; + +- (void)downloadNextChunkWithCoub:(id)coub + downloadSucces:(void (^)(id coub, NSInteger tag))success + downloadFailure:(void (^)(id coub, NSInteger tag, NSError *error))failure; +//- (void)downloadRemainingChunksWithCoub:(id)coub +// tag:(NSInteger)tag +// downloadChunkSucces:(void (^)(id coub, NSInteger tag, NSInteger chunkIdx))chunkDownloaded +// downloadSucces:(void (^)(id coub, NSInteger tag))success +// downloadFailure:(void (^)(id coub, NSInteger tag, NSError *error))failure; + +- (void)stopAllDownloads; + ++ (instancetype)sharedManager; + +@end \ No newline at end of file diff --git a/submodules/LegacyComponents/LegacyComponents/CBAssetDownloadManager.m b/submodules/LegacyComponents/LegacyComponents/CBAssetDownloadManager.m new file mode 100755 index 0000000000..36792d0480 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/CBAssetDownloadManager.m @@ -0,0 +1,290 @@ +// +// Created by Tikhonenko Pavel on 23/05/2014. +// Copyright (c) 2014 Coub. All rights reserved. +// + +#import "CBAssetDownloadManager.h" +#import "CBCoubDownloadOperation.h" +#import "CBChunkDownloadOperation.h" +#import "CBDownloadOperation.h" +#import "CBDownloadOperationDelegate.h" + + +@interface CBAssetDownloadManager () + +@property (nonatomic, strong) CBCoubDownloadOperation *currentCoubDownloadingOperation; +@property (nonatomic, strong) CBCoubDownloadOperation *pausedCoubDownloadingOperation; +@property (nonatomic, strong) CBCoubDownloadOperation *nextCoubDownloadingOperation; +@property (nonatomic, strong) CBChunkDownloadOperation *currentChunksDownloadingOperation; + +- (void)resumeCoubDownloading; +- (void)destroyDownloadOperation:(id)downloadOperation; + +- (id)operationWithType:(CBDownloadProcessType)type + coub:(id)coub tag:(NSInteger)tag + downloadSucces:(void (^)(id coub, NSInteger tag))success + downloadFailure:(void (^)(id coub, NSInteger tag, NSError *error))failure; +@end + +@implementation CBAssetDownloadManager + +- (id)init +{ + self = [super init]; + + if(self) + { + } + + return self; +} + +- (void)downloadCoubWithoutChunks:(id)coub + tag:(NSInteger)tag + withDelegate:(id)delegate + downloadSucces:(void (^)(id coub, NSInteger tag))success downloadFailure:(void (^)(id coub, NSInteger tag, NSError *error))failure +{ + if(_currentChunksDownloadingOperation) + { + //_pausedCoubDownloadingOperation = _currentChunksDownloadingOperation; + [self destroyDownloadOperation:_currentChunksDownloadingOperation]; + _currentChunksDownloadingOperation = nil; + } + + [self downloadCoub:coub tag:tag withDelegate:delegate queuePriority:NSOperationQueuePriorityNormal withChunks:NO downloadSucces:success downloadFailure:failure]; +} + + +#pragma mark - +#pragma mark Downloading video and first chunk + +- (void)downloadCoub:(id)coub + tag:(NSInteger)tag + withDelegate:(id)delegate + withChunks:(BOOL)withChunks + downloadSucces:(void (^)(id coub, NSInteger tag))success downloadFailure:(void (^)(id coub, NSInteger tag, NSError *error))failure +{ + if(_currentChunksDownloadingOperation) + { + //_pausedCoubDownloadingOperation = _currentChunksDownloadingOperation; + [self destroyDownloadOperation:_currentChunksDownloadingOperation]; + _currentChunksDownloadingOperation = nil; + } + + [self downloadCoub:coub tag:tag withDelegate:delegate queuePriority:NSOperationQueuePriorityNormal withChunks:withChunks downloadSucces:success downloadFailure:failure]; +} + +- (void)downloadCoub:(id)coub + tag:(NSInteger)tag + withDelegate:(id)delegate + queuePriority:(NSOperationQueuePriority)queuePriority + withChunks:(BOOL)withChunks + downloadSucces:(void (^)(id coub, NSInteger tag))success downloadFailure:(void (^)(id coub, NSInteger tag, NSError *error))failure +{ + if(_currentCoubDownloadingOperation && _currentCoubDownloadingOperation.coub == coub && !_currentChunksDownloadingOperation.comleted) + { + _currentCoubDownloadingOperation.operationViewDelegate = delegate; + [_currentCoubDownloadingOperation setClientSuccess:success]; + [_currentCoubDownloadingOperation setClientFailure:failure]; + return; + } + + if(_nextCoubDownloadingOperation && _nextCoubDownloadingOperation.coub == coub && !_nextCoubDownloadingOperation.comleted) + { + [self destroyDownloadOperation:_nextCoubDownloadingOperation]; + } + + if(_currentCoubDownloadingOperation) + [self destroyDownloadOperation:_currentCoubDownloadingOperation]; + + id downloadOperation = [self operationWithType:CBDownloadProcessTypeCoub + coub:coub tag:tag downloadSucces:success downloadFailure:failure]; + downloadOperation.queuePriority = queuePriority; + + _currentCoubDownloadingOperation = downloadOperation; + _currentCoubDownloadingOperation.chunkDownloadingNeeded = withChunks; + [_currentCoubDownloadingOperation setOperationViewDelegate:delegate]; + [_currentCoubDownloadingOperation start]; +} + +- (void)downloadNextCoub:(id)coub + tag:(NSInteger)tag + downloadSucces:(void (^)(id coub,NSInteger tag))success downloadFailure:(void (^)(id coub,NSInteger tag, NSError *error))failure +{ + if(_currentChunksDownloadingOperation && _currentChunksDownloadingOperation.coub == coub && !_currentChunksDownloadingOperation.comleted) + { + failure(self, 0, nil); + return; + } + + + if(_nextCoubDownloadingOperation) + [self destroyDownloadOperation:_nextCoubDownloadingOperation]; + + + id downloadOperation = [self operationWithType:CBDownloadProcessTypeCoub + coub:coub tag:tag downloadSucces:success downloadFailure:failure]; + downloadOperation.queuePriority = NSOperationQueuePriorityVeryLow; + + _nextCoubDownloadingOperation = downloadOperation; + [_nextCoubDownloadingOperation setOperationViewDelegate:nil]; + [_nextCoubDownloadingOperation start]; +} + +- (void)cancelDownloadingForCoub:(id)coub +{ + if(_currentCoubDownloadingOperation && _currentCoubDownloadingOperation.coub == coub) + [self destroyDownloadOperation:_currentCoubDownloadingOperation]; +} + +#pragma mark - +#pragma mark Downloading remaining chunks + +- (void)downloadChunkWithCoub:(id)coub + tag:(NSInteger)tag + chunkIdx:(NSInteger)chunkIdx + downloadSucces:(void (^)(id coub, NSInteger tag))success + downloadFailure:(void (^)(id coub, NSInteger tag, NSError *error))failure +{ + [self destroyDownloadOperation:_currentChunksDownloadingOperation]; + + _currentChunksDownloadingOperation = [self operationWithType:CBDownloadProcessTypeChunks coub:coub + tag:tag downloadSucces:success downloadFailure:failure]; + + __weak typeof(self) wSelf = self; + + _currentChunksDownloadingOperation.chunkIdx = chunkIdx; + [_currentChunksDownloadingOperation setCompletionBlock:^(id process, NSError *error) { + [wSelf destroyDownloadOperation:process]; + [wSelf resumeCoubDownloading]; + }]; + + [_currentChunksDownloadingOperation start]; +} + +- (void)downloadNextChunkWithCoub:(id)coub + downloadSucces:(void (^)(id coub, NSInteger tag))success + downloadFailure:(void (^)(id coub, NSInteger tag, NSError *error))failure +{ + NSInteger chunkIdx = 0; + [self downloadChunkWithCoub:coub tag:chunkIdx chunkIdx:chunkIdx downloadSucces:success downloadFailure:failure]; +} + +- (void)downloadRemainingChunksWithCoub:(id)coub tag:(NSInteger)tag + downloadChunkSucces:(void (^)(id coub, NSInteger tag, NSInteger chunkIdx))chunkDownloaded + downloadSucces:(void (^)(id coub,NSInteger tag))success + downloadFailure:(void (^)(id coub, NSInteger tag, NSError *error))failure +{ + [self destroyDownloadOperation:_currentChunksDownloadingOperation]; + + _currentChunksDownloadingOperation = [self operationWithType:CBDownloadProcessTypeChunks coub:coub + tag:tag downloadSucces:success downloadFailure:failure]; + + __weak typeof(self) wSelf = self; + + _currentChunksDownloadingOperation.chunkDownloadedBlock = chunkDownloaded; + [_currentChunksDownloadingOperation setCompletionBlock:^(id process, NSError *error) { + [wSelf destroyDownloadOperation:process]; + [wSelf resumeCoubDownloading]; + }]; + + [_currentChunksDownloadingOperation start]; +} + +- (void)stopAllDownloads +{ + [self destroyCurrentOperations]; + + _pausedCoubDownloadingOperation = nil; +} + +#pragma mark - +#pragma mark private method + +- (void)resumeCoubDownloading +{ + if(_pausedCoubDownloadingOperation) + { + _currentCoubDownloadingOperation = _pausedCoubDownloadingOperation; + [_currentCoubDownloadingOperation start]; + } +} + +- (void)destroyCurrentOperations +{ + [self destroyDownloadOperation:_currentCoubDownloadingOperation]; + + [self destroyDownloadOperation:_currentChunksDownloadingOperation]; + + //_pausedCoubDownloadingOperation = nil; +} + +- (void)destroyDownloadOperation:(id)downloadOperation +{ + if(downloadOperation) + [downloadOperation cancel]; + + if(downloadOperation == _currentCoubDownloadingOperation) + _currentCoubDownloadingOperation = nil; + + if(downloadOperation == _currentChunksDownloadingOperation) + _currentChunksDownloadingOperation = nil; +} + +#pragma mark - +#pragma mark Factory method + +- (id)operationWithType:(CBDownloadProcessType)type + coub:(id)coub tag:(NSInteger)tag + downloadSucces:(void (^)(id, NSInteger tag))success + downloadFailure:(void (^)(id, NSInteger tag, NSError *error))failure +{ + id downloadOperation; + + switch(type) + { + case CBDownloadProcessTypeCoub: + { + downloadOperation = [CBCoubDownloadOperation new]; + break; + } + + case CBDownloadProcessTypeChunks: + { + downloadOperation = [CBChunkDownloadOperation new]; + break; + } + } + + [downloadOperation setTag:tag]; + [downloadOperation setCoub:coub]; + [downloadOperation setClientSuccess:success]; + [downloadOperation setClientFailure:failure]; + + __weak typeof(self) wSelf = self; + + [downloadOperation setCompletionBlock:^(id operation, NSError *error) { + [wSelf destroyDownloadOperation:operation]; + [wSelf resumeCoubDownloading]; + }]; + + return downloadOperation; +} + +#pragma mark - +#pragma mark Singleton implementation + +static CBAssetDownloadManager *instance = nil; + ++ (instancetype)sharedManager +{ + if(instance != nil) return instance; + + static dispatch_once_t onceToken = 0; + dispatch_once(&onceToken, ^{ + instance = [[self alloc] init]; + }); + return instance; +} + +@end \ No newline at end of file diff --git a/submodules/LegacyComponents/LegacyComponents/CBChunkDownloadOperation.h b/submodules/LegacyComponents/LegacyComponents/CBChunkDownloadOperation.h new file mode 100755 index 0000000000..d7439ca0d3 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/CBChunkDownloadOperation.h @@ -0,0 +1,14 @@ +// +// Created by Tikhonenko Pavel on 23/05/2014. +// Copyright (c) 2014 Coub. All rights reserved. +// + +#import "CBGenericDownloadOperation.h" + +@interface CBChunkDownloadOperation : CBGenericDownloadOperation + +@property (nonatomic, assign) NSInteger chunkIdx; + +@property(nonatomic, copy) void (^chunkDownloadedBlock)(id, NSInteger tag, NSInteger chunkIdx); + +@end \ No newline at end of file diff --git a/submodules/LegacyComponents/LegacyComponents/CBChunkDownloadOperation.m b/submodules/LegacyComponents/LegacyComponents/CBChunkDownloadOperation.m new file mode 100755 index 0000000000..467bd77c95 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/CBChunkDownloadOperation.m @@ -0,0 +1,65 @@ +// +// Created by Tikhonenko Pavel on 23/05/2014. +// Copyright (c) 2014 Coub. All rights reserved. +// + +#import "CBChunkDownloadOperation.h" + +#import "LegacyComponentsInternal.h" +#import "LegacyHTTPRequestOperation.h" + +@implementation CBChunkDownloadOperation +{ + +} + +- (void)start +{ + [super start]; + [self downloadChunk]; +} + +- (void)downloadChunk +{ + NSURL *localURL = [self.coub localAudioChunkWithIdx:_chunkIdx]; + + NSURL *remoteURL = [self.coub remoteAudioChunkWithIdx:_chunkIdx+1]; + + NSURLRequest *downloadRequest = [NSURLRequest requestWithURL:remoteURL]; + + self.downloadOperation = [[LegacyComponentsGlobals provider] makeHTTPRequestOperationWithRequest:downloadRequest]; + self.downloadOperation.queuePriority = self.queuePriority; + self.downloadOperation.outputStream = [NSOutputStream outputStreamToFileAtPath:localURL.path append:NO]; + //[self.downloadOperation setShouldExecuteAsBackgroundTaskWithExpirationHandler:nil]; + + __weak typeof(self) wSelf = self; + + [self.downloadOperation setCompletionBlockWithSuccess:^(__unused id operation, __unused id responseObject) { + [wSelf successChunkDownload]; + } failure:^(__unused id operation, __unused NSError *error) { + [wSelf failureDownloadWithError:error]; + }]; + + [self.downloadOperation start]; +} + +- (void)successChunkDownload +{ + //NSLog(@"successChunkDownload"); + + [self successDownload]; +} + +- (instancetype)clone +{ + CBChunkDownloadOperation *clone = [CBChunkDownloadOperation new]; + [clone setTag:self.tag]; + [clone setCoub:self.coub]; + [clone setCompletionBlock:self.completionBlock]; + [clone setClientSuccess:self.clientSuccess]; + [clone setClientFailure:self.clientFailure]; + [clone setChunkDownloadedBlock:self.chunkDownloadedBlock]; + return clone; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/CBConstance.h b/submodules/LegacyComponents/LegacyComponents/CBConstance.h new file mode 100755 index 0000000000..f448703234 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/CBConstance.h @@ -0,0 +1,33 @@ +// +// CBConstance.h +// CoubPlayer +// +// Created by Pavel Tikhonenko on 19/10/14. +// Copyright (c) 2014 Pavel Tikhonenko. All rights reserved. +// + +#import + +typedef void (^CBSuccessBlock)(id result); +//typedef void (^CBSuccessWithUserInfoBlock)(id result, id userInfo); +typedef void (^CBFailureBlock)(NSError *error); + +extern NSString * const kCBServerURL; + +extern NSString * const CBCoubLoopErrorDomain; +enum { + CBCoubLoopErrorUnknown = 1, + CBCoubLoopErrorNoSuchFile, + CBCoubLoopErrorUnsupportedAudioFormat, + CBCoubLoopErrorUnreadableAudioTrack, + CBCoubLoopErrorNoVideoTracks, + CBCoubLoopErrorCanceled, +}; + + +extern NSString *const CBPlayerInterruptionDidBeginNotification; +extern NSString *const CBPlayerInterruptionDidEndNotification; + +@interface CBConstance : NSObject + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/CBConstance.m b/submodules/LegacyComponents/LegacyComponents/CBConstance.m new file mode 100755 index 0000000000..8528fb4e1c --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/CBConstance.m @@ -0,0 +1,21 @@ +// +// CBConstance.m +// CoubPlayer +// +// Created by Pavel Tikhonenko on 19/10/14. +// Copyright (c) 2014 Pavel Tikhonenko. All rights reserved. +// + +#import "CBConstance.h" + +NSString *const kCBServerURL = @"http://coub.com"; + +NSString *const CBCoubLoopErrorDomain = @"com.coub.loop.ErrorDomain"; + +NSString *const CBPlayerInterruptionDidBeginNotification = @"CBPlayerInterruptionDidBeginNotification"; +NSString *const CBPlayerInterruptionDidEndNotification = @"CBPlayerInterruptionDidEndNotification"; + + +@implementation CBConstance + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/CBCoubAsset.h b/submodules/LegacyComponents/LegacyComponents/CBCoubAsset.h new file mode 100755 index 0000000000..bb921e8773 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/CBCoubAsset.h @@ -0,0 +1,25 @@ +// +// Created by Tikhonenko Pavel on 04/04/2014. +// Copyright (c) 2014 Coub. All rights reserved. +// + +#import +#import "CBCoubPlayerContance.h" + +@protocol CBCoubAsset + +@property(nonatomic, readonly) NSString *assetId; //permalink +@property(nonatomic, readonly) NSURL *localVideoFileURL; +@property(nonatomic, readonly) CBCoubAudioType audioType; +@property(nonatomic, readonly) NSURL *externalAudioURL; //may be nil +@property(nonatomic, readonly) NSURL *largeImageURL; + +- (BOOL)failedDownloadChunk; + +- (NSURL *)remoteVideoFileURL; + +- (NSURL *)localAudioChunkWithIdx:(NSInteger)idx; +- (NSURL *)remoteAudioChunkWithIdx:(NSInteger)idx; + + +@end \ No newline at end of file diff --git a/submodules/LegacyComponents/LegacyComponents/CBCoubAudioSource.h b/submodules/LegacyComponents/LegacyComponents/CBCoubAudioSource.h new file mode 100755 index 0000000000..a9161dab91 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/CBCoubAudioSource.h @@ -0,0 +1,21 @@ +// +// CBCoubAudioSource.h +// Coub +// +// Created by Tikhonenko Pavel on 18/11/2013. +// Copyright (c) 2013 Coub. All rights reserved. +// + +#import + +@interface CBCoubAudioSource : NSObject + +@property (nonatomic, strong) NSString *cover; +@property (nonatomic, strong) NSString *title; +@property (nonatomic, strong) NSString *artist; +@property (nonatomic, strong) NSString *itunesURL; +@property (nonatomic, strong) NSString *songName; + ++ (CBCoubAudioSource *)sourceFromData:(NSDictionary *)dict; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/CBCoubAudioSource.m b/submodules/LegacyComponents/LegacyComponents/CBCoubAudioSource.m new file mode 100755 index 0000000000..be04846066 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/CBCoubAudioSource.m @@ -0,0 +1,32 @@ +// +// CBCoubAudioSource.m +// Coub +// +// Created by Tikhonenko Pavel on 18/11/2013. +// Copyright (c) 2013 Coub. All rights reserved. +// + +#import "CBCoubAudioSource.h" + +@implementation CBCoubAudioSource + ++ (CBCoubAudioSource *)sourceFromData:(NSDictionary *)dict +{ + if(!dict.count) return nil; + + + NSDictionary *meta = dict[@"meta"]; + + CBCoubAudioSource *source = [[CBCoubAudioSource alloc] init]; + source.cover = dict[@"image"]; + source.itunesURL = dict[@"url"]; + source.songName = meta[@"title"]; + source.title = source.songName; + source.artist = meta[@"artist"]; + if(source.title == nil || source.itunesURL == nil) + return nil; + + return source; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/CBCoubAuthorVO.h b/submodules/LegacyComponents/LegacyComponents/CBCoubAuthorVO.h new file mode 100755 index 0000000000..2d1ca547ae --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/CBCoubAuthorVO.h @@ -0,0 +1,23 @@ +// +// CBCoubAuthorVO.h +// Coub +// +// Created by Tikhonenko Pavel on 21/11/2013. +// Copyright (c) 2013 Coub. All rights reserved. +// + +#import + +@interface CBCoubAuthorVO : NSObject + +@property (nonatomic, strong) NSString *avatarURL; +@property (nonatomic, strong) NSString *largeAvatarURL; +@property (nonatomic, assign) NSInteger followersCount; +@property (nonatomic, assign) NSInteger viewsCount; +@property (nonatomic, strong) NSString *userId; +@property (nonatomic, strong) NSString *name; +@property (nonatomic, strong) NSString *permalink; + ++ (CBCoubAuthorVO *)coubAuthorWithAttributes:(NSDictionary *)attributes; ++ (CBCoubAuthorVO *)currentUser; +@end diff --git a/submodules/LegacyComponents/LegacyComponents/CBCoubAuthorVO.m b/submodules/LegacyComponents/LegacyComponents/CBCoubAuthorVO.m new file mode 100755 index 0000000000..cd54782c86 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/CBCoubAuthorVO.m @@ -0,0 +1,42 @@ +// +// CBCoubAuthorVO.m +// Coub +// +// Created by Tikhonenko Pavel on 21/11/2013. +// Copyright (c) 2013 Coub. All rights reserved. +// + +#import "CBCoubAuthorVO.h" + +@implementation CBCoubAuthorVO + ++ (CBCoubAuthorVO *)coubAuthorWithAttributes:(NSDictionary *)attributes +{ + CBCoubAuthorVO *author = [CBCoubAuthorVO new]; + author.avatarURL = attributes[@"avatar"]; + author.largeAvatarURL = attributes[@"large_avatar"]; + author.followersCount = [attributes[@"followers_count"] integerValue]; + author.userId = [attributes[@"id"] stringValue]; + author.name = attributes[@"name"]; + author.permalink = attributes[@"permalink"]; + author.viewsCount = [attributes[@"views_count"] integerValue]; + //author.viewsCount; + return author; +} + +//+ (CBCoubAuthorVO *)currentUser +//{ +// CBUserNew *curUser = [CBUserNew currentUser]; +// +// CBCoubAuthorVO *author = [CBCoubAuthorVO new]; +// author.avatarURL = curUser.mediumAvatar; +// author.largeAvatarURL = curUser.largeAvatar; +// author.followersCount = curUser.followerCount; +// author.userId = curUser.coubID; +// author.name = curUser.fullName; +// author.permalink = curUser.permalink; +// author.viewsCount = curUser.viewsCount; +// return author; +//} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/CBCoubDownloadOperation.h b/submodules/LegacyComponents/LegacyComponents/CBCoubDownloadOperation.h new file mode 100755 index 0000000000..6fcc7eb0a5 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/CBCoubDownloadOperation.h @@ -0,0 +1,12 @@ +// +// Created by Tikhonenko Pavel on 23/05/2014. +// Copyright (c) 2014 Coub. All rights reserved. +// + +#import "CBGenericDownloadOperation.h" + +@interface CBCoubDownloadOperation : CBGenericDownloadOperation + +@property (nonatomic, assign) NSInteger neededChunkCount; + +@end \ No newline at end of file diff --git a/submodules/LegacyComponents/LegacyComponents/CBCoubDownloadOperation.m b/submodules/LegacyComponents/LegacyComponents/CBCoubDownloadOperation.m new file mode 100755 index 0000000000..eadca4f671 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/CBCoubDownloadOperation.m @@ -0,0 +1,190 @@ +// +// Created by Tikhonenko Pavel on 23/05/2014. +// Copyright (c) 2014 Coub. All rights reserved. +// + +#import "CBCoubDownloadOperation.h" +#import "CBDownloadOperationDelegate.h" + +#import "LegacyComponentsInternal.h" + +@implementation CBCoubDownloadOperation +{ + BOOL _isVideoDownloaded; + NSInteger _currentChunkIdx; +} + +- (void)start +{ + [super start]; + + _neededChunkCount = NSNotFound; + _currentChunkIdx = 0; + + [self downloadVideo]; +} + +- (void)downloadVideo +{ + NSURL *remoteURL = [self.coub remoteVideoFileURL]; + NSURL *localURL = [self.coub localVideoFileURL]; + + _isVideoDownloaded = [[NSFileManager defaultManager] fileExistsAtPath:[localURL path]]; + if(_isVideoDownloaded) + { + [self successVideoDownload]; + return; + }else{ + //KALog(@"video doesn't %i", _currentChunkIdx); + } + + NSURLRequest *downloadRequest = [NSURLRequest requestWithURL:remoteURL]; + + __weak typeof(self) wSelf = self; + + self.downloadOperation = [[LegacyComponentsGlobals provider] makeHTTPRequestOperationWithRequest:downloadRequest]; + self.downloadOperation.queuePriority = self.queuePriority; + self.downloadOperation.outputStream = [NSOutputStream outputStreamToFileAtPath:localURL.path append:NO]; + //[self.downloadOperation setShouldExecuteAsBackgroundTaskWithExpirationHandler:nil]; + [self.downloadOperation setDownloadProgressBlock:^(__unused NSInteger bytesRead, NSInteger totalBytesRead, NSInteger totalBytesExpectedToRead) { + float progress = totalBytesRead / (float) totalBytesExpectedToRead; + [wSelf progressDownload:progress]; + }]; + + [self.downloadOperation setCompletionBlockWithSuccess:^(__unused id operation, __unused id responseObject) { + [wSelf successVideoDownload]; + } failure:^(__unused id operation, NSError *error) { + + if([[NSFileManager defaultManager] fileExistsAtPath:[localURL path]]) + [[NSFileManager defaultManager] removeItemAtPath:[localURL path] error:nil]; + + [wSelf failureDownloadWithError:error]; + }]; + + [self.downloadOperation start]; +} + +- (void)downloadChunk +{ + NSURL *localURL = [self.coub localAudioChunkWithIdx:_currentChunkIdx]; + + //int fileExist = [[CBLibrary sharedLibrary] isCoubChunkDownloadedByPermalink:self.coub.permalink idx:_currentChunkIdx]; + int fileExist = [[NSFileManager defaultManager] fileExistsAtPath:[localURL path]]; + + if(fileExist || (_neededChunkCount == NSNotFound && _currentChunkIdx > 1)) + { + [self successDownload]; + return; + } + + NSURL *remoteURL = [self.coub remoteAudioChunkWithIdx:_currentChunkIdx + 1]; + NSURLRequest *downloadRequest = [NSURLRequest requestWithURL:remoteURL]; + + //NSLog(@"chunk idx = %i, remoteURL = %@", _currentChunkIdx, remoteURL); + + __weak typeof(self) wSelf = self; + + self.downloadOperation = [[LegacyComponentsGlobals provider] makeHTTPRequestOperationWithRequest:downloadRequest]; + self.downloadOperation.queuePriority = self.queuePriority; + self.downloadOperation.outputStream = [NSOutputStream outputStreamToFileAtPath:localURL.path append:NO]; + //[self.downloadOperation setShouldExecuteAsBackgroundTaskWithExpirationHandler:nil]; + [self.downloadOperation setDownloadProgressBlock:^(__unused NSInteger bytesRead, NSInteger totalBytesRead, NSInteger totalBytesExpectedToRead) { + + if(wSelf.neededChunkCount == NSNotFound) + { + //NSLog(@"totalBytesExpectedToRead = %llu", totalBytesExpectedToRead); + + wSelf.neededChunkCount = MIN((int) ((1024*1024)/totalBytesExpectedToRead), 4); + wSelf.neededChunkCount = MAX(wSelf.neededChunkCount, 1); + } + + float progress = totalBytesRead / (float) totalBytesExpectedToRead; + [wSelf progressDownload:progress]; + }]; + + [self.downloadOperation setCompletionBlockWithSuccess:^(__unused id operation, __unused id responseObject) { + //[wSelf.coub setFailedDownloadChunk:NO]; + [wSelf successChunkDownload]; + } failure:^(__unused id operation, NSError *error) { + + if([[NSFileManager defaultManager] fileExistsAtPath:[localURL path]]) + [[NSFileManager defaultManager] removeItemAtPath:[localURL path] error:nil]; + + if(error.code == -1011) + { + //NSLog(@"failed download"); + + //[wSelf.coub setFailedDownloadChunk:YES]; + [wSelf successDownload]; + + return; + } + //NSLog(@"failed download unknown"); + + [wSelf failureDownloadWithError:error]; + }]; + + [self.downloadOperation start]; +} + +- (void)successVideoDownload +{ + //[[CBLibrary sharedLibrary] markCoubAsset:self.coub asDownloaded:YES]; + + _isVideoDownloaded = YES; + + if(self.starting) + { + if(self.chunkDownloadingNeeded && [self.coub audioType] == CBCoubAudioTypeExternal) + [self downloadChunk]; + else + [super successDownload]; + } +} + +- (void)successDownload +{ + _currentChunkIdx++; + + if([self.coub audioType] != CBCoubAudioTypeExternal || _currentChunkIdx >= MIN(4, _neededChunkCount)) + { + [super successDownload]; + } + else + [self downloadChunk]; +} + +- (void)successChunkDownload +{ + [self successDownload]; +} + +- (void)progressDownload:(float)progress +{ + if(self.starting && self.operationViewDelegate) + { + float newProgress; + + if(_isVideoDownloaded) + { + newProgress = .5f + (_currentChunkIdx * 1/(float)_neededChunkCount + progress/(float)_neededChunkCount)*.5f; + }else{ + newProgress = progress/2.0f; + } + [self.operationViewDelegate downloadDidReachProgress:newProgress]; + } +} + +- (instancetype)clone +{ + CBCoubDownloadOperation *clone = [CBCoubDownloadOperation new]; + [clone setOperationViewDelegate:self.operationViewDelegate]; + [clone setTag:self.tag]; + [clone setCoub:self.coub]; + [clone setCompletionBlock:self.completionBlock]; + [clone setClientSuccess:self.clientSuccess]; + [clone setClientFailure:self.clientFailure]; + return clone; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/CBCoubLoopCompositionMaker.h b/submodules/LegacyComponents/LegacyComponents/CBCoubLoopCompositionMaker.h new file mode 100755 index 0000000000..e45269c609 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/CBCoubLoopCompositionMaker.h @@ -0,0 +1,41 @@ +// +// Created by Tikhonenko Pavel on 26/11/2013. +// Copyright (c) 2013 Coub. All rights reserved. +// + + +#import +#import "CBCoubAsset.h" + +#import + +@class CBCoubLoopCompositionMaker; +@protocol CBCoubAsset; + +@protocol CBCoubLoopDelegate + +@required +- (void)coubLoopDidFinishPreparing:(CBCoubLoopCompositionMaker *)loop; +- (void)coubLoop:(CBCoubLoopCompositionMaker *)loop didFailToLoadWithError:(NSError *)error; + +@end + +@interface CBCoubLoopCompositionMaker : NSObject + +@property (nonatomic, weak) id delegate; +@property (nonatomic, strong) id asset; + +@property (nonatomic, strong) AVAsset *videoAsset; +@property (nonatomic, strong) NSError *error; +@property (nonatomic, readonly) BOOL hasAudio; + +@property (nonatomic, assign, getter=isLoopReady) BOOL loopReady; + +- (void)prepareLoop; +- (void)cancelPrepareLoop; + +- (void)notifyObservers; + ++ (instancetype)coubLoopWithAsset:(id)asset; + +@end \ No newline at end of file diff --git a/submodules/LegacyComponents/LegacyComponents/CBCoubLoopCompositionMaker.m b/submodules/LegacyComponents/LegacyComponents/CBCoubLoopCompositionMaker.m new file mode 100755 index 0000000000..6d552e1839 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/CBCoubLoopCompositionMaker.m @@ -0,0 +1,322 @@ +// +// Created by Tikhonenko Pavel on 26/11/2013. +// Copyright (c) 2013 Coub. All rights reserved. +// + + +#import +#import "CBCoubLoopCompositionMaker.h" +#import "CBCoubAsset.h" +#import "CBConstance.h" +#import "AVAsset+CBExtension.h" + +#pragma mark - +#pragma mark CBCoubLoopOperation + +static AVURLAsset *gDigitalSilenceAsset = nil; + +@interface CBCoubLoopOperation : NSOperation + +- (id)initWithCoubAsset:(id)asset loop:(CBCoubLoopCompositionMaker *)loop; + +- (void)prepareOperation; +- (void)makeLoop; +- (AVComposition *)makeLoopComposition; + +- (BOOL)checkAssetURL:(NSURL *)assetURL; + +- (void)completeWithError:(NSError *)error; + +@end + +@implementation CBCoubLoopOperation +{ +@private + id _asset; + CBCoubLoopCompositionMaker *_loop; + AVAssetExportSession *_exportSession; + + NSURL *_assetURL; + BOOL _hasExternalAudio; +} + +- (id)initWithCoubAsset:(id)asset loop:(CBCoubLoopCompositionMaker *)loop +{ + self = [super init]; + if(self) + { + _asset = asset; + _loop = loop; + } + return self; +} + +- (void)main +{ + if([self isCancelled]) + return; + + [self prepareOperation]; +} + +- (void)prepareOperation +{ + if(![self checkAssetURL:_asset.localVideoFileURL]) + return; + + _assetURL = _asset.localVideoFileURL; + _hasExternalAudio = _asset.externalAudioURL != nil; + + [self makeLoop]; +} + +- (void)makeLoop +{ + AVURLAsset *avAsset = [AVURLAsset URLAssetWithURL:_assetURL options:@{AVURLAssetPreferPreciseDurationAndTimingKey : @YES}]; +// _loop.videoAsset = avAsset; +// [self completeWithError:nil]; +// return; + if(_asset.audioType == CBCoubAudioTypeInternal) + { + AVComposition *composition = [self makeLoopComposition]; + if(![self isCancelled]) + { + _loop.videoAsset = composition; + [self completeWithError:nil]; + }else{ + + } + + }else{ + _loop.videoAsset = avAsset; + [self completeWithError:nil]; + } + +} + +- (AVComposition *)makeLoopComposition +{ + AVURLAsset *avAsset = [AVURLAsset URLAssetWithURL:_assetURL options:@{AVURLAssetPreferPreciseDurationAndTimingKey : @YES}]; + NSArray *videoTracks = [avAsset tracksWithMediaType:AVMediaTypeVideo]; + if([videoTracks count] == 0) + { + + //TODO: remove cache file and download again + + NSError *error = [NSError errorWithDomain:NSCocoaErrorDomain + code:CBCoubLoopErrorNoVideoTracks + userInfo:_assetURL ? @{NSURLErrorKey : _assetURL} : nil]; + + [self completeWithError:error]; + + return nil; + } + + if([self isCancelled]) + return nil; + + AVAssetTrack *originalVideoTrack = videoTracks[0]; + AVAssetTrack *originalAudioTrack = _hasExternalAudio ? nil : avAsset.anyAudioTrack; + + if(originalVideoTrack == nil) + { + NSError *error = [NSError errorWithDomain:CBCoubLoopErrorDomain + code:CBCoubLoopErrorNoVideoTracks + userInfo:nil]; + [self completeWithError:error]; + return nil; + } + + CMTimeRange videoTrackTimeRange = originalVideoTrack.timeRange; +// videoTrackTimeRange.start.value = 1; +// videoTrackTimeRange.duration.value -= 2; + + CMTimeRange audioTrackTimeRange = originalAudioTrack ? originalAudioTrack.timeRange : kCMTimeRangeZero; +// if(originalAudioTrack) +// { +// audioTrackTimeRange.start.value = 1; +// audioTrackTimeRange.duration.value -= 2; +// } + + // Calculate the minimum duration of our composition + CMTime minimumCompositionDuration = CMTimeMake(60*audioTrackTimeRange.start.timescale, audioTrackTimeRange.start.timescale); + + if(!_hasExternalAudio) + { + if(originalAudioTrack) + audioTrackTimeRange = CMTimeRangeGetIntersection(audioTrackTimeRange, videoTrackTimeRange); + } + + AVAssetTrack *silence = nil; + CMTimeRange silenceTimeRange = kCMTimeRangeZero; + + if([self isCancelled]) + return nil; + + AVMutableComposition *composition = [AVMutableComposition composition]; + AVMutableCompositionTrack *videoTrack = [composition addMutableTrackWithMediaType:AVMediaTypeVideo preferredTrackID:kCMPersistentTrackID_Invalid]; + AVMutableCompositionTrack *audioTrack = originalAudioTrack ? [composition addMutableTrackWithMediaType:AVMediaTypeAudio preferredTrackID:kCMPersistentTrackID_Invalid] : nil; + + CMTime videoTrackDuration = kCMTimeZero; + NSError *error = nil; + while(CMTIME_COMPARE_INLINE(videoTrackDuration, <, minimumCompositionDuration)) + { + if([self isCancelled]) + return nil; + + if(![videoTrack insertTimeRange:videoTrackTimeRange ofTrack:originalVideoTrack atTime:videoTrackDuration error:&error]) + { +// KAObjectLogError(@"Failed to insert %@ of video track %@ at %@: %@", CBStringFromTimeRange(videoTrackTimeRange), originalVideoTrack, CBStringFromTime(videoTrackDuration), error); + [self completeWithError:error]; + return nil; + } + if(audioTrack) + { + if(![audioTrack insertTimeRange:audioTrackTimeRange ofTrack:originalAudioTrack atTime:videoTrackDuration error:&error]) + { + +// KAObjectLogError(@"Failed to insert %@ of audio track %@ at %@: %@", CBStringFromTimeRange(audioTrackTimeRange), originalAudioTrack, CBStringFromTime(videoTrackDuration), error); + [self completeWithError:error]; + return nil; + } + } + if(silence) + { + if(![audioTrack insertTimeRange:silenceTimeRange ofTrack:silence atTime:CMTimeAdd(videoTrackDuration, audioTrackTimeRange.duration) error:&error]) + { + [self completeWithError:error]; + return nil; + } + } + videoTrackDuration = CMTimeAdd(videoTrackDuration, videoTrackTimeRange.duration); + } + + videoTrack.preferredTransform = originalVideoTrack.preferredTransform; + return composition; +} + +- (BOOL)checkAssetURL:(NSURL *)assetURL +{ + if (assetURL == nil || ([assetURL isFileURL] && ![[NSFileManager defaultManager] fileExistsAtPath: [assetURL path]])) + { + NSLog(@"File doesn't exist: %@", assetURL); + //TODO: remove cache file and download again + + NSError *error = [NSError errorWithDomain:CBCoubLoopErrorDomain + code:CBCoubLoopErrorNoSuchFile + userInfo:assetURL ? @{NSURLErrorKey : assetURL} : nil]; + [self completeWithError:error]; + return NO; + }else{ + return YES; + } +} + +- (void)cancel +{ + [_exportSession cancelExport]; + [super cancel]; +} + +- (void)completeWithError:(NSError *)error +{ + dispatch_async(dispatch_get_main_queue(), ^ + { + [_exportSession cancelExport]; + + _loop.error = error; + [_loop notifyObservers]; + }); +} +@end + +#pragma mark - +#pragma mark CBCoubLoop + +static NSOperationQueue *gOperationQueue = nil; + +@implementation CBCoubLoopCompositionMaker +{ +@private + CBCoubLoopOperation *_operation; +} + ++ (void)initialize +{ + if(!gOperationQueue) + { + gOperationQueue = [NSOperationQueue new]; + [gOperationQueue setMaxConcurrentOperationCount:1]; + } +} + +- (id)initWithAsset:(id)asset +{ + self = [super init]; + + if(self) + { + _asset = asset; + } + + return self; +} + +- (BOOL)hasAudio +{ + return !(_asset.audioType == CBCoubAudioTypeNone); +} + +- (void)prepareLoop +{ + if(gOperationQueue.operations.count) + [gOperationQueue cancelAllOperations]; + + if(!_operation) + { + self.error = nil; + + CBCoubLoopOperation *operation = [[CBCoubLoopOperation alloc] initWithCoubAsset:_asset loop:self]; + [gOperationQueue addOperation:operation]; + + _operation = operation; + //[_operation start]; + } +} + +- (void)cancelPrepareLoop +{ + self.videoAsset = nil; + + [_operation cancel]; + _operation = nil; + + self.error = [NSError errorWithDomain:CBCoubLoopErrorDomain + code:CBCoubLoopErrorCanceled + userInfo:nil]; + [_delegate coubLoop:self didFailToLoadWithError:self.error]; + +} + +- (void)notifyObservers +{ + NSAssert([NSThread isMainThread], @"%s must be called on the main thread", __PRETTY_FUNCTION__); + + if(self.error) + [_delegate coubLoop:self didFailToLoadWithError:self.error]; + else + { + _loopReady = YES; + [_delegate coubLoopDidFinishPreparing:self]; + } + + _operation = nil; +} + ++ (instancetype)coubLoopWithAsset:(id)asset +{ + CBCoubLoopCompositionMaker *instance = [[CBCoubLoopCompositionMaker alloc] initWithAsset:asset]; + return instance; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/CBCoubNew.h b/submodules/LegacyComponents/LegacyComponents/CBCoubNew.h new file mode 100755 index 0000000000..19b534278a --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/CBCoubNew.h @@ -0,0 +1,70 @@ +// +// CBCoubNew.h +// Coub +// +// Created by Tikhonenko Pavel on 18/11/2013. +// Copyright (c) 2013 Coub. All rights reserved. +// + +#import +#import "CBCoubAuthorVO.h" +#import "CBCoubAudioSource.h" +#import "CBCoubVideoSource.h" +#import "CBCoubAsset.h" + +@interface CBCoubNew : NSObject + +@property(nonatomic, strong) NSString *coubID; +@property(nonatomic, strong) NSString *permalink; +@property(nonatomic, strong) NSString *originalPermalink; +@property(nonatomic, strong) NSString *visibility; // kCBCoubVisibility*, see above +@property(nonatomic, assign) BOOL isDone; + +@property(nonatomic, strong) CBCoubAuthorVO *author; +@property(nonatomic, strong) CBCoubAuthorVO *recouber; + +@property(nonatomic, strong) NSString *title; +@property(nonatomic, strong) NSDate *creationDate; +@property(nonatomic, strong) NSDate *originalCreationDate; +@property(nonatomic, strong) NSArray *tags; //CBTagNew + +//Stats +@property(nonatomic, assign) NSUInteger viewCount; +@property(nonatomic, assign) NSUInteger likeCount; +@property(nonatomic, assign) NSUInteger recoubCount; +@property(nonatomic, assign) BOOL liked; // whether the current user likes the coub; does not update likeCount +@property(nonatomic, assign) BOOL recoubed; +@property(nonatomic, assign) BOOL cotd; +@property(nonatomic, assign) BOOL flagged; +@property(nonatomic, assign) BOOL deleted; + +@property(nonatomic, assign) CBCoubAudioType audioType; + +@property(nonatomic, strong) NSString *externalDownloadType; +@property(nonatomic, strong) NSString *externalDownloadSource; // youtube/vimeo URL + +//@property (nonatomic, readonly) NSOrderedSet *recoubersOthenThanCurrentUser; +@property (nonatomic, readonly) NSURL *coubWebViewURL; +@property(nonatomic, readonly) NSURL *mediumImageURL; +@property(nonatomic, readonly) NSURL *largeImageURL; +@property(nonatomic, readonly) BOOL isRecoub; +@property(nonatomic, readonly) BOOL isMyCoub; + +@property(nonatomic, retain) NSURL *customLocalVideoFileURL; +@property(nonatomic, retain) NSString *remoteVideoLocation; +@property(nonatomic, retain) NSString *remoteAudioLocation; +@property(nonatomic, retain) NSString *remoteAudioLocationPattern; +@property(nonatomic, retain) NSString *mediumPicture; +@property(nonatomic, retain) NSString *largePicture; +@property(nonatomic, retain) NSString *creationDateAsString; +@property(nonatomic, retain) NSString *originalCreationDateAsString; + +@property(nonatomic, readonly) NSURL *remoteVideoFileURL; + +@property(nonatomic, assign) BOOL isCoubSourcesAvailable; +@property(nonatomic, strong) CBCoubAudioSource *audioSource; +@property(nonatomic, strong) CBCoubVideoSource *videoSource; + ++ (CBCoubNew *)coubWithAttributes:(NSDictionary *)attributes; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/CBCoubNew.m b/submodules/LegacyComponents/LegacyComponents/CBCoubNew.m new file mode 100755 index 0000000000..36c1db6f10 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/CBCoubNew.m @@ -0,0 +1,199 @@ +// +// CBCoubNew.m +// Coub +// +// Created by Tikhonenko Pavel on 18/11/2013. +// Copyright (c) 2013 Coub. All rights reserved. +// + +#import "CBCoubNew.h" +//#import "Formatters.h" +//#import "NSDictionary+Extensions.h" +#import "CBLibrary.h" +#import "CBJSONCoubMapper.h" +#import "CBConstance.h" + +@interface CBCoubNew () +{ + BOOL _failedDownloadChunks; +} +@end + +@implementation CBCoubNew + +//- (void)setNaturalVideoSize:(CGSize)size +//{ +// _naturalVideoSize = size; +//} + +- (BOOL)isDraft +{ + return NO; +} + +//- (NSDate *)creationDate +//{ +// if(!_creationDate) +// _creationDate = [[NSDateFormatter sharedCoubJSONDateFormatter] dateFromString:_creationDateAsString]; +// +// return _creationDate; +//} + +//- (NSDate *)originalCreationDate +//{ +// if(!_originalCreationDate) +// _originalCreationDate = [[NSDateFormatter sharedCoubJSONDateFormatter] dateFromString:_originalCreationDateAsString]; +// +// return _originalCreationDate; +//} + +- (NSURL *)mediumImageURL +{ + NSString *remoteImageFilePath = self.mediumPicture; + if(remoteImageFilePath) + return [NSURL URLWithString:[remoteImageFilePath hasPrefix:@"http://"] ? remoteImageFilePath : [[@"http://" stringByAppendingString:kCBServerURL] stringByAppendingPathComponent:remoteImageFilePath]]; + return nil; +} + + +- (NSURL *)largeImageURL +{ + NSString *remoteImageFilePath = self.largePicture; + if(remoteImageFilePath) + return [NSURL URLWithString:[remoteImageFilePath hasPrefix:@"http://"] ? remoteImageFilePath : [[@"http://" stringByAppendingString:kCBServerURL] stringByAppendingPathComponent:remoteImageFilePath]]; + return nil; +} + +- (BOOL)isRecoub +{ + return _recouber != nil ? YES : NO; +} + +//- (CBCoubStatusFlags)statusFlags +//{ +// CBCoubStatusFlags statusFlags = 0; +// +// if([_visibility isEqualToString:kCBCoubVisibilityFriends]) +// statusFlags |= CBCoubStatusFriendsOnly; +// else if([_visibility isEqualToString:kCBCoubVisibilityPrivate]) +// statusFlags |= CBCoubStatusPrivate; +// else if([_visibility isEqualToString:kCBCoubVisibilityUnlisted]) +// statusFlags |= CBCoubStatusUnlisted; +// switch(_audioType) +// { +// case CBCoubAudioTypeExternal: +// statusFlags |= CBCoubStatusExternalAudio; +// break; +// case CBCoubAudioTypeInternal: +// statusFlags |= CBCoubStatusHasAudioTrack; +// break; +// default: +// break; +// } +// +// return statusFlags; +//} + +- (NSURL *)remoteVideoFileURL +{ + NSURL *url = nil; + + NSString *remoteFilePath = self.remoteVideoLocation; + if([remoteFilePath isKindOfClass:[NSString class]] && remoteFilePath.length > 0) + { + url = [NSURL URLWithString:[remoteFilePath hasPrefix:@"http://"] ? remoteFilePath : [[@"http://" stringByAppendingString:kCBServerURL] stringByAppendingPathComponent:remoteFilePath]]; + if(!url) + [NSException raise:NSInternalInconsistencyException format:@"Could not make a URL from \"%@\"", remoteFilePath]; + } + return url; +} + +- (NSURL *)externalAudioURL +{ + NSURL *url = nil; + + NSString *remoteFilePath = self.remoteAudioLocation; + if([remoteFilePath isKindOfClass:[NSString class]] && remoteFilePath.length > 0) + { + url = [NSURL URLWithString:[remoteFilePath hasPrefix:@"http://"] ? remoteFilePath : [[@"http://" stringByAppendingString:kCBServerURL] stringByAppendingPathComponent:remoteFilePath]]; + if(!url) + [NSException raise:NSInternalInconsistencyException format:@"Could not make a URL from \"%@\"", remoteFilePath]; + } + return url; +} + +- (NSURL *)localVideoFileURL +{ + if (self.customLocalVideoFileURL != nil) + return self.customLocalVideoFileURL; + + if(self.permalink == nil) + return nil; + + + NSString *fileNameExtension = [self.remoteVideoLocation pathExtension] ?: @"mp4"; + NSString *localFileName = [[@"coub " stringByAppendingString:self.permalink] stringByAppendingPathExtension:fileNameExtension]; + NSString *path = [[CBLibrary sharedLibrary].mediaDirectory.path stringByAppendingPathComponent:localFileName]; + return [NSURL fileURLWithPath:path isDirectory:NO]; +} + +- (NSURL *)localAudioFileURL +{ + if(self.permalink == nil) + return nil; + + NSString *fileNameExtension = @"m4a"; + NSString *localFileName = [[@"coub " stringByAppendingString:self.permalink] stringByAppendingPathExtension:fileNameExtension]; + return [NSURL fileURLWithPath:[[CBLibrary sharedLibrary].mediaDirectory.path stringByAppendingPathComponent:localFileName] isDirectory:NO]; +} + +- (NSString *)assetId +{ + return self.permalink; +} + +- (BOOL)isAudioAvailable +{ + return (_audioType != CBCoubAudioTypeNone && !_failedDownloadChunks); +} + +- (BOOL)isEqualToCoub:(CBCoubNew *)coub +{ + return [coub.permalink isEqualToString:self.permalink]; +} + ++ (CBCoubNew *)coubWithAttributes:(NSDictionary *)attributes +{ + return [CBJSONCoubMapper coubFromJSONObject:attributes]; +} + +- (NSURL *)remoteAudioChunkWithIdx:(NSInteger)idx +{ + //NSLog(@"remoteAudioChunkWithIdx = %@ ", [NSURL URLWithString:[NSString stringWithFormat:_remoteAudioLocationPattern, idx]]); + + //NOTE: sometimes _remoteAudioLocationPattern + return [NSURL URLWithString:[NSString stringWithFormat:_remoteAudioLocationPattern, idx]]; +} + +- (NSURL *)localAudioChunkWithIdx:(NSInteger)idx +{ + //NSLog(@"localAudioChunkWithIdx = %i", idx); + + NSString *fileNameExtension = @"mp3"; + NSString *fileName = [[NSString stringWithFormat:@"coub mp3 chunk %i ", idx] stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; + NSString *localFileName = [[fileName stringByAppendingString:self.permalink] stringByAppendingPathExtension:fileNameExtension]; + + return [NSURL fileURLWithPath:[[CBLibrary sharedLibrary].mediaDirectory.path stringByAppendingPathComponent:localFileName] isDirectory:NO]; +} + +- (BOOL)failedDownloadChunk +{ + return _failedDownloadChunks; +} + +- (void)setFailedDownloadChunk:(BOOL)failed +{ + _failedDownloadChunks = failed; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/CBCoubPlayer.h b/submodules/LegacyComponents/LegacyComponents/CBCoubPlayer.h new file mode 100755 index 0000000000..b212174afe --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/CBCoubPlayer.h @@ -0,0 +1,96 @@ +// +// CBCoubPlayer.h +// Coub +// +// Created by Pavel Tikhonenko on 12/08/14. +// Copyright (c) 2014 Coub. All rights reserved. +// + +#import + +@class CBCoubPlayer; +@protocol CBCoubAsset; +@class AVPlayerLayer; + +typedef enum +{ + CBCoubPlayerStateReadyToPlay, + CBCoubPlayerStateRunning = 1, + CBCoubPlayerStatePlaying = (1 << 1) | CBCoubPlayerStateRunning, + CBCoubPlayerStatePaused = (1 << 2) | CBCoubPlayerStatePlaying, + CBCoubPlayerStateInterrupted = (1 << 3) | CBCoubPlayerStatePlaying, + CBCoubPlayerStateStopped = (1 << 4), + CBCoubPlayerStatePseudoStopped = (1 << 5) | CBCoubPlayerStateStopped, + CBCoubPlayerStateError = (1 << 6) | CBCoubPlayerStateStopped, + CBCoubPlayerStatePrepairing = (1 << 7) | CBCoubPlayerStateRunning, + CBCoubPlayerStateUnknown = NSNotFound, +} +CBCoubPlayerState; + +typedef enum +{ + CBCoubPlayerVideoPlayMethodDefault, + CBCoubPlayerVideoPlayMethodStream +} +CBCoubPlayerVideoPlayMethod; + +#pragma mark - +#pragma mark CBCoubPlayerDelegate + +@protocol CBCoubPlayerDelegate +@required +- (void)playerReadyToPlay:(CBCoubPlayer *)player; +- (void)playerDidStartPlaying:(CBCoubPlayer *)player; +- (void)playerDidPause:(CBCoubPlayer *)player withUserAction:(BOOL)isUserAction; +- (void)playerDidResume:(CBCoubPlayer *)player; +- (void)playerDidStop:(CBCoubPlayer *)player; +- (void)playerDidFail:(CBCoubPlayer *)player error:(NSError *)error; +- (void)player:(CBCoubPlayer *)player didReachProgressWhileDownloading:(float)progress; +@end + +#pragma mark - +#pragma mark CBCoubPlayer + +@interface CBCoubPlayer : NSObject + +@property (nonatomic, weak) id delegate; + +/// indicate whether player is playing +@property(nonatomic, readonly) BOOL isPlaying; +/// indicate whether player was paused +@property(nonatomic, readonly) BOOL isPaused; +/// indicate whether player was interrupted by system +@property(nonatomic, readonly) BOOL isInterrupted; +/// If Yes then this instance is current/active player +@property(nonatomic, readonly) BOOL isActivePlayer; +/// set/get current player state +@property(nonatomic, assign) CBCoubPlayerState state; + +/// set/get whether player playback only video +@property(nonatomic, assign) BOOL withoutAudio; //Default NO + +//Temp later move back to private var +@property(nonatomic, strong) id asset; + +@property(nonatomic, assign) CBCoubPlayerVideoPlayMethod videoPlayMethod; + + + +- (instancetype)initWithVideoLayer:(AVPlayerLayer *)layer; + +- (void)playAsset:(id)asset; +- (void)pause; +- (void)resume; +- (void)stop; + +// pause video and mute audio +- (void)pseudoStop; + +- (BOOL)isVideoLayerPlaying:(AVPlayerLayer *)layer; +- (BOOL)coubPlaying:(id)coub; +- (BOOL)coubPrepairing:(id)coub; + ++ (instancetype)activePlayer; ++ (instancetype)setActivePlayer:(CBCoubPlayer *)player; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/CBCoubPlayer.m b/submodules/LegacyComponents/LegacyComponents/CBCoubPlayer.m new file mode 100755 index 0000000000..4c2d059adc --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/CBCoubPlayer.m @@ -0,0 +1,585 @@ +// +// CBCoubPlayer.m +// Coub +// +// Created by Pavel Tikhonenko on 12/08/14. +// Copyright (c) 2014 Coub. All rights reserved. +// + +#import +#import "CBCoubPlayer.h" +#import "CBCoubAsset.h" +#import "CBVideoPlayer.h" +#import "CBCoubLoopCompositionMaker.h" +#import "CBAssetDownloadManager.h" +#import "STKAudioPlayer.h" +#import "CBLibrary.h" +#import "CBConstance.h" +#import "CBDownloadOperationDelegate.h" + +static CBCoubPlayer *gActivePlayer = nil; +static NSLock *gLock = nil; +static BOOL gPlaybackIsInterrupted = NO; + +@interface CBCoubPlayer () +{ + BOOL _shouldPlayWhenReady; + BOOL _shouldResumeWhenAppIsActive; + BOOL _shouldResumeWhenInterruptionEnds; + BOOL _shouldPlayAfterStop; + NSInteger _currentPlayingChunk; + double _startTime; +} + + +@property(nonatomic, strong) CBVideoPlayer *videoPlayer; +@property(nonatomic, strong) STKAudioPlayer *audioPlayer; +@property(nonatomic, strong) CBCoubLoopCompositionMaker *loopMaker; + + +@end + +@implementation CBCoubPlayer + ++ (void)initialize +{ + if(!gLock) + { + NSError *categoryError = NULL; + NSError *activeError = NULL; + + [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:&categoryError]; + [[AVAudioSession sharedInstance] setActive:YES error:&activeError]; + + Float32 bufferLength = 0.1; + AudioSessionSetProperty(kAudioSessionProperty_PreferredHardwareIOBufferDuration, sizeof(bufferLength), &bufferLength); + + gLock = [[NSLock alloc] init]; + + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(pauseActivePlayer:) name:UIApplicationWillResignActiveNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(resumeActivePlayer:) name:UIApplicationDidBecomeActiveNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(pauseActivePlayer:) name:CBPlayerInterruptionDidBeginNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(resumeActivePlayer:) name:CBPlayerInterruptionDidEndNotification object:nil]; + } +} + +- (instancetype)initWithVideoLayer:(AVPlayerLayer *)layer +{ + return [self initWithVideoLayer:layer videoPlayMethod:CBCoubPlayerVideoPlayMethodDefault]; +} + +- (instancetype)initWithVideoLayer:(AVPlayerLayer *)layer videoPlayMethod:(CBCoubPlayerVideoPlayMethod)videoPlayMethod +{ + self = [super init]; + + if(self) + { + _currentPlayingChunk = -1; + _videoPlayMethod = videoPlayMethod; + _state = CBCoubPlayerStateUnknown; + + [self createVideoPlayerWithLayer:layer]; + + } + + return self; +} + +#pragma mark - +#pragma mark Getter/Setter + +- (BOOL)isPlaying +{ + return _state == CBCoubPlayerStatePlaying; +} + +- (BOOL)isPaused +{ + return _state == CBCoubPlayerStatePaused; +} + +- (BOOL)isActivePlayer +{ + return (gActivePlayer == self); +} + +#pragma mark - +#pragma mark Public methods + +- (void)playAsset:(id)asset +{ + NSLog(@"playAsset"); + + if(_state == CBCoubPlayerStateRunning && self.asset == asset) + return; + + if(gActivePlayer == self && self.asset == asset && gActivePlayer.state == CBCoubPlayerStatePlaying) + return; + + [[NSNotificationCenter defaultCenter] postNotificationName:@"interruptDownloading" object:nil]; + + //_startTime = [CBUtils timestamp]; + + if(gActivePlayer == self) + { + [_videoPlayer stop]; + [_audioPlayer dispose]; + }else{ + [self stopActivePlayer]; + [self becomeActivePlayer]; + } + + gPlaybackIsInterrupted = NO; //NOTE: note note note + _shouldPlayWhenReady = YES; + self.state = CBCoubPlayerStateRunning; + + self.asset = asset; + + [self downloadMediaAssetsWithCompletion:^(__unused id result) { + [self prepareLoopCompostion]; + }failure:^(NSError *error) { + [self failPlaybackWithError:error]; + }]; +} + +- (void)pause +{ + _shouldPlayWhenReady = NO; + + if(!self.isPlaying) + return; + + self.state = CBCoubPlayerStatePaused; + + [_videoPlayer pause]; + + if(_asset.audioType == CBCoubAudioTypeExternal) + [_audioPlayer pause]; + + if([_delegate respondsToSelector:@selector(playerDidPause:withUserAction:)]) + [_delegate playerDidPause:self withUserAction:YES]; +} + +- (void)resume +{ + _shouldPlayWhenReady = YES; + +// if(_state == CBCoubPlayerStateReadyToPlay) +// { +// [self startPlayingIfPossible]; +// } + if(self.state == CBCoubPlayerStatePaused){ + self.state = STKAudioPlayerStatePlaying; + + [_videoPlayer play]; + + if(_asset.audioType == CBCoubAudioTypeExternal) + [_audioPlayer resume]; + } + + if([_delegate respondsToSelector:@selector(playerDidResume:)]) + [_delegate playerDidResume:self]; +} + +- (void)stop +{ + _shouldPlayWhenReady = NO; + + if(self.state == CBCoubPlayerStateStopped || self.state == NSNotFound) + return; + + //KAObjectLog(@"stop player"); + + //[[CBAssetDownloadManager sharedManager] cancelDownloadingForCoub:_asset]; + + if([_delegate respondsToSelector:@selector(playerDidStop:)]) + [_delegate playerDidStop:self]; + +// [_audioPlayer pause]; + if(_asset.audioType == CBCoubAudioTypeExternal) + { + _audioPlayer.delegate = nil; + [_audioPlayer dispose]; + _audioPlayer = nil; + } + + [_videoPlayer stop]; + +// dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ +// +// }); + + self.state = CBCoubPlayerStateStopped; + + [self resignActivePlayer]; +} + +- (void)pseudoStop +{ + _shouldPlayWhenReady = NO; + + if(self.isPlaying) + { + if(_asset.audioType == CBCoubAudioTypeExternal) + [_audioPlayer pause]; + else + [_videoPlayer pause]; + } + + self.state = CBCoubPlayerStatePseudoStopped; +} + +- (void)stopActivePlayer +{ + [gActivePlayer stop]; +} + +- (void)resetCurrentPlayer +{ + self.state = NSNotFound; + _shouldPlayWhenReady = YES; + _currentPlayingChunk = -1; + [self.loopMaker cancelPrepareLoop]; +} + +#pragma mark - +#pragma mark Private methods + +- (void)createVideoPlayerWithLayer:(AVPlayerLayer *)layer +{ + self.videoPlayer = [[CBVideoPlayer alloc] initWithVideoLayer:layer]; +} + +- (void)prepareVideoPlayer +{ + [_videoPlayer prepareWithAVAsset:self.loopMaker.videoAsset completion:^(NSError *error) { + if(error) + [self failPlaybackWithError:error]; + else{ + if([_delegate respondsToSelector:@selector(playerReadyToPlay:)]) + [_delegate playerReadyToPlay:self]; + + [self startPlayingIfPossible]; + } + }]; + + [self.loopMaker cancelPrepareLoop]; +} + +- (void)playVideoPlayer +{ + [_videoPlayer play]; + + if(_shouldPlayWhenReady == NO) + [self pause]; +} + +- (void)createAudioPlayer +{ + self.audioPlayer = [[STKAudioPlayer alloc] init]; + _audioPlayer.delegate = self; +} + +- (void)prepareAudioPlayer +{ + _currentPlayingChunk = -1; + +// STKAudioPlayerState state = _audioPlayer.state; + + [self startPlayingFirstAudioChunk]; +} + +- (void)startPlayingFirstAudioChunk +{ +// NSFileManager *fileManager = [NSFileManager defaultManager]; +// NSDictionary *attributes = [fileManager attributesOfItemAtPath:[_asset localAudioChunkWithIdx:0].path error:nil]; + + _shouldPlayAfterStop = NO; + + NSURL * url = [_asset localAudioChunkWithIdx:0]; + + if (url) { + [_audioPlayer playURL:url]; + } +} + +- (void)startPlayingIfPossible +{ + if(self.state == CBCoubPlayerStatePlaying) + return; + + self.state = CBCoubPlayerStateReadyToPlay; + + if(gPlaybackIsInterrupted && _shouldPlayWhenReady) + [self pauseWhileInterrupted]; + + if(!_shouldPlayWhenReady) + return; + + if(!_withoutAudio && _asset.audioType == CBCoubAudioTypeExternal && _audioPlayer.state != STKAudioPlayerStatePlaying) + { + [self createAudioPlayer]; + [self prepareAudioPlayer]; + return; + } + + [[NSNotificationCenter defaultCenter] postNotificationName:@"resumeDownloading" object:nil]; + + self.state = CBCoubPlayerStatePlaying; + + if([_delegate respondsToSelector:@selector(playerDidStartPlaying:)]) + [_delegate playerDidStartPlaying:self]; + + + [self playVideoPlayer]; + +// NSNumber *number = [NSNumber numberWithDouble:[CBUtils timestamp] - _startTime]; +// NSString *curPlace = [CBAnalyticManager sharedManager].currentPlayerPlace; +// NSString *assetId = _asset.assetId; +// +// NSDictionary *dict = @{@"loading_time":number, +// @"coubId":assetId, +// @"place": curPlace}; +// +// [[CBAnalyticManager sharedManager] track:@"player_started" +// properties:dict +// method:CBAnalyticManagerMethodCES]; +} + +- (void)downloadMediaAssetsWithCompletion:(CBSuccessBlock)success failure:(CBFailureBlock)failure +{ + [[CBAssetDownloadManager sharedManager] downloadCoub:_asset tag:-1 + withDelegate:self + withChunks:!_withoutAudio + downloadSucces:^(__unused id coub, __unused NSInteger tag) { + success(nil); + }downloadFailure:^(__unused id coub, __unused NSInteger tag, NSError *error) { + failure(error); + }]; +} + +- (void)downloadChunk:(NSInteger)idx +{ + if(_asset.audioType != CBCoubAudioTypeExternal) + return; + + [[CBAssetDownloadManager sharedManager] downloadChunkWithCoub:_asset tag:NSNotFound chunkIdx:idx downloadSucces:^(id __unused coub, NSInteger __unused tag) { + [_audioPlayer queueURL:[_asset localAudioChunkWithIdx:idx]]; + } downloadFailure:^(__unused id coub, __unused NSInteger tag, __unused NSError *error) { + + }]; + +// [[CBAssetDownloadManager sharedManager] downloadNextChunkWithCoub:_asset downloadSucces:^(id coub, NSInteger tag) { +// [_audioPlayer queueURL:[_asset localAudioChunkWithIdx:tag]]; +// } downloadFailure:^(id coub, NSInteger tag, NSError *error) { +// +// }]; +} + +- (void)prepareLoopCompostion +{ + if(self.state == CBCoubPlayerStateStopped) + return; + + self.loopMaker = [CBCoubLoopCompositionMaker coubLoopWithAsset:_asset]; + _loopMaker.delegate = self; + [_loopMaker prepareLoop]; +} + +- (void)failPlaybackWithError:(NSError *)error +{ + if(self.state == CBCoubPlayerStateStopped) + return; + + self.state = CBCoubPlayerStateError; + + if([_delegate respondsToSelector:@selector(playerDidFail:error:)]) + [_delegate playerDidFail:self error:error]; +} + +#pragma mark - + +- (void)becomeActivePlayer +{ + [gLock lock]; + if(gActivePlayer != self) + { + //[gActivePlayer pause]; + gActivePlayer = self; + } + [gLock unlock]; +} + + +- (void)resignActivePlayer +{ + [gLock lock]; + if(gActivePlayer == self && self.isPlaying == NO) + gActivePlayer = nil; + [gLock unlock]; +} + +#pragma mark - +#pragma mark Audio Player Delegate methods + +- (void)audioPlayer:(STKAudioPlayer *)audioPlayer stateChanged:(STKAudioPlayerState)state previousState:(STKAudioPlayerState)previousState +{ + if(state == STKAudioPlayerStateStopped && _shouldPlayAfterStop) + { + [self startPlayingFirstAudioChunk]; + return; + } + + if(state == STKAudioPlayerStatePlaying && self.state != CBCoubPlayerStatePlaying) + { + //KAObjectLog(@"STKAudioPlayerStatePlaying"); + + [self startPlayingIfPossible]; + } +} + +- (void)audioPlayer:(STKAudioPlayer *)audioPlayer unexpectedError:(STKAudioPlayerErrorCode)errorCode +{ + [self failPlaybackWithError:nil]; +} + +- (void)audioPlayer:(STKAudioPlayer *)audioPlayer didStartPlayingQueueItemId:(NSObject *)queueItemId +{ + if(_currentPlayingChunk == 3) + _currentPlayingChunk = -1; + + _currentPlayingChunk++; + + NSInteger nextItem = _currentPlayingChunk+1; + nextItem = nextItem > 3 ? 0 : nextItem; + + if([[CBLibrary sharedLibrary] isCoubChunkDownloadedByPermalink:_asset.assetId idx:nextItem]) + [_audioPlayer queueURL:[_asset localAudioChunkWithIdx:nextItem]]; + else + [self downloadChunk:nextItem]; +} + +- (void)audioPlayer:(STKAudioPlayer *)audioPlayer didFinishBufferingSourceWithQueueItemId:(NSObject *)queueItemId +{ + +} + +- (void)audioPlayer:(STKAudioPlayer *)audioPlayer didFinishPlayingQueueItemId:(NSObject *)queueItemId withReason:(STKAudioPlayerStopReason)stopReason andProgress:(double)progress andDuration:(double)duration +{ + +} + +- (void)audioPlayer:(STKAudioPlayer *)audioPlayer logInfo:(NSString *)line +{ + //KAObjectLog(@"%@", line); +} + +#pragma mark - +#pragma mark Download Manager Delegate methods + +- (void)downloadDidReachProgress:(float)progress +{ + if([_delegate respondsToSelector:@selector(player:didReachProgressWhileDownloading:)]) + [_delegate player:self didReachProgressWhileDownloading:progress]; +} + +- (void)downloadHasBeenCancelledWithError:(NSError *)error +{ + if(error == nil) + return; + + [self failPlaybackWithError:error]; +} + +#pragma mark - +#pragma mark Loop Composition Maker Delegate methods + +- (void)coubLoopDidFinishPreparing:(CBCoubLoopCompositionMaker *)loop +{ + [self prepareVideoPlayer]; +} + +- (void)coubLoop:(CBCoubLoopCompositionMaker *)loop didFailToLoadWithError:(NSError *)error +{ + if(error.code == CBCoubLoopErrorCanceled) + return; + + [self failPlaybackWithError:error]; +} + +#pragma mark - + ++ (void)pauseActivePlayer:(NSNotification *)notification +{ + //KAObjectLog(@"pauseActivePlayer %p", gActivePlayer); + + if([[notification name] isEqualToString:UIApplicationWillResignActiveNotification]) + { + [gActivePlayer pauseWhileInBackground]; + }else + { + gPlaybackIsInterrupted = YES; + [gActivePlayer pauseWhileInterrupted]; + } +} + + ++ (void)resumeActivePlayer:(NSNotification *)notification +{ + //KAObjectLog(@"resumeActivePlayer %p", gActivePlayer); + + NSLog(@"resumeActivePlayer"); + + if([[notification name] isEqualToString:UIApplicationDidBecomeActiveNotification]) + { + [gActivePlayer resumeIfPausedWhileInBackground]; + }else + { + gPlaybackIsInterrupted = NO; + [gActivePlayer resumeIfPausedWhileInterrupted]; + } +} + +- (void)pauseWhileInBackground +{ + _shouldResumeWhenAppIsActive = self.isPlaying || _shouldPlayWhenReady; + [self pause]; +} + + +- (void)resumeIfPausedWhileInBackground +{ + NSLog(@"resumeIfPausedWhileInBackground"); + + if(_shouldResumeWhenAppIsActive) + [self resume]; +} + + +- (void)pauseWhileInterrupted +{ + _shouldResumeWhenInterruptionEnds = self.isPlaying || _shouldPlayWhenReady; + [self pause]; +} + + +- (void)resumeIfPausedWhileInterrupted +{ + if(_shouldResumeWhenInterruptionEnds) + [self resume]; +} + +#pragma mark - + ++ (instancetype)activePlayer +{ + return gActivePlayer; +} + +- (void)dealloc +{ + _audioPlayer.delegate = nil; + [_audioPlayer dispose]; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/CBCoubPlayerContance.h b/submodules/LegacyComponents/LegacyComponents/CBCoubPlayerContance.h new file mode 100755 index 0000000000..935c8e4685 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/CBCoubPlayerContance.h @@ -0,0 +1,20 @@ +// +// CBCoubPlayerContance.h +// CoubPlayer +// +// Created by Pavel Tikhonenko on 19/10/14. +// Copyright (c) 2014 Pavel Tikhonenko. All rights reserved. +// + +#import + +typedef NS_ENUM(UInt16, CBCoubAudioType) +{ + CBCoubAudioTypeNone = 0, + CBCoubAudioTypeInternal, + CBCoubAudioTypeExternal, +}; + +@interface CBCoubPlayerContance : NSObject + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/CBCoubPlayerContance.m b/submodules/LegacyComponents/LegacyComponents/CBCoubPlayerContance.m new file mode 100755 index 0000000000..d2850886ff --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/CBCoubPlayerContance.m @@ -0,0 +1,13 @@ +// +// CBCoubPlayerContance.m +// CoubPlayer +// +// Created by Pavel Tikhonenko on 19/10/14. +// Copyright (c) 2014 Pavel Tikhonenko. All rights reserved. +// + +#import "CBCoubPlayerContance.h" + +@implementation CBCoubPlayerContance + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/CBCoubVideoSource.h b/submodules/LegacyComponents/LegacyComponents/CBCoubVideoSource.h new file mode 100755 index 0000000000..a07b1d4c4c --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/CBCoubVideoSource.h @@ -0,0 +1,20 @@ +// +// CBCoubVideoSource.h +// Coub +// +// Created by Tikhonenko Pavel on 18/11/2013. +// Copyright (c) 2013 Coub. All rights reserved. +// + +#import + +@interface CBCoubVideoSource : NSObject + +@property (nonatomic, strong) NSString *yourtubeURL; +@property (nonatomic, strong) NSString *title; +@property (nonatomic, strong) NSString *thumbnail; +@property (nonatomic, readonly) BOOL isYouTube; + ++ (CBCoubVideoSource *)sourceFromData:(NSDictionary *)dict; + +@end \ No newline at end of file diff --git a/submodules/LegacyComponents/LegacyComponents/CBCoubVideoSource.m b/submodules/LegacyComponents/LegacyComponents/CBCoubVideoSource.m new file mode 100755 index 0000000000..7226506f97 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/CBCoubVideoSource.m @@ -0,0 +1,33 @@ +// +// CBCoubVideoSource.m +// Coub +// +// Created by Tikhonenko Pavel on 18/11/2013. +// Copyright (c) 2013 Coub. All rights reserved. +// + +#import "CBCoubVideoSource.h" + +@implementation CBCoubVideoSource + +- (BOOL)isYouTube +{ + return [_yourtubeURL rangeOfString:@"youtube"].length != 0; +} + ++ (CBCoubVideoSource *)sourceFromData:(NSDictionary *)dict +{ + if(!dict.count) return nil; + + CBCoubVideoSource *source = [[CBCoubVideoSource alloc] init]; + source.yourtubeURL = dict[@"url"]; + source.title = dict[@"title"]; + source.thumbnail = dict[@"image"]; + + if(source.yourtubeURL == nil) + return nil; + + return source; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/CBDownloadOperation.h b/submodules/LegacyComponents/LegacyComponents/CBDownloadOperation.h new file mode 100755 index 0000000000..fb98a3edfb --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/CBDownloadOperation.h @@ -0,0 +1,37 @@ +// +// CBDownloadOperation.h +// CoubPlayer +// +// Created by Pavel Tikhonenko on 19/10/14. +// Copyright (c) 2014 Pavel Tikhonenko. All rights reserved. +// + +#import +@protocol CBCoubAsset; +@protocol CBDownloadOperationDelegate; + +@protocol CBDownloadOperation + +@required +- (void)setTag:(NSInteger)tag; +- (NSInteger)tag; + +- (void)setCoub:(id)coub; +- (id)coub; + +- (void)setQueuePriority:(NSOperationQueuePriority)queuePriority; +- (NSOperationQueuePriority)queuePriority; + +//- (void)setDelegate:(id)delegate; + +- (void)setClientSuccess:(void (^)(id operation, NSInteger tag))success; +- (void)setClientFailure:(void (^)(id operation, NSInteger tag, NSError *error))failure; + +- (void)setCompletionBlock:(void (^)(id operation, NSError *error))completion; + +- (void)start; +- (void)cancel; + +- (instancetype)clone; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/CBDownloadOperationDelegate.h b/submodules/LegacyComponents/LegacyComponents/CBDownloadOperationDelegate.h new file mode 100755 index 0000000000..9639ef811b --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/CBDownloadOperationDelegate.h @@ -0,0 +1,17 @@ +// +// CBDownloadOperationDelegate.h +// CoubPlayer +// +// Created by Pavel Tikhonenko on 19/10/14. +// Copyright (c) 2014 Pavel Tikhonenko. All rights reserved. +// + +#import + +@protocol CBDownloadOperationDelegate + +@optional +- (void)downloadDidReachProgress:(float)progress; +- (void)downloadHasBeenCancelledWithError:(NSError *)error; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/CBGenericDownloadOperation.h b/submodules/LegacyComponents/LegacyComponents/CBGenericDownloadOperation.h new file mode 100755 index 0000000000..6b55e97fd5 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/CBGenericDownloadOperation.h @@ -0,0 +1,28 @@ +#import +#import "CBDownloadOperation.h" +#import "CBCoubAsset.h" +#import "CBDownloadOperationDelegate.h" +#import "LegacyHTTPRequestOperation.h" + +@interface CBGenericDownloadOperation : NSObject + +@property (nonatomic, strong) NSOperation *downloadOperation; +@property (nonatomic, readwrite) NSOperationQueuePriority queuePriority; + +@property (nonatomic, assign) NSInteger tag; +@property (nonatomic, assign) BOOL starting; +@property (nonatomic, assign) BOOL comleted; +@property (nonatomic, assign) BOOL chunkDownloadingNeeded; + +@property (nonatomic, weak) id coub; +@property (nonatomic, weak) id operationViewDelegate; + +@property(nonatomic, copy) void (^clientSuccess)(id, NSInteger tag); +@property(nonatomic, copy) void (^clientFailure)(id, NSInteger tag, NSError *error); + +@property (nonatomic, copy) void (^completionBlock)(id process, NSError *error); + +//protected +- (void)successDownload; +- (void)failureDownloadWithError:(NSError *)error; +@end diff --git a/submodules/LegacyComponents/LegacyComponents/CBGenericDownloadOperation.m b/submodules/LegacyComponents/LegacyComponents/CBGenericDownloadOperation.m new file mode 100755 index 0000000000..a86dfc3b8c --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/CBGenericDownloadOperation.m @@ -0,0 +1,89 @@ +// +// Created by Tikhonenko Pavel on 27/05/2014. +// Copyright (c) 2014 Coub. All rights reserved. +// + +#import "CBGenericDownloadOperation.h" +#import "CBCoubAsset.h" + +@implementation CBGenericDownloadOperation +{ + NSOperationQueuePriority _queuePriority; +} + +- (NSOperationQueuePriority)queuePriority +{ + return _queuePriority; +} + +- (void)setQueuePriority:(NSOperationQueuePriority)queuePriority +{ + _queuePriority = queuePriority; + + if(_downloadOperation) + _downloadOperation.queuePriority = queuePriority; +} + +- (void)start +{ + self.starting = YES; +} + +- (void)cancel +{ + self.starting = NO; + + NSError *cancelError = nil; + + if (!_downloadOperation.isFinished && !_downloadOperation.isCancelled) + { + //cancelError = [NSError errorWithDomain:kCBAssetDownloadManagerErrorDomain code:99 userInfo:nil]; + [_downloadOperation cancel]; + } + + [_operationViewDelegate downloadHasBeenCancelledWithError:cancelError]; + + self.clientSuccess = nil; + self.clientFailure = nil; +} + +- (void)successDownload +{ + if(self.starting) + { + self.starting = NO; + self.comleted = YES; + + if(self.operationViewDelegate != nil)[self.operationViewDelegate downloadHasBeenCancelledWithError:nil]; + if(self.clientSuccess != nil) self.clientSuccess(self.coub, self.tag); + if(self.completionBlock != nil) self.completionBlock(self, nil); + + self.downloadOperation = nil; + } +} + + +- (void)failureDownloadWithError:(NSError *)error +{ + if(self.starting) + { + self.starting = NO; + self.comleted = YES; + + if(self.operationViewDelegate != nil)[self.operationViewDelegate downloadHasBeenCancelledWithError:error]; + if(self.clientFailure != nil) self.clientFailure(self.coub, self.tag, error); + if(self.completionBlock != nil) self.completionBlock(self, error); + } +} + +- (instancetype)clone +{ + return nil; +} + +- (void)dealloc +{ + self.completionBlock = nil; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/CBJSONCoubMapper.h b/submodules/LegacyComponents/LegacyComponents/CBJSONCoubMapper.h new file mode 100755 index 0000000000..6effbcbaec --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/CBJSONCoubMapper.h @@ -0,0 +1,18 @@ +// +// Created by Tikhonenko Pavel on 29/11/2013. +// Copyright (c) 2013 Coub. All rights reserved. +// + + +#import + +@class CBCoubNew; + + +@interface CBJSONCoubMapper : NSObject + ++ (CBCoubNew *)updateCoubFromCoub:(CBCoubNew *)newCoub coub:(CBCoubNew *)coub; ++ (CBCoubNew *)updateCoubFromJSONObject:(NSDictionary *)jsonObj coub:(CBCoubNew *)coub; ++ (CBCoubNew *)coubFromJSONObject:(NSDictionary *)jsonObj; + +@end \ No newline at end of file diff --git a/submodules/LegacyComponents/LegacyComponents/CBJSONCoubMapper.m b/submodules/LegacyComponents/LegacyComponents/CBJSONCoubMapper.m new file mode 100755 index 0000000000..ebdc9e9dcf --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/CBJSONCoubMapper.m @@ -0,0 +1,254 @@ +// +// Created by Tikhonenko Pavel on 29/11/2013. +// Copyright (c) 2013 Coub. All rights reserved. +// + + +#import "CBJSONCoubMapper.h" +#import "CBCoubNew.h" +#import "NSDictionary+CBExtensions.h" +#import "CBCoubVideoSource.h" +#import "CBCoubAudioSource.h" +#import "CBTagNew.h" + + + +@implementation CBJSONCoubMapper +{ + +} + ++ (CBCoubNew *)updateCoubFromCoub:(CBCoubNew *)newCoub coub:(CBCoubNew *)coub +{ + coub.title = newCoub.title; + coub.liked = newCoub.liked; + coub.visibility = newCoub.visibility; + coub.recoubed = newCoub.recoubed; + coub.likeCount = newCoub.likeCount; + coub.recoubCount = newCoub.recoubCount; + coub.cotd = newCoub.cotd; + coub.flagged = newCoub.flagged; + coub.creationDate = coub.creationDate; + coub.creationDateAsString = coub.creationDateAsString; + +// if(newCoub.isDone) +// coub.state = CBCoubDraftCompleted; + + return coub; +} + ++ (CBCoubNew *)updateCoubFromJSONObject:(NSDictionary *)attributes coub:(CBCoubNew *)coub +{ + static NSDictionary *kCoubJSONKeys = nil; + if(!kCoubJSONKeys) + { + kCoubJSONKeys = @{ + //@"permalink" : @"permalink", + @"title" : @"title", + @"visibility_type" : @"visibility", + //@"created_at" : @"creationDateAsString", + //@"external_download" : @"externalDownloadAsDictionary", + @"like" : @"liked", + @"recoub" : @"recoubed", + @"likes_count" : @"likeCount", + @"views_count" : @"viewCount", + @"recoubs_count" : @"recoubCount", + @"cotd" : @"cotd", + @"flag" : @"flagged", + }; + } + + if ([attributes[@"title"] isKindOfClass:[NSString class]]) + { + NSString *title = attributes[@"title"]; + coub.title = title; + } + + NSDictionary *recoubInfo = nil; + + if(attributes[@"permalink"]) + coub.permalink = attributes[@"permalink"]; + + if(attributes[@"id"]) + coub.coubID = [attributes[@"id"] stringValue]; + + coub.originalPermalink = attributes[@"permalink"]; + coub.originalCreationDateAsString = attributes[@"created_at"]; + + if (coub.author == nil) + { + coub.author = [CBCoubAuthorVO coubAuthorWithAttributes:attributes[@"user"]]; + //coub.authorCD = [CBUserNewuserWithAttributes:attributes[@"user"]]; + } + + if (coub.author.name.length == 0) + { + NSDictionary *channel = attributes[@"channel"]; + if ([channel respondsToSelector:@selector(objectForKey:)]) + coub.author.name = channel[@"title"]; + } + + if(attributes[@"is_done"]) + coub.isDone = [attributes[@"is_done"] boolValue]; + + NSDictionary *fileVersions = attributes[@"file_versions"]; + NSString *remoteVideoLocation = attributes[@"explicitVideoLocation"]; + if (remoteVideoLocation) + { + remoteVideoLocation = [remoteVideoLocation stringByReplacingOccurrencesOfString:@"muted_mp4_med_" withString:@"mp4_med_"]; + remoteVideoLocation = [remoteVideoLocation stringByReplacingOccurrencesOfString:@"_muted_med" withString:@"_med"]; + coub.remoteVideoLocation = remoteVideoLocation; + } + else + { + if (fileVersions) + remoteVideoLocation = fileVersions[@"iphone"][@"url"]; + if(!remoteVideoLocation) + remoteVideoLocation = attributes[@"file"]; + if(!remoteVideoLocation || [remoteVideoLocation isEqual:[NSNull null]]) + remoteVideoLocation = fileVersions[@"html5"][@"video"][@"med"][@"url"]; + + if (remoteVideoLocation) + { + if ([remoteVideoLocation rangeOfString:@"muted_mp4"].location != NSNotFound) + { + remoteVideoLocation = [remoteVideoLocation stringByReplacingOccurrencesOfString:@"muted_mp4_med_" withString:@"mp4_med_"]; + remoteVideoLocation = [remoteVideoLocation stringByReplacingOccurrencesOfString:@"_muted_med" withString:@"_med"]; + coub.remoteVideoLocation = remoteVideoLocation; + } + else + { + NSRange r1 = [remoteVideoLocation rangeOfString:@"iphone_"]; + NSRange r2 = [remoteVideoLocation rangeOfString:@"_iphone"]; + + if (r1.location == NSNotFound) + { + r1 = [remoteVideoLocation rangeOfString:@"gifv_"]; + r2 = [remoteVideoLocation rangeOfString:@"_gifv"]; + } + + if (r1.location != NSNotFound) + { + NSInteger loc = r1.length+r1.location; + NSString *someVideoMetadataString = [remoteVideoLocation substringWithRange:NSMakeRange(loc, r2.location - loc)]; + + remoteVideoLocation = fileVersions[@"web"][@"template"]; + NSRange r3 = [remoteVideoLocation rangeOfString:@"%{"]; + + remoteVideoLocation = [remoteVideoLocation substringToIndex:r3.location]; + remoteVideoLocation = [NSString stringWithFormat:@"%@mp4_med_size_%@_med.mp4", remoteVideoLocation, someVideoMetadataString]; + coub.remoteVideoLocation = remoteVideoLocation; + } + else if ([remoteVideoLocation rangeOfString:@"mp4_med_size_"].location != NSNotFound) + { + coub.remoteVideoLocation = remoteVideoLocation; + } + } + } + } + + NSString *remoteAudioLocation = [attributes[@"audio_versions"] coubURIFromVersionTemplateWithPreferredSubstitutions:@[@"low", @"mid", @"high"]]; + if(!remoteAudioLocation) + remoteAudioLocation = attributes[@"audio_file_url"]; + if(!remoteAudioLocation || [remoteAudioLocation isEqual:[NSNull null]]) + remoteAudioLocation = fileVersions[@"mobile"][@"mp3"]; + if(remoteAudioLocation) + coub.remoteAudioLocation = remoteAudioLocation; + + NSDictionary *chunks = attributes[@"audio_versions"][@"chunks"]; + + if(chunks) + { + NSString *audioTemplate = chunks[@"template"]; + audioTemplate = [audioTemplate stringByReplacingOccurrencesOfString:@"%{version}" withString:@"low"]; + audioTemplate = [audioTemplate stringByReplacingOccurrencesOfString:@"%{chunk}" withString:@"%i"]; + coub.remoteAudioLocationPattern = audioTemplate; + } + + // big, med, small, ios_large + //KALog(@"ff=%@", [attributes[@"first_frame_versions"][@"versions"] componentsJoinedByString: @", "]); + NSString *largePictureLocation = [attributes[@"first_frame_versions"] coubURIFromVersionTemplateWithPreferredSubstitutions:@[@"med", @"big", @"ios_large"]]; + if(largePictureLocation) + coub.largePicture = largePictureLocation; + + // micro, tiny, age_restricted, ios_large, ios_mosaic, big, med, small + NSString *mediumPictureLocation = [attributes[@"image_versions"] coubURIFromVersionTemplateWithPreferredSubstitutions:@[@"ios_mosaic", @"small", @"med"]]; + if(!mediumPictureLocation) + mediumPictureLocation = attributes[@"picture"]; // short JSON + if(mediumPictureLocation) + coub.mediumPicture = mediumPictureLocation; + + // Determine the type of audio based on the values of "has_sound" and "audio_file_url" + if(coub.remoteAudioLocation && ([coub.remoteAudioLocation isKindOfClass:[NSString class]] && [coub.remoteAudioLocation length] < 8)) + coub.remoteAudioLocation = nil; + + if(attributes[@"visibility_type"]) + { + coub.audioType = coub.remoteAudioLocationPattern ? CBCoubAudioTypeExternal : ([attributes[@"has_sound"] boolValue] ? CBCoubAudioTypeInternal : CBCoubAudioTypeNone); + } + // else a short JSON doesn't have any information on sound + + NSDictionary *mediaBlock = attributes[@"media_blocks"]; + + BOOL isCoubAudioSourceAvailable = mediaBlock[@"audio_track"] != nil; + BOOL isCoubVideoSourceAvailable = mediaBlock[@"external_video"] != nil; + + if(isCoubVideoSourceAvailable) + coub.videoSource = [CBCoubVideoSource sourceFromData:mediaBlock[@"external_video"]]; + if(isCoubAudioSourceAvailable) + coub.audioSource = [CBCoubAudioSource sourceFromData:mediaBlock[@"audio_track"]]; + + if(coub.audioSource || coub.videoSource) + coub.isCoubSourcesAvailable = YES; + + NSDictionary *externalDownloadInfo = attributes[@"external_download"]; + if([externalDownloadInfo isKindOfClass:[NSDictionary class]]) + { + coub.externalDownloadType = externalDownloadInfo[@"type"]; + coub.externalDownloadSource = externalDownloadInfo[@"url"]; + }else + { + // Coub API may return a boolean "false" as "external_download" value + coub.externalDownloadType = nil; + coub.externalDownloadSource = nil; + } + + NSArray *tags = attributes[@"tags"]; + if(tags) + { + NSMutableArray *mutableArray = [NSMutableArray arrayWithCapacity:tags.count]; + [tags enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) + { + [mutableArray addObject:[CBTagNew tagWithAttributes:obj]]; + }]; + + coub.tags = [NSArray arrayWithArray:mutableArray]; + } + + + + if(recoubInfo) + { + coub.recouber = [CBCoubAuthorVO coubAuthorWithAttributes:recoubInfo[@"user"]]; + coub.liked = [recoubInfo[@"like"] boolValue]; + coub.recoubed = [recoubInfo[@"recoub"] boolValue]; + coub.flagged = [recoubInfo[@"flagged"] boolValue]; + } + + if(!coub.coubID) + [NSException raise:NSInternalInconsistencyException format:@"No coub id found in %@", attributes]; + if(!coub.permalink) + [NSException raise:NSInternalInconsistencyException format:@"No coub permalink found in %@", attributes]; + + NSArray *size = attributes[@"dimensions"][@"small"]; + + //coub.naturalVideoSize = CGSizeMake([size[0] floatValue], [size[1] floatValue]); + return coub; +} + ++ (CBCoubNew *)coubFromJSONObject:(NSDictionary *)jsonObj +{ + return [self updateCoubFromJSONObject:jsonObj coub:[CBCoubNew new]]; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/CBLibrary.h b/submodules/LegacyComponents/LegacyComponents/CBLibrary.h new file mode 100755 index 0000000000..bc708069dd --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/CBLibrary.h @@ -0,0 +1,36 @@ +// +// CBLibrary.h +// Coub +// +// Created by Konstantin Anoshkin on 26.06.12. +// Copyright 2012 Coub. All rights reserved. +// + +#import +#import "CBCoubAsset.h" + +#if TARGET_IPHONE_SIMULATOR +// On iPhone Simulator NSTemporaryDirectory() returns a Mac OS X temporary directory which is outside our application sandbox. +// For consistency's sake we want to make it behave as on an honest-to-goodness iPhone device. +NSString *CBTemporaryDirectory (void); +#define NSTemporaryDirectory() CBTemporaryDirectory() +#endif + +NSString *CBDocumentsDirectory (void); +NSString *CBCachesDirectory (void); + +@interface CBLibrary : NSObject + ++ (CBLibrary *)sharedLibrary; + +- (void)markCoubAsset:(id)coub asDownloaded:(BOOL)downloaded; +- (BOOL)isCoubDownloadedByPermalink:(NSString *)permalink; + +- (void)markCoubChunk:(id)coub idx:(NSInteger)idx asDownloaded:(BOOL)downloaded; +- (BOOL)isCoubChunkDownloadedByPermalink:(NSString *)permalink idx:(NSInteger)idx; + +- (void)cleanUpMediaCache; + +@property (strong, nonatomic) NSURL *mediaDirectory; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/CBLibrary.m b/submodules/LegacyComponents/LegacyComponents/CBLibrary.m new file mode 100755 index 0000000000..609349f176 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/CBLibrary.m @@ -0,0 +1,236 @@ +// +// CBLibrary.m +// Coub +// +// Created by Konstantin Anoshkin on 26.06.12. +// Copyright 2012 Coub. All rights reserved. +// + +#define CBLIBRARY_IMPLEMENTATION_FILE + +#import "CBLibrary.h" +#import + + +#if TARGET_IPHONE_SIMULATOR +NSString *CBTemporaryDirectory (void) +{ + return [NSHomeDirectory() stringByAppendingPathComponent: @"tmp"]; +} +#endif + + +NSString *CBDocumentsDirectory (void) +{ + static NSString *sPath = nil; + if (!sPath) + sPath = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject] copy]; + return sPath; +} + + +NSString *CBCachesDirectory (void) +{ + static NSString *sPath = nil; + if (!sPath) { + sPath = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject] copy]; + if (![[NSFileManager defaultManager] fileExistsAtPath: sPath]) + [[NSFileManager defaultManager] createDirectoryAtPath: sPath withIntermediateDirectories: YES attributes: nil error: NULL]; + } + return sPath; +} + + +NSString *CBMediaDirectory (void) +{ + static NSString *sPath = nil; + if (!sPath) { + sPath = [[[NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent: @"Media"] copy]; + } + return sPath; +} + +NSString *const kCBLibraryDatabaseFileNameExtension = @"sqlite"; +NSString *const kCBLibraryCurrentUserID = @"currentUserID"; + + +@implementation CBLibrary +{ +@private + NSURL *_mediaDirectory; + NSMutableDictionary *_cachedMediaFiles; +} + + ++ (CBLibrary *)sharedLibrary +{ + static id sSharedInstance = nil; + static dispatch_once_t onceToken = 0; + dispatch_once(&onceToken, ^ + { + sSharedInstance = [[self alloc] init]; + }); + return sSharedInstance; +} + + +- (id)init +{ + self = [super init]; + + if(self) + { + _cachedMediaFiles = [NSMutableDictionary new]; + } + + return self; +} + +#pragma mark - Paths & URLs + +- (void)setMediaDirectory:(NSURL *)mediaDirectory +{ + if (_mediaDirectory && ![mediaDirectory.absoluteString isEqualToString:_mediaDirectory.absoluteString]) { + [self cleanUpMediaCache]; + } + + if (mediaDirectory && ![mediaDirectory.absoluteString isEqualToString:_mediaDirectory.absoluteString]) { + _mediaDirectory = mediaDirectory; + [self createMediaDirectory]; + } else { + _mediaDirectory = mediaDirectory; + } +} + +- (NSURL *)mediaDirectory +{ + if(!_mediaDirectory) { + _mediaDirectory = [NSURL fileURLWithPath:CBMediaDirectory() isDirectory:YES]; + [self createMediaDirectory]; + } + return _mediaDirectory; +} + +- (void)createMediaDirectory +{ + NSError *error = nil; + NSString *mediaDirectoryPath = [[self mediaDirectory] path]; + if(![[NSFileManager defaultManager] fileExistsAtPath:mediaDirectoryPath]) + { + if(![[NSFileManager defaultManager] createDirectoryAtPath:mediaDirectoryPath withIntermediateDirectories:YES attributes:nil error:&error]) + { + NSLog(@"*** Could not recreate Media directory, %@", error); + return; + } + + if(&NSURLIsExcludedFromBackupKey) + { + if(![[self mediaDirectory] setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:&error]) + NSLog(@"*** Failed to set NSURLIsExcludedFromBackupKey for %@, %@", [self mediaDirectory], error); + }else + { + // Set the Do Not Backup extended attribute, http://developer.apple.com/library/ios/#qa/qa1719/_index.html + + + u_int8_t attrValue = 1; + if(setxattr([mediaDirectoryPath fileSystemRepresentation], "com.apple.MobileBackup", &attrValue, sizeof(attrValue), 0, 0)) + { + //KAObjectLogError(@"setxattr(%@) failed: %s (%d)", mediaDirectoryPath, strerror(errno), errno); + } + } + } + + + +} + +- (void)markCoubAsset:(id)coub asDownloaded:(BOOL)downloaded +{ + if(downloaded) + { + if([self isCoubDownloadedByPermalink:coub.assetId]) + return; + + NSError *error = nil; + NSDictionary *attrs = [[NSFileManager defaultManager] attributesOfItemAtPath:[coub.localVideoFileURL path] error:&error]; + if(attrs) + { + _cachedMediaFiles[coub.assetId] = [NSMutableDictionary dictionaryWithDictionary:@{@"coub" : coub, @"downloadedChunks": @0}]; + }else + { + //KAObjectLogError(@"Can't get attributes at %@: %@", coub.localVideoFileURL, error); + } + }else + { + [_cachedMediaFiles removeObjectForKey:coub.assetId]; + } +} + +- (BOOL)isCoubDownloadedByPermalink:(NSString *)permalink +{ + return _cachedMediaFiles[permalink] != nil; +} + +- (void)markCoubChunk:(id)coub idx:(NSInteger)idx asDownloaded:(BOOL)downloaded +{ + //NSLog(@"markCoubChunk %i", idx); + + NSMutableDictionary *mediaFile = _cachedMediaFiles[coub.assetId]; + + if(mediaFile) + { + NSInteger downloadedChunks = ((NSNumber *)mediaFile[@"downloadedChunks"]).integerValue; + NSInteger normalIdx = 1< + +@interface CBPlayerLayerView : UIView + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/CBPlayerLayerView.m b/submodules/LegacyComponents/LegacyComponents/CBPlayerLayerView.m new file mode 100755 index 0000000000..9e25cc8573 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/CBPlayerLayerView.m @@ -0,0 +1,20 @@ +// +// CBPlayerLayerView.m +// Coub +// +// Created by Tikhonenko Pavel on 23/11/2013. +// Copyright (c) 2013 Coub. All rights reserved. +// + +#import "CBPlayerLayerView.h" + +#import + +@implementation CBPlayerLayerView + ++ (Class)layerClass +{ + return [AVPlayerLayer class]; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/CBPlayerView.h b/submodules/LegacyComponents/LegacyComponents/CBPlayerView.h new file mode 100755 index 0000000000..06350aee73 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/CBPlayerView.h @@ -0,0 +1,27 @@ +// +// CBPlayerView.h +// CoubPlayer +// +// Created by Pavel Tikhonenko on 17/10/14. +// Copyright (c) 2014 Pavel Tikhonenko. All rights reserved. +// + +#import + +#import + +#import "CBCoubPlayer.h" +#import "CBPlayerLayerView.h" + +@interface CBPlayerView : UIView + +@property (nonatomic, readonly) UIImageView *preview; +@property (nonatomic, readonly) CBPlayerLayerView *videoPlayerView; + +- (void)play; +- (void)stop; + +@end + +//@interface CBPlayerLayerView : UIView +//@end \ No newline at end of file diff --git a/submodules/LegacyComponents/LegacyComponents/CBPlayerView.m b/submodules/LegacyComponents/LegacyComponents/CBPlayerView.m new file mode 100755 index 0000000000..9bda935f06 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/CBPlayerView.m @@ -0,0 +1,101 @@ +// +// CBPlayerView.m +// CoubPlayer +// +// Created by Pavel Tikhonenko on 17/10/14. +// Copyright (c) 2014 Pavel Tikhonenko. All rights reserved. +// + +#import "CBPlayerView.h" + + + +@implementation CBPlayerView + +- (id)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + + if(self) + { + [self setupView]; + } + + return self; +} +- (void)setupView +{ + self.clipsToBounds = YES; + + _preview = [[UIImageView alloc] initWithFrame:self.bounds]; + _preview.contentMode = UIViewContentModeScaleAspectFill; + [self addSubview:_preview]; + + _videoPlayerView = [[CBPlayerLayerView alloc] initWithFrame:self.bounds]; + _videoPlayerView.hidden = YES; + ((AVPlayerLayer *) _videoPlayerView.layer).videoGravity = AVLayerVideoGravityResizeAspectFill; + [self addSubview:_videoPlayerView]; +} + +- (void)setContentMode:(UIViewContentMode)contentMode +{ + [super setContentMode:contentMode]; + + _preview.contentMode = contentMode; + ((AVPlayerLayer *)_videoPlayerView.layer).videoGravity = (contentMode == UIViewContentModeScaleAspectFit) ? AVLayerVideoGravityResizeAspect : AVLayerVideoGravityResizeAspectFill; +} + +- (void)layoutSubviews +{ + [super layoutSubviews]; + + CGRect bounds = self.bounds; + + _videoPlayerView.frame = bounds; + _preview.frame = bounds; +} + +- (void)play +{ + _videoPlayerView.hidden = NO; +} + +- (void)stop +{ + _videoPlayerView.hidden = YES; +} + +//- (void) layoutSubviews +//{ +// // We need manual aspect fill/fit layout if we want fluid frame animations +// CGRect bounds = self.bounds; +// //_preview.frame = bounds; +// +// if (self.contentMode == UIViewContentModeScaleAspectFit) { +// CGRect viewFrame = bounds; +// +// CGSize size = _player.loop.videoTrackSize; +// if (size.width && size.height) { +// CGFloat scale = fminf(viewFrame.size.width / size.width, viewFrame.size.height / size.height); +// size.width = roundf(size.width * scale); +// size.height = roundf(size.height * scale); +// viewFrame = CGRectInset(viewFrame, 0.5f * (viewFrame.size.width - size.width), 0.5f * (viewFrame.size.height - size.height)); +// } +// +// _videoPlayerView.frame = viewFrame; +// } else +// _videoPlayerView.frame = bounds; +// +// CGPoint center = (CGPoint) { CGRectGetMidX(bounds), CGRectGetMidY(bounds) }; +// _spinner.center = center; +// _reloadButton.center = center; +//} + +@end + +//#pragma mark - +//#pragma mark CBPlayerLayerView +// +//@implementation CBPlayerLayerView +//+ (Class)layerClass { return [AVPlayerLayer class]; } +//@end diff --git a/submodules/LegacyComponents/LegacyComponents/CBTagNew.h b/submodules/LegacyComponents/LegacyComponents/CBTagNew.h new file mode 100755 index 0000000000..6052d00c11 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/CBTagNew.h @@ -0,0 +1,18 @@ +// +// Created by Tikhonenko Pavel on 30/11/2013. +// Copyright (c) 2013 Coub. All rights reserved. +// + + +#import + + +@interface CBTagNew : NSObject +@property (nonatomic, strong) NSNumber *tagId; +@property (nonatomic, strong) NSString *title; +@property (nonatomic, strong) NSString *value; +@property (nonatomic, readonly) NSString *hashTag; + ++ (instancetype)tagWithAttributes:(NSDictionary *)attributes; + +@end \ No newline at end of file diff --git a/submodules/LegacyComponents/LegacyComponents/CBTagNew.m b/submodules/LegacyComponents/LegacyComponents/CBTagNew.m new file mode 100755 index 0000000000..7229d24414 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/CBTagNew.m @@ -0,0 +1,29 @@ +// +// Created by Tikhonenko Pavel on 30/11/2013. +// Copyright (c) 2013 Coub. All rights reserved. +// + + +#import "CBTagNew.h" + + +@implementation CBTagNew +{ + +} + +- (NSString *)hashTag +{ + return [@"#" stringByAppendingString:_title]; +} + ++ (instancetype)tagWithAttributes:(NSDictionary *)attributes +{ + CBTagNew *tag = [CBTagNew new]; + tag.tagId = attributes[@"id"]; + tag.title = attributes[@"title"]; + tag.value = attributes[@"value"]; + return tag; +} + +@end \ No newline at end of file diff --git a/submodules/LegacyComponents/LegacyComponents/CBVideoPlayer.h b/submodules/LegacyComponents/LegacyComponents/CBVideoPlayer.h new file mode 100755 index 0000000000..7bc9cb7f0b --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/CBVideoPlayer.h @@ -0,0 +1,36 @@ +// +// CBVideoPlayer.h +// Coub +// +// Created by Pavel Tikhonenko on 12/08/14. +// Copyright (c) 2014 Coub. All rights reserved. +// + +#import +#import + +typedef enum +{ + CBVideoPlayerStatusInited, + CBVideoPlayerStatusPrepairing = 1, + CBVideoPlayerStatusReadyToPlay = 1 << 1, + CBVideoPlayerStatusFailed = 1 << 2, + CBVideoPlayerStatusUnknown = NSNotFound + +} +CBVideoPlayerStatus; + +@interface CBVideoPlayer : NSObject + +@property (nonatomic, assign) CBVideoPlayerStatus status; + +- (id)initWithVideoLayer:(AVPlayerLayer *)layer; + +- (void)prepareWithAVAsset:(AVAsset *)asset completion:(void (^)(NSError *error))completion; +- (void)stopPrepairing; + +- (void)play; +- (void)pause; +- (void)stop; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/CBVideoPlayer.m b/submodules/LegacyComponents/LegacyComponents/CBVideoPlayer.m new file mode 100755 index 0000000000..adae6c2cab --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/CBVideoPlayer.m @@ -0,0 +1,193 @@ +// +// CBVideoPlayer.m +// Coub +// +// Created by Pavel Tikhonenko on 12/08/14. +// Copyright (c) 2014 Coub. All rights reserved. +// + +#import "CBVideoPlayer.h" + +static void *kPlayerItemContext = (void *) 1; +static void *kPlayerStatusContext = (void *) 2; + +static void *kPlayerLayerReadyToDisplayContext = (void *) 4; + +@interface CBVideoPlayer () + +@property (nonatomic, strong) AVPlayerLayer *layer; +@property (nonatomic, strong) AVPlayer *videoPlayer; +@property (nonatomic, strong) AVPlayerItem *nextItem; +@property (nonatomic, assign) BOOL hasBeenReseted; + +@property (nonatomic, copy) void (^prepairingCompletion)(NSError *error); + +@end + +@implementation CBVideoPlayer + +- (id)initWithVideoLayer:(AVPlayerLayer *)layer +{ + self = [super init]; + + if(self) + { + self.status = CBVideoPlayerStatusUnknown; + self.hasBeenReseted = YES; + + self.layer = layer; + + [self createVideoPlayer]; + + + self.status = CBVideoPlayerStatusInited; + } + + return self; +} + +#pragma mark - +#pragma mark Public methods + +- (void)prepareWithAVAsset:(AVAsset *)asset completion:(void (^)(NSError *error))completion +{ + self.prepairingCompletion = completion; + + self.status = CBVideoPlayerStatusPrepairing; + + if(!_hasBeenReseted) + [self resetPlayer]; + + _hasBeenReseted = NO; + + [self prepareVideoPlayer]; + [self prepareVideoLayer]; + + AVPlayerItem *item = [AVPlayerItem playerItemWithAsset:asset]; + [self.videoPlayer replaceCurrentItemWithPlayerItem:item]; + +} + +- (void)play +{ + [_videoPlayer play]; +} + +- (void)pause +{ + _videoPlayer.rate = 0; +} + +- (void)stop +{ + _videoPlayer.rate = 0; + + [self resetPlayer]; + [self.videoPlayer replaceCurrentItemWithPlayerItem:nil]; +} + +- (void)stopPrepairing +{ +} + +#pragma mark - +#pragma mark Private methods + +- (void)createVideoPlayer +{ + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(restartAVPlayer:) name:AVPlayerItemDidPlayToEndTimeNotification object:nil]; + + self.videoPlayer = [AVPlayer new]; + _videoPlayer.actionAtItemEnd = AVPlayerActionAtItemEndNone; + +} + +- (void)prepareVideoPlayer +{ + [_videoPlayer addObserver:self forKeyPath:@"currentItem" options:NSKeyValueObservingOptionNew context:kPlayerItemContext]; + [_videoPlayer addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:kPlayerStatusContext]; +} + +- (void)prepareVideoLayer +{ + [_layer addObserver:self forKeyPath:@"readyForDisplay" options:NSKeyValueObservingOptionNew context:kPlayerLayerReadyToDisplayContext]; +} + +- (void)resetPlayer +{ + @try{ + [_videoPlayer removeObserver:self forKeyPath:@"currentItem" context:kPlayerItemContext]; + }@catch(id anException){} + + @try{ + [_videoPlayer removeObserver:self forKeyPath:@"status" context:kPlayerStatusContext]; + }@catch(id anException){} + + @try{ + [_layer removeObserver:self forKeyPath:@"readyForDisplay" context:kPlayerLayerReadyToDisplayContext]; + }@catch(id anException){} +} + +- (void)prerollPlayer +{ + [_videoPlayer prerollAtRate:1 completionHandler:^(BOOL finished) { + [_layer setPlayer:self.videoPlayer]; + }]; +} +- (void)completeVideoPrepairing +{ + [self resetPlayer]; + self.prepairingCompletion(nil); +} + +- (void)restartAVPlayer:(NSNotification *)notification +{ + AVPlayerItem *playerItem = [notification object]; + if (playerItem == _videoPlayer.currentItem) + [_videoPlayer seekToTime:kCMTimeZero]; +} + +#pragma mark - +#pragma mark Observe value + +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context +{ + AVPlayer *player = (AVPlayer *) object; + + if(context == kPlayerLayerReadyToDisplayContext) + { + [self completeVideoPrepairing]; + } + + if(context == kPlayerStatusContext || context == kPlayerItemContext) + { + //KAObjectLog(@"%@ status: %i", player == self.videoPlayer ? @"videoPlayer" : @"audioPlayer", player.status); + + NSLog(@"status: %i", player.status); + if(context == kPlayerItemContext && player.currentItem == nil && _nextItem) + return; + + if(context == kPlayerItemContext && player.currentItem == nil) + return; + + if(player.status == AVPlayerStatusReadyToPlay) + { + [self prerollPlayer]; + }else if(player.status == AVPlayerStatusFailed) + { + //[self.delegate playerInitialzeProccess:self completeWithError:[NSError errorWithDomain:@"com.coub.player" code:99 userInfo:nil]]; + + }//else if(player.status == AVPlayerStatusUnknown) + //KAObjectLog(@"%@ AVPlayerStatusUnknown: %@", player == self.videoPlayer ? @"videoPlayer" : @"audioPlayer", player.error); + } +} + +- (void)dealloc +{ + @try{ + [[NSNotificationCenter defaultCenter] removeObserver:self name:AVPlayerItemDidPlayToEndTimeNotification object:nil]; + }@catch(id anException){} + + [self resetPlayer]; +} +@end diff --git a/submodules/LegacyComponents/LegacyComponents/FLAnimatedImage.h b/submodules/LegacyComponents/LegacyComponents/FLAnimatedImage.h new file mode 100644 index 0000000000..657eeec99f --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/FLAnimatedImage.h @@ -0,0 +1,76 @@ +// +// FLAnimatedImage.h +// Flipboard +// +// Created by Raphael Schaad on 7/8/13. +// Copyright (c) 2013-2014 Flipboard. All rights reserved. +// + + +#import +#import + +@protocol FLAnimatedImageDebugDelegate; + + +// +// An `FLAnimatedImage`'s job is to deliver frames in a highly performant way and works in conjunction with `FLAnimatedImageView`. +// It subclasses `NSObject` and not `UIImage` because it's only an "image" in the sense that a sea lion is a lion. +// It tries to intelligently choose the frame cache size depending on the image and memory situation with the goal to lower CPU usage for smaller ones, lower memory usage for larger ones and always deliver frames for high performant play-back. +// Note: `posterImage`, `size`, `loopCount`, `delayTimes` and `frameCount` don't change after successful initialization. +// +@interface FLAnimatedImage : NSObject + +@property (nonatomic, strong) UIImage *(^imageDrawingBlock)(UIImage *); + +@property (nonatomic, strong, readonly) UIImage *posterImage; // Guaranteed to be loaded; usually equivalent to `-imageLazilyCachedAtIndex:0` +@property (nonatomic, assign, readonly) CGSize size; // The `.posterImage`'s `.size` + +@property (nonatomic, assign, readonly) NSUInteger loopCount; // 0 means repeating the animation indefinitely +@property (nonatomic, strong, readonly) NSArray *delayTimes; // Of type `NSTimeInterval` boxed in `NSNumber`s +@property (nonatomic, assign, readonly) NSUInteger frameCount; // Number of valid frames; equal to `[.delayTimes count]` + +@property (nonatomic, assign, readonly) NSUInteger frameCacheSizeCurrent; // Current size of intelligently chosen buffer window; can range in the interval [1..frameCount] +@property (nonatomic, assign) NSUInteger frameCacheSizeMax; // Allow to cap the cache size; 0 means no specific limit (default) + +// Intended to be called from main thread synchronously; will return immediately. +// If the result isn't cached, will return `nil`; the caller should then pause playback, not increment frame counter and keep polling. +// After an initial loading time, depending on `frameCacheSize`, frames should be available immediately from the cache. +- (UIImage *)imageLazilyCachedAtIndex:(NSUInteger)index; + +// Pass either a `UIImage` or an `FLAnimatedImage` and get back its size ++ (CGSize)sizeForImage:(id)image; + +// Designated initializer +// On success, returns a new `FLAnimatedImage` with all fields populated, on failure returns `nil` and an error will be logged. +- (instancetype)initWithAnimatedGIFData:(NSData *)data imageDrawingBlock:(UIImage *(^)(UIImage *))imageDrawingBlock; + +@property (nonatomic, strong, readonly) NSData *data; // The data the receiver was initialized with; read-only + +#if DEBUG +// Only intended to report internal state for debugging +@property (nonatomic, weak) id debug_delegate; +@property (nonatomic, strong) NSMutableDictionary *debug_info; // To track arbitrary data (e.g. original URL, loading durations, cache hits, etc.) +#endif + +@end + + +@interface FLWeakProxy : NSProxy + ++ (instancetype)weakProxyForObject:(id)targetObject; + +@end + + +#if DEBUG +@protocol FLAnimatedImageDebugDelegate + +@optional + +- (void)debug_animatedImage:(FLAnimatedImage *)animatedImage didUpdateCachedFrames:(NSIndexSet *)indexesOfFramesInCache; +- (void)debug_animatedImage:(FLAnimatedImage *)animatedImage didRequestCachedFrame:(NSUInteger)index; +- (CGFloat)debug_animatedImagePredrawingSlowdownFactor:(FLAnimatedImage *)animatedImage; + +@end +#endif diff --git a/submodules/LegacyComponents/LegacyComponents/FLAnimatedImage.m b/submodules/LegacyComponents/LegacyComponents/FLAnimatedImage.m new file mode 100644 index 0000000000..dd3014c232 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/FLAnimatedImage.m @@ -0,0 +1,740 @@ +// +// FLAnimatedImage.m +// Flipboard +// +// Created by Raphael Schaad on 7/8/13. +// Copyright (c) 2013-2014 Flipboard. All rights reserved. +// + + +#import "FLAnimatedImage.h" +#import +#import +#import + + +// From vm_param.h, define for iOS 8.0 or higher to build on device. +#ifndef BYTE_SIZE +#define BYTE_SIZE 8 // byte size in bits +#endif + +#define MEGABYTE (1024 * 1024) + + +// An animated image's data size (dimensions * frameCount) category; its value is the max allowed memory (in MB). +// E.g.: A 100x200px GIF with 30 frames is ~2.3MB in our pixel format and would fall into the `FLAnimatedImageDataSizeCategoryAll` category. +typedef NS_ENUM(NSUInteger, FLAnimatedImageDataSizeCategory) { + FLAnimatedImageDataSizeCategoryAll = 10, // All frames permanently in memory (be nice to the CPU) + FLAnimatedImageDataSizeCategoryDefault = 75, // A frame cache of default size in memory (usually real-time performance and keeping low memory profile) + FLAnimatedImageDataSizeCategoryOnDemand = 250, // Only keep one frame at the time in memory (easier on memory, slowest performance) + FLAnimatedImageDataSizeCategoryUnsupported // Even for one frame too large, computer says no. +}; + +typedef NS_ENUM(NSUInteger, FLAnimatedImageFrameCacheSize) { + FLAnimatedImageFrameCacheSizeNoLimit = 0, // 0 means no specific limit + FLAnimatedImageFrameCacheSizeLowMemory = 1, // The minimum frame cache size; this will produce frames on-demand. + FLAnimatedImageFrameCacheSizeGrowAfterMemoryWarning = 2, // If we can produce the frames faster than we consume, one frame ahead will already result in a stutter-free playback. + FLAnimatedImageFrameCacheSizeDefault = 5 // Build up a comfy buffer window to cope with CPU hiccups etc. +}; + + +@interface FLAnimatedImage () +{ + // Use old school ivar instead of property for retained non-object types (CF type, dispatch "object") to avoid ARC confusion: http://stackoverflow.com/questions/9684972/strong-property-with-attribute-nsobject-for-a-cf-type-doesnt-retain/9690656#9690656 + CGImageSourceRef _imageSource; + // Note: Only if the deployment target is iOS 6.0 or higher, dispatch objects are declared as "true objects" and participate in ARC, etc.; See or https://github.com/AFNetworking/AFNetworking/pull/517 for details. + dispatch_queue_t _serialQueue; +} + +@property (nonatomic, assign, readonly) NSUInteger frameCacheSizeOptimal; // The optimal number of frames to cache based on image size & number of frames; never changes +@property (nonatomic, assign) NSUInteger frameCacheSizeMaxInternal; // Allow to cap the cache size e.g. when memory warnings occur; 0 means no specific limit (default) +@property (nonatomic, assign) NSUInteger requestedFrameIndex; // Most recently requested frame index +@property (nonatomic, assign, readonly) NSUInteger posterImageFrameIndex; // Index of non-purgable poster image; never changes +@property (nonatomic, strong, readonly) NSMutableArray *cachedFrames; // Uncached frame indexes hold `NSNull` +@property (nonatomic, strong, readonly) NSMutableIndexSet *cachedFrameIndexes; // Indexes of cached frames +@property (nonatomic, strong, readonly) NSMutableIndexSet *requestedFrameIndexes; // Indexes of frames that are currently produced in the background +@property (nonatomic, strong, readonly) NSIndexSet *allFramesIndexSet; // Default index set with the full range of indexes; never changes +@property (nonatomic, assign) NSUInteger memoryWarningCount; + +// The weak proxy is used to break retain cycles with delayed actions from memory warnings. +// We are lying about the actual type here to gain static type checking and eliminate casts. +// The actual type of the object is `FLWeakProxy`. Lazily instantiated since it is not typically needed. +@property (nonatomic, strong, readonly) FLAnimatedImage *weakProxy; + +@end + + +@implementation FLAnimatedImage + +#pragma mark - Accessors +#pragma mark Public + +// This is the definite value the frame cache needs to size itself to. +- (NSUInteger)frameCacheSizeCurrent +{ + NSUInteger frameCacheSizeCurrent = self.frameCacheSizeOptimal; + + // If set, respect the caps. + if (self.frameCacheSizeMax > FLAnimatedImageFrameCacheSizeNoLimit) { + frameCacheSizeCurrent = MIN(frameCacheSizeCurrent, self.frameCacheSizeMax); + } + + if (self.frameCacheSizeMaxInternal > FLAnimatedImageFrameCacheSizeNoLimit) { + frameCacheSizeCurrent = MIN(frameCacheSizeCurrent, self.frameCacheSizeMaxInternal); + } + + return frameCacheSizeCurrent; +} + + +- (void)setFrameCacheSizeMax:(NSUInteger)frameCacheSizeMax +{ + if (_frameCacheSizeMax != frameCacheSizeMax) { + + // Remember whether the new cap will cause the current cache size to shrink; then we'll make sure to purge from the cache if needed. + BOOL willFrameCacheSizeShrink = (frameCacheSizeMax < self.frameCacheSizeCurrent); + + // Update the value + _frameCacheSizeMax = frameCacheSizeMax; + + if (willFrameCacheSizeShrink) { + [self purgeFrameCacheIfNeeded]; + } + } +} + + +#pragma mark Private + +- (void)setFrameCacheSizeMaxInternal:(NSUInteger)frameCacheSizeMaxInternal +{ + if (_frameCacheSizeMaxInternal != frameCacheSizeMaxInternal) { + + // Remember whether the new cap will cause the current cache size to shrink; then we'll make sure to purge from the cache if needed. + BOOL willFrameCacheSizeShrink = (frameCacheSizeMaxInternal < self.frameCacheSizeCurrent); + + // Update the value + _frameCacheSizeMaxInternal = frameCacheSizeMaxInternal; + + if (willFrameCacheSizeShrink) { + [self purgeFrameCacheIfNeeded]; + } + } +} + + +// Explicit synthesizing for `readonly` property with overridden getter. +@synthesize weakProxy = _weakProxy; + +- (FLAnimatedImage *)weakProxy +{ + if (!_weakProxy) { + _weakProxy = (id)[FLWeakProxy weakProxyForObject:self]; + } + + return _weakProxy; +} + + +#pragma mark - Life Cycle + +- (id)init +{ + NSLog(@"Error: Use `-initWithAnimatedGIFData:` and supply the animated GIF data as an argument to initialize an object of type `FLAnimatedImage`."); + return nil; +} + + +- (instancetype)initWithAnimatedGIFData:(NSData *)data imageDrawingBlock:(UIImage *(^)(UIImage *))imageDrawingBlock +{ + // Early return if no data supplied! + BOOL hasData = ([data length] > 0); + if (!hasData) { + NSLog(@"Error: No animated GIF data supplied."); + return nil; + } + + self = [super init]; + if (self) { + self.imageDrawingBlock = imageDrawingBlock; + // Do one-time initializations of `readonly` properties directly to ivar to prevent implicit actions and avoid need for private `readwrite` property overrides. + + // Keep a strong reference to `data` and expose it read-only publicly. + // However, we will use the `_imageSource` as handler to the image data throughout our life cycle. + _data = data; + + // Initialize internal data structures + // We'll fill in the initial `NSNull` values below, when we loop through all frames. + _cachedFrames = [[NSMutableArray alloc] init]; + _cachedFrameIndexes = [[NSMutableIndexSet alloc] init]; + _requestedFrameIndexes = [[NSMutableIndexSet alloc] init]; + + // Note: We could leverage `CGImageSourceCreateWithURL` too to add a second initializer `-initWithAnimatedGIFContentsOfURL:`. + _imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL); + // Early return on failure! + if (!_imageSource) { + NSLog(@"Error: Failed to `CGImageSourceCreateWithData` for animated GIF data %@", data); + return nil; + } + + // Early return if not GIF! + CFStringRef imageSourceContainerType = CGImageSourceGetType(_imageSource); + BOOL isGIFData = UTTypeConformsTo(imageSourceContainerType, kUTTypeGIF); + if (!isGIFData) { + NSLog(@"Error: Supplied data is of type %@ and doesn't seem to be GIF data %@", imageSourceContainerType, data); + return nil; + } + + // Get `LoopCount` + // Note: 0 means repeating the animation indefinitely. + // Image properties example: + // { + // FileSize = 314446; + // "{GIF}" = { + // HasGlobalColorMap = 1; + // LoopCount = 0; + // }; + // } + NSDictionary *imageProperties = (__bridge_transfer NSDictionary *)CGImageSourceCopyProperties(_imageSource, NULL); + _loopCount = [[[imageProperties objectForKey:(id)kCGImagePropertyGIFDictionary] objectForKey:(id)kCGImagePropertyGIFLoopCount] unsignedIntegerValue]; + + // Iterate through frame images + size_t imageCount = CGImageSourceGetCount(_imageSource); + NSMutableArray *delayTimesMutable = [NSMutableArray arrayWithCapacity:imageCount]; + for (size_t i = 0; i < imageCount; i++) { + CGImageRef frameImageRef = CGImageSourceCreateImageAtIndex(_imageSource, i, NULL); + if (frameImageRef) { + UIImage *frameImage = [UIImage imageWithCGImage:frameImageRef]; + // Check for valid `frameImage` before parsing its properties as frames can be corrupted (and `frameImage` even `nil` when `frameImageRef` was valid). + if (frameImage) { + // Set poster image + if (!self.posterImage) { + if (_imageDrawingBlock) + _posterImage = _imageDrawingBlock(frameImage); + else + _posterImage = frameImage; + // Set its size to proxy our size. + _size = _posterImage.size; + // Remember index of poster image so we never purge it; also add it to the cache. + _posterImageFrameIndex = i; + self.cachedFrames[self.posterImageFrameIndex] = self.posterImage; + [self.cachedFrameIndexes addIndex:self.posterImageFrameIndex]; + } else { + // Placeholder indicates that we don't have a cached frame. + // We use an array instead of a dictionary for slightly faster access. + self.cachedFrames[i] = [NSNull null]; + } + + // Get `DelayTime` + // Note: It's not in (1/100) of a second like still falsly described in the documentation as per iOS 7 but in seconds stored as `kCFNumberFloat32Type`. + // Frame properties example: + // { + // ColorModel = RGB; + // Depth = 8; + // PixelHeight = 960; + // PixelWidth = 640; + // "{GIF}" = { + // DelayTime = "0.4"; + // UnclampedDelayTime = "0.4"; + // }; + // } + + NSDictionary *frameProperties = (__bridge_transfer NSDictionary *)CGImageSourceCopyPropertiesAtIndex(_imageSource, i, NULL); + NSDictionary *framePropertiesGIF = [frameProperties objectForKey:(id)kCGImagePropertyGIFDictionary]; + + // Try to use the unclamped delay time; fall back to the normal delay time. + NSNumber *delayTime = [framePropertiesGIF objectForKey:(id)kCGImagePropertyGIFUnclampedDelayTime]; + if (!delayTime) { + delayTime = [framePropertiesGIF objectForKey:(id)kCGImagePropertyGIFDelayTime]; + } + // If we don't get a delay time from the properties, fall back to `kDelayTimeIntervalDefault` or carry over the preceding frame's value. + const NSTimeInterval kDelayTimeIntervalDefault = 0.1; + if (!delayTime) { + if (i == 0) { + NSLog(@"Verbose: Falling back to default delay time for first frame %@ because none found in GIF properties %@", frameImage, frameProperties); + delayTime = @(kDelayTimeIntervalDefault); + } else { + NSLog(@"Verbose: Falling back to preceding delay time for frame %zu %@ because none found in GIF properties %@", i, frameImage, frameProperties); + delayTime = delayTimesMutable[i - 1]; + } + } + // Support frame delays as low as `kDelayTimeIntervalMinimum`, with anything below being rounded up to `kDelayTimeIntervalDefault` for legacy compatibility. + // This is how the fastest browsers do it as per 2012: http://nullsleep.tumblr.com/post/16524517190/animated-gif-minimum-frame-delay-browser-compatibility + const NSTimeInterval kDelayTimeIntervalMinimum = 0.02; + // To support the minimum even when rounding errors occur, use an epsilon when comparing. We downcast to float because that's what we get for delayTime from ImageIO. + if ([delayTime floatValue] < ((float)kDelayTimeIntervalMinimum - FLT_EPSILON)) { + NSLog(@"Verbose: Rounding frame %zu's `delayTime` from %f up to default %f (minimum supported: %f).", i, [delayTime floatValue], kDelayTimeIntervalDefault, kDelayTimeIntervalMinimum); + delayTime = @(kDelayTimeIntervalDefault); + } + delayTimesMutable[i] = delayTime; + } else { + NSLog(@"Verbose: Dropping frame %zu because valid `CGImageRef` %@ did result in `nil`-`UIImage`.", i, frameImageRef); + } + CFRelease(frameImageRef); + } else { + NSLog(@"Verbose: Dropping frame %zu because failed to `CGImageSourceCreateImageAtIndex` with image source %@", i, _imageSource); + } + } + _delayTimes = [delayTimesMutable copy]; + _frameCount = [_delayTimes count]; + + if (self.frameCount == 0) { + NSLog(@"Error: Failed to create any valid frames for GIF with properties %@", imageProperties); + return nil; + } else if (self.frameCount == 1) { + // Warn when we only have a single frame but return a valid GIF. + NSLog(@"Verbose: Created valid GIF but with only a single frame. Image properties: %@", imageProperties); + } else { + // We have multiple frames, rock on! + } + + // Calculate the optimal frame cache size: try choosing a larger buffer window depending on the predicted image size. + // It's only dependent on the image size & number of frames and never changes. + CGFloat animatedImageDataSize = CGImageGetBytesPerRow(self.posterImage.CGImage) * self.size.height * self.frameCount / MEGABYTE; + if (animatedImageDataSize <= FLAnimatedImageDataSizeCategoryAll) { + _frameCacheSizeOptimal = self.frameCount; + } else if (animatedImageDataSize <= FLAnimatedImageDataSizeCategoryDefault) { + // This value doesn't depend on device memory much because if we're not keeping all frames in memory we will always be decoding 1 frame up ahead per 1 frame that gets played and at this point we might as well just keep a small buffer just large enough to keep from running out of frames. + _frameCacheSizeOptimal = FLAnimatedImageFrameCacheSizeDefault; + } else { + // The predicted size exceeds the limits to build up a cache and we go into low memory mode from the beginning. + _frameCacheSizeOptimal = FLAnimatedImageFrameCacheSizeLowMemory; + } + // In any case, cap the optimal cache size at the frame count. + _frameCacheSizeOptimal = MIN(_frameCacheSizeOptimal, self.frameCount); + + // Convenience/minor performance optimization; keep an index set handy with the full range to return in `-frameIndexesToCache`. + _allFramesIndexSet = [[NSIndexSet alloc] initWithIndexesInRange:NSMakeRange(0, self.frameCount)]; + + // System Memory Warnings Notification Handler + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; + } + return self; +} + + +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; + + if (_weakProxy) { + [NSObject cancelPreviousPerformRequestsWithTarget:_weakProxy]; + } + + if (_imageSource) { + CFRelease(_imageSource); + } + + // Needed for deployment target iOS 5.0 +#if ((__IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_6_0) || (!defined(__IPHONE_6_0))) + if (_serialQueue) { + dispatch_release(_serialQueue); + } +#endif +} + + +#pragma mark - Public Methods + +// See header for more details. +// Note: both consumer and producer are throttled: consumer by frame timings and producer by the available memory (max buffer window size). +- (UIImage *)imageLazilyCachedAtIndex:(NSUInteger)index +{ + // Early return if the requested index is beyond bounds. + // Note: We're comparing an index with a count and need to bail on greater than or equal to. + if (index >= self.frameCount) { + NSLog(@"Error: Skipping requested frame %lu beyond bounds (total frame count: %lu) for animated image: %@", (unsigned long)index, (unsigned long)self.frameCount, self); + return nil; + } + + // Remember requested frame index, this influences what we should cache next. + self.requestedFrameIndex = index; + + // Quick check to avoid doing any work if we already have all possible frames cached, a common case. + if ([self.cachedFrameIndexes count] < self.frameCount) { + // If we have frames that should be cached but aren't and aren't requested yet, request them. + // Exclude existing cached frames, frames already requested, and specially cached poster image. + NSMutableIndexSet *frameIndexesToAddToCacheMutable = [[self frameIndexesToCache] mutableCopy]; + [frameIndexesToAddToCacheMutable removeIndexes:self.cachedFrameIndexes]; + [frameIndexesToAddToCacheMutable removeIndexes:self.requestedFrameIndexes]; + [frameIndexesToAddToCacheMutable removeIndex:self.posterImageFrameIndex]; + NSIndexSet *frameIndexesToAddToCache = [frameIndexesToAddToCacheMutable copy]; + + // Asynchronously add frames to our cache. + if ([frameIndexesToAddToCache count] > 0) { + [self addFrameIndexesToCache:frameIndexesToAddToCache]; + } + } + + // Get the specified image. Watch out for `NSNull` placeholders. + UIImage *image = nil; + id tryImage = self.cachedFrames[index]; + if ([tryImage isKindOfClass:[UIImage class]]) { + image = tryImage; + } + + // Purge if needed based on the current playhead position. + [self purgeFrameCacheIfNeeded]; + + return image; +} + + +// Only called once from `-imageLazilyCachedAtIndex` but factored into its own method for logical grouping. +- (void)addFrameIndexesToCache:(NSIndexSet *)frameIndexesToAddToCache +{ + // Order matters. First, iterate over the indexes starting from the requested frame index. + // Then, if there are any indexes before the requested frame index, do those. + NSRange firstRange = NSMakeRange(self.requestedFrameIndex, self.frameCount - self.requestedFrameIndex); + NSRange secondRange = NSMakeRange(0, self.requestedFrameIndex); + if (firstRange.length + secondRange.length != self.frameCount) { + NSLog(@"Error: Two-part frame cache range doesn't equal full range."); + } + + // Add to the requested list before we actually kick them off, so they don't get into the queue twice. + [self.requestedFrameIndexes addIndexes:frameIndexesToAddToCache]; + + // Lazily create dedicated isolation queue. + if (!_serialQueue) { + _serialQueue = dispatch_queue_create("com.flipboard.framecachingqueue", DISPATCH_QUEUE_SERIAL); + } + + // Start streaming requested frames in the background into the cache. + // Avoid capturing self in the block as there's no reason to keep doing work if the animated image went away. + FLAnimatedImage * __weak weakSelf = self; + dispatch_async(_serialQueue, ^{ + // Produce and cache next needed frame. + void (^frameRangeBlock)(NSRange, BOOL *) = ^(NSRange range, __unused BOOL *stop) { + // Iterate through contiguous indexes; can be faster than `enumerateIndexesInRange:options:usingBlock:`. + __strong FLAnimatedImage *strongSelf = weakSelf; + for (NSUInteger i = range.location; i < NSMaxRange(range); i++) { + UIImage *image = [strongSelf predrawnImageAtIndex:i]; + // The results get returned one by one as soon as they're ready (and not in batch). + // The benefits of having the first frames as quick as possible outweigh building up a buffer to cope with potential hiccups when the CPU suddenly gets busy. + if (image && strongSelf) { + dispatch_async(dispatch_get_main_queue(), ^{ + strongSelf.cachedFrames[i] = image; + [strongSelf.cachedFrameIndexes addIndex:i]; + [strongSelf.requestedFrameIndexes removeIndex:i]; + }); + } + } + }; + + // Guard against crashing on 0-length ranges with an 'NSRangeException' "last range index (-1) beyond bounds" (Apple's error message is bogus here). + // This is only needed on iOS 5, iPad only, running on device and only for the range {0,0} but regardless of whether the index set is mutable or immutable or what the indexes in the set are (can even be empty). + if (firstRange.length > 0) { + [frameIndexesToAddToCache enumerateRangesInRange:firstRange options:0 usingBlock:frameRangeBlock]; + } + if (secondRange.length > 0) { + [frameIndexesToAddToCache enumerateRangesInRange:secondRange options:0 usingBlock:frameRangeBlock]; + } + }); +} + + ++ (CGSize)sizeForImage:(id)image +{ + CGSize imageSize = CGSizeZero; + + // Early return for nil + if (!image) { + return imageSize; + } + + if ([image isKindOfClass:[UIImage class]]) { + UIImage *uiImage = (UIImage *)image; + imageSize = uiImage.size; + } else if ([image isKindOfClass:[FLAnimatedImage class]]) { + FLAnimatedImage *animatedImage = (FLAnimatedImage *)image; + imageSize = animatedImage.size; + } else { + // Bear trap to capture bad images; we have seen crashers cropping up on iOS 7. + NSLog(@"Error: `image` isn't of expected types `UIImage` or `FLAnimatedImage`: %@", image); + } + + return imageSize; +} + + +#pragma mark - Private Methods +#pragma mark Frame Loading + +- (UIImage *)predrawnImageAtIndex:(NSUInteger)index +{ + // It's very important to use the cached `_imageSource` since the random access to a frame with `CGImageSourceCreateImageAtIndex` turns from an O(1) into an O(n) operation when re-initializing the image source every time. + CGImageRef imageRef = CGImageSourceCreateImageAtIndex(_imageSource, index, NULL); + UIImage *image = [UIImage imageWithCGImage:imageRef]; + CFRelease(imageRef); + + if (_imageDrawingBlock) + image = _imageDrawingBlock(image); + else + { + // Loading in the image object is only half the work, the displaying image view would still have to synchronosly wait and decode the image, so we go ahead and do that here on the background thread. + image = [[self class] predrawnImageFromImage:image]; + } + + return image; +} + + +#pragma mark Frame Caching + +- (NSIndexSet *)frameIndexesToCache +{ + NSIndexSet *indexesToCache = nil; + // Quick check to avoid building the index set if the number of frames to cache equals the total frame count. + if (self.frameCacheSizeCurrent == self.frameCount) { + indexesToCache = self.allFramesIndexSet; + } else { + NSMutableIndexSet *indexesToCacheMutable = [[NSMutableIndexSet alloc] init]; + + // Add indexes to the set in two separate blocks- the first starting from the requested frame index, up to the limit or the end. + // The second, if needed, the remaining number of frames beginning at index zero. + NSUInteger firstLength = MIN(self.frameCacheSizeCurrent, self.frameCount - self.requestedFrameIndex); + NSRange firstRange = NSMakeRange(self.requestedFrameIndex, firstLength); + [indexesToCacheMutable addIndexesInRange:firstRange]; + NSUInteger secondLength = self.frameCacheSizeCurrent - firstLength; + if (secondLength > 0) { + NSRange secondRange = NSMakeRange(0, secondLength); + [indexesToCacheMutable addIndexesInRange:secondRange]; + } + // Double check our math, before we add the poster image index which may increase it by one. + if ([indexesToCacheMutable count] != self.frameCacheSizeCurrent) { + NSLog(@"Error: Number of frames to cache doesn't equal expected cache size."); + } + + [indexesToCacheMutable addIndex:self.posterImageFrameIndex]; + + indexesToCache = [indexesToCacheMutable copy]; + } + + return indexesToCache; +} + + +- (void)purgeFrameCacheIfNeeded +{ + // Purge frames that are currently cached but don't need to be. + // But not if we're still under the number of frames to cache. + // This way, if all frames are allowed to be cached (the common case), we can skip all the `NSIndexSet` math below. + if ([self.cachedFrameIndexes count] > self.frameCacheSizeCurrent) { + NSMutableIndexSet *indexesToPurge = [self.cachedFrameIndexes mutableCopy]; + [indexesToPurge removeIndexes:[self frameIndexesToCache]]; + [indexesToPurge enumerateRangesUsingBlock:^(NSRange range, __unused BOOL *stop) { + // Iterate through contiguous indexes; can be faster than `enumerateIndexesInRange:options:usingBlock:`. + for (NSUInteger i = range.location; i < NSMaxRange(range); i++) { + [self.cachedFrameIndexes removeIndex:i]; + self.cachedFrames[i] = [NSNull null]; + // Note: Don't `CGImageSourceRemoveCacheAtIndex` on the image source for frames that we don't want cached any longer to maintain O(1) time access. + } + }]; + } +} + + +- (void)growFrameCacheSizeAfterMemoryWarning:(NSNumber *)frameCacheSize +{ + self.frameCacheSizeMaxInternal = [frameCacheSize unsignedIntegerValue]; + NSLog(@"Verbose: Grew frame cache size max to %lu after memory warning for animated image: %@", (unsigned long)self.frameCacheSizeMaxInternal, self); + + // Schedule resetting the frame cache size max completely after a while. + const NSTimeInterval kResetDelay = 3.0; + [self.weakProxy performSelector:@selector(resetFrameCacheSizeMaxInternal) withObject:nil afterDelay:kResetDelay]; +} + + +- (void)resetFrameCacheSizeMaxInternal +{ + self.frameCacheSizeMaxInternal = FLAnimatedImageFrameCacheSizeNoLimit; + NSLog(@"Verbose: Reset frame cache size max (current frame cache size: %lu) for animated image: %@", (unsigned long)self.frameCacheSizeCurrent, self); +} + + +#pragma mark System Memory Warnings Notification Handler + +- (void)didReceiveMemoryWarning:(NSNotification *)__unused notification +{ + // Hold (and use!) a strong reference to self, since `NSNotificationCenter` no longer strongly references the observer. + // This is another example of the lame fallout from the LLVM change in Xcode 5.1. + FLAnimatedImage *strongSelf = self; + + strongSelf.memoryWarningCount++; + + // If we were about to grow larger, but got rapped on our knuckles by the system again, cancel. + [NSObject cancelPreviousPerformRequestsWithTarget:strongSelf.weakProxy selector:@selector(growFrameCacheSizeAfterMemoryWarning:) object:@(FLAnimatedImageFrameCacheSizeGrowAfterMemoryWarning)]; + [NSObject cancelPreviousPerformRequestsWithTarget:strongSelf.weakProxy selector:@selector(resetFrameCacheSizeMaxInternal) object:nil]; + + // Go down to the minimum and by that implicitly immediately purge from the cache if needed to not get jettisoned by the system and start producing frames on-demand. + NSLog(@"Verbose: Attempt setting frame cache size max to %lu (previous was %lu) after memory warning #%lu for animated image: %@", (unsigned long)FLAnimatedImageFrameCacheSizeLowMemory, (unsigned long)strongSelf.frameCacheSizeMaxInternal, (unsigned long)strongSelf.memoryWarningCount, strongSelf); + strongSelf.frameCacheSizeMaxInternal = FLAnimatedImageFrameCacheSizeLowMemory; + + // Schedule growing larger again after a while, but cap our attempts to prevent a periodic sawtooth wave (ramps upward and then sharply drops) of memory usage. + // + // [mem]^ (2) (5) (6) 1) Loading frames for the first time + // (*)| , , , 2) Mem warning #1; purge cache + // | /| (4)/| /| 3) Grow cache size a bit after a while, if no mem warning occurs + // | / | _/ | _/ | 4) Try to grow cache size back to optimum after a while, if no mem warning occurs + // |(1)/ |_/ |/ |__(7) 5) Mem warning #2; purge cache + // |__/ (3) 6) After repetition of (3) and (4), mem warning #3; purge cache + // +----------------------> 7) After 3 mem warnings, stay at minimum cache size + // [t] + // *) The mem high water mark before we get warned might change for every cycle. + // + const NSUInteger kGrowAttemptsMax = 2; + const NSTimeInterval kGrowDelay = 2.0; + if ((strongSelf.memoryWarningCount - 1) <= kGrowAttemptsMax) { + [strongSelf.weakProxy performSelector:@selector(growFrameCacheSizeAfterMemoryWarning:) withObject:@(FLAnimatedImageFrameCacheSizeGrowAfterMemoryWarning) afterDelay:kGrowDelay]; + } + + // Note: It's not possible to get the level of a memory warning with a public API: http://stackoverflow.com/questions/2915247/iphone-os-memory-warnings-what-do-the-different-levels-mean/2915477#2915477 +} + + +#pragma mark Image Decoding + +// Decodes the image's data and draws it off-screen fully in memory; it's thread-safe and hence can be called on a background thread. +// On success, the returned object is a new `UIImage` instance with the same content as the one passed in. +// On failure, the returned object is the unchanged passed in one; the data will not be predrawn in memory though and an error will be logged. +// First inspired by & good Karma to: https://gist.github.com/steipete/1144242 ++ (UIImage *)predrawnImageFromImage:(UIImage *)imageToPredraw +{ + // Always use a device RGB color space for simplicity and predictability what will be going on. + CGColorSpaceRef colorSpaceDeviceRGBRef = CGColorSpaceCreateDeviceRGB(); + // Early return on failure! + if (!colorSpaceDeviceRGBRef) { + NSLog(@"Error: Failed to `CGColorSpaceCreateDeviceRGB` for image %@", imageToPredraw); + return imageToPredraw; + } + + // Even when the image doesn't have transparency, we have to add the extra channel because Quartz doesn't support other pixel formats than 32 bpp/8 bpc for RGB: + // kCGImageAlphaNoneSkipFirst, kCGImageAlphaNoneSkipLast, kCGImageAlphaPremultipliedFirst, kCGImageAlphaPremultipliedLast + // (source: docs "Quartz 2D Programming Guide > Graphics Contexts > Table 2-1 Pixel formats supported for bitmap graphics contexts") + size_t numberOfComponents = CGColorSpaceGetNumberOfComponents(colorSpaceDeviceRGBRef) + 1; // 4: RGB + A + + // "In iOS 4.0 and later, and OS X v10.6 and later, you can pass NULL if you want Quartz to allocate memory for the bitmap." (source: docs) + void *data = NULL; + size_t width = (size_t)imageToPredraw.size.width; + size_t height = (size_t)imageToPredraw.size.height; + size_t bitsPerComponent = CHAR_BIT; + + size_t bitsPerPixel = (bitsPerComponent * numberOfComponents); + size_t bytesPerPixel = (bitsPerPixel / BYTE_SIZE); + size_t bytesPerRow = (bytesPerPixel * width); + + CGBitmapInfo bitmapInfo = kCGBitmapByteOrderDefault; + + CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(imageToPredraw.CGImage); + // If the alpha info doesn't match to one of the supported formats (see above), pick a reasonable supported one. + // "For bitmaps created in iOS 3.2 and later, the drawing environment uses the premultiplied ARGB format to store the bitmap data." (source: docs) + if (alphaInfo == kCGImageAlphaNone || alphaInfo == kCGImageAlphaOnly) { + alphaInfo = kCGImageAlphaNoneSkipFirst; + } else if (alphaInfo == kCGImageAlphaFirst) { + alphaInfo = kCGImageAlphaPremultipliedFirst; + } else if (alphaInfo == kCGImageAlphaLast) { + alphaInfo = kCGImageAlphaPremultipliedLast; + } + // "The constants for specifying the alpha channel information are declared with the `CGImageAlphaInfo` type but can be passed to this parameter safely." (source: docs) + bitmapInfo |= alphaInfo; + + // Create our own graphics context to draw to; `UIGraphicsGetCurrentContext`/`UIGraphicsBeginImageContextWithOptions` doesn't create a new context but returns the current one which isn't thread-safe (e.g. main thread could use it at the same time). + // Note: It's not worth caching the bitmap context for multiple frames ("unique key" would be `width`, `height` and `hasAlpha`), it's ~50% slower. Time spent in libRIP's `CGSBlendBGRA8888toARGB8888` suddenly shoots up -- not sure why. + CGContextRef bitmapContextRef = CGBitmapContextCreate(data, width, height, bitsPerComponent, bytesPerRow, colorSpaceDeviceRGBRef, bitmapInfo); + CGColorSpaceRelease(colorSpaceDeviceRGBRef); + // Early return on failure! + if (!bitmapContextRef) { + NSLog(@"Error: Failed to `CGBitmapContextCreate` with color space %@ and parameters (width: %zu height: %zu bitsPerComponent: %zu bytesPerRow: %zu) for image %@", colorSpaceDeviceRGBRef, width, height, bitsPerComponent, bytesPerRow, imageToPredraw); + return imageToPredraw; + } + + // Draw image in bitmap context and create image by preserving receiver's properties. + CGContextDrawImage(bitmapContextRef, CGRectMake(0.0, 0.0, imageToPredraw.size.width, imageToPredraw.size.height), imageToPredraw.CGImage); + CGImageRef predrawnImageRef = CGBitmapContextCreateImage(bitmapContextRef); + UIImage *predrawnImage = [UIImage imageWithCGImage:predrawnImageRef scale:imageToPredraw.scale orientation:imageToPredraw.imageOrientation]; + CGImageRelease(predrawnImageRef); + CGContextRelease(bitmapContextRef); + + // Early return on failure! + if (!predrawnImage) { + NSLog(@"Error: Failed to `imageWithCGImage:scale:orientation:` with image ref %@ created with color space %@ and bitmap context %@ and properties and properties (scale: %f orientation: %ld) for image %@", predrawnImageRef, colorSpaceDeviceRGBRef, bitmapContextRef, imageToPredraw.scale, (long)imageToPredraw.imageOrientation, imageToPredraw); + return imageToPredraw; + } + + return predrawnImage; +} + + +#pragma mark - Description + +- (NSString *)description +{ + NSString *description = [super description]; + + description = [description stringByAppendingFormat:@" size=%@", NSStringFromCGSize(self.size)]; + description = [description stringByAppendingFormat:@" frameCount=%lu", (unsigned long)self.frameCount]; + + return description; +} + + +@end + + +#pragma mark - FLWeakProxy + +@interface FLWeakProxy () + +@property (nonatomic, weak) id target; + +@end + + +@implementation FLWeakProxy + +#pragma mark Life Cycle + +// This is the designated creation method of an `FLWeakProxy` and +// as a subclass of `NSProxy` it doesn't respond to or need `-init`. ++ (instancetype)weakProxyForObject:(id)targetObject +{ + FLWeakProxy *weakProxy = [FLWeakProxy alloc]; + weakProxy.target = targetObject; + return weakProxy; +} + + +#pragma mark Forwarding Messages + +- (id)forwardingTargetForSelector:(SEL)__unused selector +{ + // Keep it lightweight: access the ivar directly + return _target; +} + + +#pragma mark - NSWeakProxy Method Overrides +#pragma mark Handling Unimplemented Methods + +- (void)forwardInvocation:(NSInvocation *)invocation +{ + // Fallback for when target is nil. Don't do anything, just return 0/NULL/nil. + // The method signature we've received to get here is just a dummy to keep `doesNotRecognizeSelector:` from firing. + // We can't really handle struct return types here because we don't know the length. + void *nullPointer = NULL; + [invocation setReturnValue:&nullPointer]; +} + + +- (NSMethodSignature *)methodSignatureForSelector:(SEL)__unused selector +{ + // We only get here if `forwardingTargetForSelector:` returns nil. + // In that case, our weak target has been reclaimed. Return a dummy method signature to keep `doesNotRecognizeSelector:` from firing. + // We'll emulate the Obj-c messaging nil behavior by setting the return value to nil in `forwardInvocation:`, but we'll assume that the return value is `sizeof(void *)`. + // Other libraries handle this situation by making use of a global method signature cache, but that seems heavier than necessary and has issues as well. + // See https://www.mikeash.com/pyblog/friday-qa-2010-02-26-futures.html and https://github.com/steipete/PSTDelegateProxy/issues/1 for examples of using a method signature cache. + return [NSObject instanceMethodSignatureForSelector:@selector(init)]; +} + + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/FloatConversion.h b/submodules/LegacyComponents/LegacyComponents/FloatConversion.h new file mode 100644 index 0000000000..4a1616603a --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/FloatConversion.h @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2007 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of + * its contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef FloatConversion_h +#define FloatConversion_h + +#include + +namespace WebCore { + + template + float narrowPrecisionToFloat(T); + + template<> + inline float narrowPrecisionToFloat(double number) + { + return static_cast(number); + } + + template + CGFloat narrowPrecisionToCGFloat(T); + + template<> + inline CGFloat narrowPrecisionToCGFloat(double number) + { + return static_cast(number); + } + +} // namespace WebCore + +#endif // FloatConversion_h diff --git a/submodules/LegacyComponents/LegacyComponents/Freedom.h b/submodules/LegacyComponents/LegacyComponents/Freedom.h new file mode 100644 index 0000000000..481cc07ab2 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/Freedom.h @@ -0,0 +1,54 @@ +#ifndef Freedom_h +#define Freedom_h + +#import +#import + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct { + const char *string; + uint32_t key; +} FreedomIdentifier; + +char *copyFreedomIdentifierValue(FreedomIdentifier identifier); + +extern FreedomIdentifier FreedomIdentifierEmpty; + +typedef struct { + uint32_t name; + IMP imp; + FreedomIdentifier newIdentifier; + FreedomIdentifier newEncoding; +} FreedomDecoration; + +typedef struct { + ptrdiff_t offset; + int bit; +} FreedomBitfield; + +void freedomInit(); +bool freedomInitialized(); + +Class freedomClass(uint32_t name); +Class freedomMakeClass(Class superclass, Class subclass); +ptrdiff_t freedomIvarOffset(Class targetClass, uint32_t name); +FreedomBitfield freedomIvarBitOffset(Class targetClass, uint32_t fieldName, uint32_t bitfieldName); +FreedomBitfield freedomIvarBitOffset2(Class targetClass, uint32_t fieldName, uint32_t bitfieldName); +void freedomSetBitfield(void *object, FreedomBitfield bitfield, int value); +int freedomGetBitfield(void *object, FreedomBitfield bitfield); +void freedomDumpBitfields(Class targetClass, void *object, uint32_t fieldName); + +IMP freedomNativeImpl(Class targetClass, SEL selector); +void freedomClassAutoDecorate(uint32_t name, FreedomDecoration *classDecorations, int numClassDecorations, FreedomDecoration *instanceDecorations, int numInstanceDecorations); +void freedomClassAutoDecorateExplicit(Class c, uint32_t name, FreedomDecoration *classDecorations, int numClassDecorations, FreedomDecoration *instanceDecorations, int numInstanceDecorations); +IMP freedomImpl(id target, uint32_t name, SEL *selector); +IMP freedomImplInstancesOfClass(Class targetClass, uint32_t name, SEL *selector); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/submodules/LegacyComponents/LegacyComponents/Freedom.mm b/submodules/LegacyComponents/LegacyComponents/Freedom.mm new file mode 100644 index 0000000000..f25b1e355f --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/Freedom.mm @@ -0,0 +1,675 @@ +#import "Freedom.h" + +#import +#import + +#import + +#include + +#define FORCE_INLINE __attribute__((always_inline)) + +static inline uint32_t rotl32 ( uint32_t x, int8_t r ) +{ + return (x << r) | (x >> (32 - r)); +} + +#define ROTL32(x,y) rotl32(x,y) + +static FORCE_INLINE uint32_t getblock ( const uint32_t * p, int i ) +{ + return p[i]; +} + +static FORCE_INLINE uint32_t fmix ( uint32_t h ) +{ + h ^= h >> 16; + h *= 0x85ebca6b; + h ^= h >> 13; + h *= 0xc2b2ae35; + h ^= h >> 16; + + return h; +} + +static void MurmurHash3_x86_32 ( const void * key, int len, + uint32_t seed, void * out ) +{ + const uint8_t * data = (const uint8_t*)key; + const int nblocks = len / 4; + + uint32_t h1 = seed; + + const uint32_t c1 = 0xcc9e2d51; + const uint32_t c2 = 0x1b873593; + + //---------- + // body + + const uint32_t * blocks = (const uint32_t *)(data + nblocks*4); + + for(int i = -nblocks; i; i++) + { + uint32_t k1 = getblock(blocks,i); + + k1 *= c1; + k1 = ROTL32(k1,15); + k1 *= c2; + + h1 ^= k1; + h1 = ROTL32(h1,13); + h1 = h1*5+0xe6546b64; + } + + //---------- + // tail + + const uint8_t * tail = (const uint8_t*)(data + nblocks*4); + + uint32_t k1 = 0; + + switch(len & 3) + { + case 3: k1 ^= tail[2] << 16; + case 2: k1 ^= tail[1] << 8; + case 1: k1 ^= tail[0]; + k1 *= c1; k1 = ROTL32(k1,15); k1 *= c2; h1 ^= k1; + }; + + //---------- + // finalization + + h1 ^= len; + + h1 = fmix(h1); + + *(uint32_t*)out = h1; +} + +static int32_t murMurHashBytes32(void *bytes, int length) +{ + int32_t result = 0; + MurmurHash3_x86_32(bytes, length, -137723950, &result); + + return result; +} + +static const char *freedomDecoratedClass = "freedomDecoratedClass"; + +static int (*freedom_getClassList)(Class *, int) = NULL; +static const char * (*freedom_class_getName)(Class cls) = NULL; +static bool freedom_initialized = false; + +FreedomIdentifier FreedomIdentifierEmpty = (FreedomIdentifier){ .string = NULL, .key = 0 }; + +char *copyFreedomIdentifierValue(FreedomIdentifier identifier) +{ + if (identifier.string == NULL) + return NULL; + + int length = (int)strlen(identifier.string) / 2; + char *buf = (char *)malloc(length + 1); + buf[length] = 0; + + for (int i = 0; i < length; i++) + { + int b = 0; + sscanf(identifier.string + i * 2, "%02x", &b); + buf[i] = ((char)b) ^ (((uint8_t *)&identifier.key)[i % 4]); + } + + return buf; +} + +Class freedomClass(uint32_t name) +{ + static std::map classMap; + + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^ + { + int classCount = freedom_getClassList(NULL, 0); + if (classCount > 0) + { + __unsafe_unretained Class *classList = (Class *)malloc(classCount * sizeof(Class)); + objc_getClassList(classList, classCount); + + for (int i = 0; i < classCount; i++) + { + const char *className = freedom_class_getName(classList[i]); + uint32_t hashName = (uint32_t)murMurHashBytes32((void *)className, (int)strlen(className)); + //NSLog(@"0x%" PRIx32 " -> %s", hashName, className); + classMap[hashName] = classList[i]; + } + + free(classList); + } + freedom_initialized = true; + }); + + auto it = classMap.find(name); + if (it != classMap.end()) + return it->second; + + return nil; +} + +Class freedomMakeClass(Class superclass, Class subclass) +{ + if (superclass == Nil || subclass == Nil) + return nil; + + int32_t randomId = 0; + arc4random_buf(&randomId, 4); + Class decoratedClass = objc_allocateClassPair(superclass, [[NSString alloc] initWithFormat:@"Decorated%" PRIx32 "", randomId].UTF8String, 0); + + unsigned int count = 0; + Method *methodList = class_copyMethodList(subclass, &count); + if (methodList != NULL) + { + for (unsigned int i = 0; i < count; i++) + { + class_addMethod(decoratedClass, method_getName(methodList[i]), method_getImplementation(methodList[i]), method_getTypeEncoding(methodList[i])); + } + + free(methodList); + } + + objc_registerClassPair(decoratedClass); + + return decoratedClass; +} + +ptrdiff_t freedomIvarOffset(Class targetClass, uint32_t name) +{ + ptrdiff_t result = -1; + + unsigned int count = 0; + Ivar *ivarList = class_copyIvarList(targetClass, &count); + + if (ivarList != NULL) + { + for (unsigned int i = 0; i < count; i++) + { + const char *ivarName = ivar_getName(ivarList[i]); + uint32_t hashName = (uint32_t)murMurHashBytes32((void *)ivarName, (int)strlen(ivarName)); + //NSLog(@"%s -> 0x%x", ivarName, hashName); + if (hashName == name) + { + result = ivar_getOffset(ivarList[i]); + } + } + + free(ivarList); + } + + return result; +} + +static bool consumeToken(const char *desc, int length, int &offset, const char *token) +{ + size_t tokenLength = strlen(token); + if (offset + (int)tokenLength > length) + return false; + + if (strncmp(desc + offset, token, tokenLength)) + return false; + + offset += tokenLength; + return true; +} + +static bool consumeNumber(const char *desc, int length, int &offset, int &number) +{ + if (offset + 1 > length) + return false; + + int result = 0; + + for (int i = offset; i < length; i++) + { + if (desc[i] < '0' || desc[i] > '9') + { + if (i != offset) + { + number = result; + offset = i; + return true; + } + + return false; + } + else + result = result * 10 + (desc[i] - '0'); + } + + return false; +} + +static bool consumeField(const char *desc, int length, int &offset, uint32_t &outName, int &outBitSize) +{ + if (!consumeToken(desc, length, offset, "\"")) + return false; + + for (int i = offset; i < length; i++) + { + if (desc[i] == '"') + { + outName = (uint32_t)murMurHashBytes32((void *)(desc + offset), i - offset); + + char buf[i - offset + 1]; + memcpy(buf, desc + offset, i - offset); + buf[i - offset] = 0; + + int newOffset = i + 1; + if (consumeToken(desc, length, newOffset, "b")) + { + int fieldLength = 0; + if (consumeNumber(desc, length, newOffset, fieldLength)) + { + offset = newOffset; + outBitSize = fieldLength; + //NSLog(@"%s", buf); + return true; + } + else + return false; + } + else if (consumeToken(desc, length, newOffset, "I")) + { + offset = newOffset; + outBitSize = sizeof(unsigned int) * 8; + //NSLog(@"%s", buf); + return true; + } + else + return false; + + break; + } + } + + return false; +} + +static int freedomBitfieldOffsetInBits(const char *desc, uint32_t name) +{ + size_t length = strlen(desc); + int offset = 0; + + if (!consumeToken(desc, (int)length, offset, "{?=")) + return -1; + + int currentBitOffset = 0; + while (true) + { + uint32_t fieldName = 0; + int bitLength = 0; + if (consumeField(desc, (int)length, offset, fieldName, bitLength)) + { + if (fieldName == name) + return currentBitOffset; + + currentBitOffset += bitLength; + } + else + break; + } + + return -1; +} + +static void freedomDumpBitfieldsByDescription(void *address, const char *desc) +{ + size_t length = strlen(desc); + int offset = 0; + + if (!consumeToken(desc, (int)length, offset, "{?=")) + return; + + int currentBitOffset = 0; + while (true) + { + uint32_t fieldName = 0; + int bitLength = 0; + if (consumeField(desc, (int)length, offset, fieldName, bitLength)) + { + if (bitLength == 1) + { + uint8_t *value = ((uint8_t *)address) + currentBitOffset / 8 + (currentBitOffset % 8 == 0 ? 0 : 1); + NSLog(@" : %d", ((*value) & ((uint8_t)(1 << (8 - currentBitOffset % 8)))) != 0 ? 1 : 0); + } + + currentBitOffset += bitLength; + } + else + break; + } +} + +FreedomBitfield freedomIvarBitOffset(Class targetClass, uint32_t fieldName, uint32_t bitfieldName) +{ + FreedomBitfield result = {.offset = -1, .bit = -1 }; + + unsigned int count = 0; + Ivar *ivarList = class_copyIvarList(targetClass, &count); + + if (ivarList != NULL) + { + for (unsigned int i = 0; i < count; i++) + { + const char *ivarName = ivar_getName(ivarList[i]); + uint32_t hashName = (uint32_t)murMurHashBytes32((void *)ivarName, (int)strlen(ivarName)); + //NSLog(@"%s -> 0x%x", ivarName, hashName); + if (hashName == fieldName) + { + ptrdiff_t ivarOffset = ivar_getOffset(ivarList[i]); + int bitOffset = freedomBitfieldOffsetInBits(ivar_getTypeEncoding(ivarList[i]), bitfieldName); + if (bitOffset >= 0) + { + result.offset = ivarOffset + bitOffset / 8 + (bitOffset % 8 == 0 ? 0 : 1); + result.bit = bitOffset % 8; + } + } + } + + free(ivarList); + } + + return result; +} + +FreedomBitfield freedomIvarBitOffset2(Class targetClass, uint32_t fieldName, uint32_t bitfieldName) +{ + FreedomBitfield result = {.offset = -1, .bit = -1 }; + + unsigned int count = 0; + Ivar *ivarList = class_copyIvarList(targetClass, &count); + + if (ivarList != NULL) + { + for (unsigned int i = 0; i < count; i++) + { + const char *ivarName = ivar_getName(ivarList[i]); + uint32_t hashName = (uint32_t)murMurHashBytes32((void *)ivarName, (int)strlen(ivarName)); + //NSLog(@"%s -> 0x%x", ivarName, hashName); + if (hashName == fieldName) + { + ptrdiff_t ivarOffset = ivar_getOffset(ivarList[i]); + int bitOffset = freedomBitfieldOffsetInBits(ivar_getTypeEncoding(ivarList[i]), bitfieldName); + if (bitOffset >= 0) + { + result.offset = ivarOffset + bitOffset / 8; + result.bit = bitOffset % 8; + } + } + } + + free(ivarList); + } + + return result; +} + +void freedomSetBitfield(void *object, FreedomBitfield bitfield, int value) +{ + if (object == nil || bitfield.offset < 0 || bitfield.bit < 0) + return; + + uint8_t *bytePtr = (((uint8_t *)object) + bitfield.offset); + + if (value != 0) + *bytePtr = (*bytePtr) | ((uint8_t)(1 << (bitfield.bit))); + else + *bytePtr = (*bytePtr) & ((uint8_t)~(1 << (bitfield.bit))); +} + +int freedomGetBitfield(void *object, FreedomBitfield bitfield) +{ + if (object == nil || bitfield.offset < 0 || bitfield.bit < 0) + return 0; + + uint8_t *bytePtr = (((uint8_t *)object) + bitfield.offset); + + return ((*bytePtr) & ((uint8_t)(1 << (bitfield.bit)))) != 0; +} + +void freedomDumpBitfields(Class targetClass, void *object, uint32_t fieldName) +{ + unsigned int count = 0; + Ivar *ivarList = class_copyIvarList(targetClass, &count); + + if (ivarList != NULL) + { + for (unsigned int i = 0; i < count; i++) + { + const char *ivarName = ivar_getName(ivarList[i]); + uint32_t hashName = (uint32_t)murMurHashBytes32((void *)ivarName, (int)strlen(ivarName)); + if (hashName == fieldName) + { + ptrdiff_t ivarOffset = ivar_getOffset(ivarList[i]); + freedomDumpBitfieldsByDescription(((uint8_t *)object) + ivarOffset, ivar_getTypeEncoding(ivarList[i])); + } + } + + free(ivarList); + } +} + +IMP freedomNativeImpl(Class targetClass, SEL selector) +{ + return class_getMethodImplementation(class_getSuperclass(targetClass), selector); +} + +Class adjustDecoratedClass(Class targetClass) +{ + __unsafe_unretained Class decoratedClass = Nil; + __unsafe_unretained Class currentClass = targetClass; + while (currentClass != Nil) + { + __unsafe_unretained Class currentDecoratedClass = objc_getAssociatedObject(currentClass, freedomDecoratedClass); + if (currentDecoratedClass != Nil) + { + decoratedClass = currentDecoratedClass; + + if (currentClass != targetClass) + { + const char *currentClassName = class_getName(targetClass); + Class adjustedDecoratedClass = objc_allocateClassPair(targetClass, [[NSString alloc] initWithFormat:@"%s_Super%" PRIx32 "", class_getName(decoratedClass), murMurHashBytes32((void *)currentClassName, (int)strlen(currentClassName))].UTF8String, 0); + + unsigned int methodCount = 0; + Method *methodList = class_copyMethodList(decoratedClass, &methodCount); + for (unsigned int i = 0; i < methodCount; i++) + { + class_addMethod(adjustedDecoratedClass, method_getName(methodList[i]), method_getImplementation(methodList[i]), method_getTypeEncoding(methodList[i])); + } + free(methodList); + + objc_registerClassPair(adjustedDecoratedClass); + + decoratedClass = adjustedDecoratedClass; + objc_setAssociatedObject(targetClass, freedomDecoratedClass, adjustedDecoratedClass, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + } + + break; + } + + currentClass = class_getSuperclass(currentClass); + } + + return decoratedClass; +} + +id freedomAllocImpl(id self, SEL _cmd) +{ + static id (*nativeImp)(id, SEL) = NULL; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^ + { + nativeImp = (id (*)(id, SEL))class_getMethodImplementation(object_getClass([NSObject class]), @selector(alloc)); + }); + + id result = nativeImp(self, _cmd); + + Class decoratedClass = adjustDecoratedClass(self); + if (decoratedClass != Nil) + object_setClass(result, decoratedClass); + + return result; +} + +static SEL freedomFindMethodName(uint32_t name, Class className, Method *methodList, unsigned int methodCount, const char **methodTypeEncoding) +{ + for (unsigned int j = 0; j < methodCount; j++) + { + SEL methodName = method_getName(methodList[j]); + const char *selectorName = sel_getName(methodName); + uint32_t methodHashName = (uint32_t)murMurHashBytes32((void *)selectorName, (int)strlen(selectorName)); + //NSLog(@"0x%" PRIx32 " -> %s", methodHashName, selectorName); + if (methodHashName == name) + { + if (methodTypeEncoding != NULL) + *methodTypeEncoding = method_getTypeEncoding(methodList[j]); + return methodName; + } + } + + Class superClass = class_getSuperclass(className); + if (superClass != NULL) + { + unsigned int superClassMethodCount = 0; + Method *superClassMethodList = class_copyMethodList(superClass, &superClassMethodCount); + SEL result = freedomFindMethodName(name, superClass, superClassMethodList, superClassMethodCount, methodTypeEncoding); + free(superClassMethodList); + + return result; + } + + return NULL; +} + +void freedomClassAutoDecorate(uint32_t name, __unused FreedomDecoration *classDecorations, __unused int numClassDecorations, FreedomDecoration *instanceDecorations, int numInstanceDecorations) +{ + __unsafe_unretained Class className = freedomClass(name); + freedomClassAutoDecorateExplicit(className, name, classDecorations, numClassDecorations, instanceDecorations, numInstanceDecorations); +} + +void freedomClassAutoDecorateExplicit(Class className, uint32_t name, __unused FreedomDecoration *classDecorations, __unused int numClassDecorations, FreedomDecoration *instanceDecorations, int numInstanceDecorations) +{ + if (className != nil) + { + if (name == 0) + { + const char *string = [NSStringFromClass(className) UTF8String]; + name = (uint32_t)murMurHashBytes32((void *)string, (int)strlen(string)); + } + + Class decoratedClass = objc_allocateClassPair(className, [[NSString alloc] initWithFormat:@"Decorated%" PRIx32 "", name].UTF8String, 0); + + unsigned int methodCount = 0; + Method *methodList = class_copyMethodList(className, &methodCount); + + for (int i = 0; i < numInstanceDecorations; i++) + { + if (instanceDecorations[i].name != 0) + { + const char *methodTypeEncoding = NULL; + SEL methodName = freedomFindMethodName(instanceDecorations[i].name, className, methodList, methodCount, &methodTypeEncoding); + if (methodName != NULL) + class_addMethod(decoratedClass, methodName, instanceDecorations[i].imp, methodTypeEncoding); + else + { +#ifdef DEBUG + NSLog(@"[Freedom coulnd'n find method named 0x%" PRIx32 "]", instanceDecorations[i].name); +#endif + } + } + else if (instanceDecorations[i].newIdentifier.string != NULL && instanceDecorations[i].newEncoding.string != NULL) + { + char *identifier = copyFreedomIdentifierValue(instanceDecorations[i].newIdentifier); + char *encoding = copyFreedomIdentifierValue(instanceDecorations[i].newEncoding); + + SEL selector = sel_getUid(identifier); + if (!sel_isMapped(selector)) + selector = sel_registerName(identifier); + + class_addMethod(decoratedClass, selector, instanceDecorations[i].imp, encoding); + + free(identifier); + free(encoding); + } + else + { +#ifdef DEBUG + NSLog(@"[Freedom invalid decoration description]"); +#endif + } + } + + free(methodList); + + objc_registerClassPair(decoratedClass); + + objc_setAssociatedObject(className, freedomDecoratedClass, decoratedClass, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + Class metaClass = object_getClass(className); + class_addMethod(metaClass, @selector(alloc), (IMP)&freedomAllocImpl, "@:@"); + } + else + { +#ifdef DEBUG + NSLog(@"[Freedom coulnd'n find class named 0x%" PRIx32 "]", name); + assert(false); +#endif + } +} + +IMP freedomImpl(id target, uint32_t name, SEL *selector) +{ + if (target == nil) + return NULL; + + Class targetClass = object_getClass(target); + return freedomImplInstancesOfClass(targetClass, name, selector); +} + +IMP freedomImplInstancesOfClass(Class targetClass, uint32_t name, SEL *selector) { + if (targetClass == NULL) + return NULL; + + unsigned int methodCount = 0; + Method *methodList = class_copyMethodList(targetClass, &methodCount); + SEL methodName = freedomFindMethodName(name, targetClass, methodList, methodCount, NULL); + free(methodList); + + if (methodName != NULL) + { + if (selector != NULL) + *selector = methodName; + IMP result = class_getMethodImplementation(targetClass, methodName); + if (result == NULL) + { +#ifdef DEBUG + NSLog(@"[Freedom coulnd'n find method named 0x%" PRIx32 "]", name); +#endif + } + + return result; + } + + return NULL; +} + +void freedomInit() +{ + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^ + { + freedom_getClassList = &objc_getClassList; + freedom_class_getName = &class_getName; + }); +} + +bool freedomInitialized() +{ + return freedom_initialized; +} diff --git a/submodules/LegacyComponents/LegacyComponents/FreedomUIKit.h b/submodules/LegacyComponents/LegacyComponents/FreedomUIKit.h new file mode 100644 index 0000000000..a51fa397d6 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/FreedomUIKit.h @@ -0,0 +1,27 @@ +#ifndef FreedomUIKit_h +#define FreedomUIKit_h + +#import + +#ifdef __cplusplus +extern "C" { +#endif + +void freedomUIKitInit(); + +bool freedomUIKitTest3(); +bool freedomUIKitTest3_1(); +void freedomUIKitTest4(dispatch_block_t); +void freedomUIKitTest4_1(); + +@interface FFNotificationCenter : NSNotificationCenter + ++ (void)setShouldRotateBlock:(bool (^)())block; + +@end + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/submodules/LegacyComponents/LegacyComponents/FreedomUIKit.m b/submodules/LegacyComponents/LegacyComponents/FreedomUIKit.m new file mode 100644 index 0000000000..bf25711750 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/FreedomUIKit.m @@ -0,0 +1,288 @@ +#import "FreedomUIKit.h" + +#import "LegacyComponentsInternal.h" +#import "Freedom.h" + +#import +#import + +#import "TGHacks.h" +#import "TGNavigationController.h" + +#define DEBUG_KEYBOARD_QUEUE false + +void freedomUIKitInit2(); // iOS 6 +void freedomUIKitInit3(); // Notification Center Hook + +#if defined(DEBUG) && DEBUG_KEYBOARD_QUEUE +void freedomUIKitInit4(); // Keyboard Queue Debug +#endif + +void freedomUIKitInit() +{ + freedomUIKitInit2(); + freedomUIKitInit3(); + +#if defined(DEBUG) && DEBUG_KEYBOARD_QUEUE + freedomUIKitInit4(); +#endif +} + +#pragma mark - + +static int freedomUIKit_decorated2(__unused id self, __unused SEL _cmd) +{ + return 0; +} + +void freedomUIKitInit2() +{ + if (iosMajorVersion() == 6) + { + FreedomDecoration instanceDecorations[] = { + { .name = 0, + .imp = (IMP)&freedomUIKit_decorated2, + .newIdentifier = (FreedomIdentifier){ .string = "3066a13b3e6b", .key = 0x52d50551U }, + .newEncoding = (FreedomIdentifier){ .string = "1a7291", .key = 0x3ab3273U } + } + }; + + freedomClassAutoDecorate(0x1468e61aU, NULL, 0, instanceDecorations, sizeof(instanceDecorations) / sizeof(instanceDecorations[0])); + } +} + +#pragma mark - + +static bool test3 = false; +static bool test3_1 = false; + +@implementation FFNotificationCenter + +static bool (^shouldRotateBlock)() = nil; + ++ (void)setShouldRotateBlock:(bool (^)())block +{ + shouldRotateBlock = [block copy]; +} + +- (void)postNotificationName:(NSString *)aName object:(id)anObject userInfo:(NSDictionary *)aUserInfo +{ + bool clearFlag = false; + if ([aName isEqualToString:UIDeviceOrientationDidChangeNotification]) + { + clearFlag = true; + test3 = true; + test3_1 = true; + } + + if ([aName isEqualToString:UIDeviceOrientationDidChangeNotification]) + { + if (shouldRotateBlock == nil || shouldRotateBlock()) + [super postNotificationName:aName object:anObject userInfo:aUserInfo]; + } + else + [super postNotificationName:aName object:anObject userInfo:aUserInfo]; + + if (clearFlag) + test3 = false; +} + +@end + +bool freedomUIKitTest3() +{ + return test3; +} + +bool freedomUIKitTest3_1() +{ + bool value = test3_1; + test3_1 = false; + return value; +} + +void freedomUIKitInit3() +{ + object_setClass([NSNotificationCenter defaultCenter], [FFNotificationCenter class]); +} + +#pragma mark - + +static bool test4 = false; +static bool test4_1 = false; + +@interface TGHelperQueue : NSProxy +{ + id _target; +} + +@end + +@implementation TGHelperQueue + +- (instancetype)initWithTargetQueue:(id)target +{ + if (self != nil) + { + _target = target; + } + return self; +} + +- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector +{ + return [_target methodSignatureForSelector:selector]; +} + +- (void)forwardInvocation:(NSInvocation *)__unused invocation +{ +} + +- (id)forwardingTargetForSelector:(SEL)selector +{ + static char *name = NULL; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^ + { + name = copyFreedomIdentifierValue((FreedomIdentifier){ .string = "536772a971686fb4484777b1706768b6574769b8626f75b4576e7eb9", .key = 0xdd1b0624U }); + }); + + if (!strcmp(sel_getName(selector), name)) + { + if (test4 || test4_1) + { + return nil; + } + } + + return _target; +} + +- (void)doWork +{ +} + +@end + +static UIView *freedomUIKitFindView(UIView *view) +{ + if (view == nil) + return nil; + + if (object_getClass(view) == freedomClass(0x9cb128e7U)) + return view; + + for (UIView *subview in view.subviews) + { + UIView *result = freedomUIKitFindView(subview); + if (result != nil) + return result; + } + + return nil; +} + +void freedomUIKitTest4(dispatch_block_t block) +{ + if (iosMajorVersion() < 7 || iosMajorVersion() > 7 || (iosMajorVersion() == 7 && iosMinorVersion() >= 1)) + { + if (block != nil) + block(); + } + else + { + UIView *view = freedomUIKitFindView([TGHacks applicationKeyboardWindow]); + if (view != nil) + { + static ptrdiff_t queueOffset = -1; + static bool queueInitialized = false; + if (!queueInitialized) + { + queueInitialized = true; + queueOffset = freedomIvarOffset(object_getClass(view), 0xba913cbU); + } + + if (queueOffset >= 0) + { + __strong NSObject **queue = ((__strong NSObject **)(void *)(((uint8_t *)(__bridge void *)view) + queueOffset)); + if (*queue != nil) + { + if (object_getClass(*queue) != [TGHelperQueue class]) + { + TGHelperQueue *helper = [[TGHelperQueue alloc] initWithTargetQueue:*queue]; + *queue = (NSObject *)helper; + } + } + } + } + + bool previousTest4 = test4; + test4 = true; + + if (block != nil) + block(); + + test4 = previousTest4; + } +} + +void freedomUIKitTest4_1() +{ + test4_1 = true; + + dispatch_async(dispatch_get_main_queue(), ^ + { + test4_1 = false; + }); +} + +#if defined(DEBUG) && DEBUG_KEYBOARD_QUEUE +void freedomUIKit_decorated4_1(id self, SEL _cmd, id arg1) +{ + static void (*nativeImpl)(id, SEL, id) = NULL; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^ + { + nativeImpl = (void *)freedomNativeImpl([self class], _cmd); + }); + + if (nativeImpl != NULL) + nativeImpl(self, _cmd, arg1); + + TGLegacyLog(@"invoke %@", NSStringFromSelector(_cmd)); +} + + +void freedomUIKit_decorated4_2(id self, SEL _cmd) +{ + static void (*nativeImpl)(id, SEL) = NULL; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^ + { + nativeImpl = (void *)freedomNativeImpl([self class], _cmd); + }); + + if (nativeImpl != NULL) + nativeImpl(self, _cmd); + + TGLegacyLog(@"invoke %@", NSStringFromSelector(_cmd)); +} + +void freedomUIKitInit4() +{ + FreedomDecoration instanceDecorations[] = { + { .name = 0xbb6dbb9eU, //addTask: + .imp = (IMP)&freedomUIKit_decorated4_1, + .newIdentifier = FreedomIdentifierEmpty, + .newEncoding = FreedomIdentifierEmpty + }, + { .name = 0x757d9b1cU, //waitUntilAllTasksAreFinished + .imp = (IMP)&freedomUIKit_decorated4_2, + .newIdentifier = FreedomIdentifierEmpty, + .newEncoding = FreedomIdentifierEmpty + } + }; + + freedomClassAutoDecorate(0xfed2643dU, NULL, 0, instanceDecorations, sizeof(instanceDecorations) / sizeof(instanceDecorations[0])); +} +#endif diff --git a/submodules/LegacyComponents/LegacyComponents/GLProgram.h b/submodules/LegacyComponents/LegacyComponents/GLProgram.h new file mode 100755 index 0000000000..572d8ae9d0 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/GLProgram.h @@ -0,0 +1,42 @@ +// This is Jeff LaMarche's GLProgram OpenGL shader wrapper class from his OpenGL ES 2.0 book. +// A description of this can be found at his page on the topic: +// http://iphonedevelopment.blogspot.com/2010/11/opengl-es-20-for-ios-chapter-4.html +// I've extended this to be able to take programs as NSStrings in addition to files, for baked-in shaders + +#import + +#if TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE +#import +#import +#else +#import +#import +#endif + +@interface GLProgram : NSObject +{ + NSMutableArray *attributes; + NSMutableArray *uniforms; + GLuint program, + vertShader, + fragShader; +} + +@property(readwrite, nonatomic) BOOL initialized; + +- (id)initWithVertexShaderString:(NSString *)vShaderString + fragmentShaderString:(NSString *)fShaderString; +- (id)initWithVertexShaderString:(NSString *)vShaderString + fragmentShaderFilename:(NSString *)fShaderFilename; +- (id)initWithVertexShaderFilename:(NSString *)vShaderFilename + fragmentShaderFilename:(NSString *)fShaderFilename; +- (void)addAttribute:(NSString *)attributeName; +- (GLuint)attributeIndex:(NSString *)attributeName; +- (GLuint)uniformIndex:(NSString *)uniformName; +- (BOOL)link; +- (void)use; +- (NSString *)vertexShaderLog; +- (NSString *)fragmentShaderLog; +- (NSString *)programLog; +- (void)validate; +@end diff --git a/submodules/LegacyComponents/LegacyComponents/GLProgram.m b/submodules/LegacyComponents/LegacyComponents/GLProgram.m new file mode 100755 index 0000000000..26173d05c6 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/GLProgram.m @@ -0,0 +1,274 @@ +// This is Jeff LaMarche's GLProgram OpenGL shader wrapper class from his OpenGL ES 2.0 book. +// A description of this can be found at his page on the topic: +// http://iphonedevelopment.blogspot.com/2010/11/opengl-es-20-for-ios-chapter-4.html + +#import "LegacyComponentsInternal.h" + +#import "GLProgram.h" +// START:typedefs +#pragma mark Function Pointer Definitions +typedef void (*GLInfoFunction)(GLuint program, + GLenum pname, + GLint* params); +typedef void (*GLLogFunction) (GLuint program, + GLsizei bufsize, + GLsizei* length, + GLchar* infolog); +// END:typedefs +#pragma mark - +#pragma mark Private Extension Method Declaration +// START:extension +@interface GLProgram() + +- (BOOL)compileShader:(GLuint *)shader + type:(GLenum)type + string:(NSString *)shaderString; +- (NSString *)logForOpenGLObject:(GLuint)object + infoCallback:(GLInfoFunction)infoFunc + logFunc:(GLLogFunction)logFunc; +@end +// END:extension +#pragma mark - + +@implementation GLProgram +// START:init + +@synthesize initialized = _initialized; + +- (id)initWithVertexShaderString:(NSString *)vShaderString + fragmentShaderString:(NSString *)fShaderString +{ + if ((self = [super init])) + { + _initialized = NO; + + attributes = [[NSMutableArray alloc] init]; + uniforms = [[NSMutableArray alloc] init]; + program = glCreateProgram(); + + if (![self compileShader:&vertShader + type:GL_VERTEX_SHADER + string:vShaderString]) + NSLog(@"Failed to compile vertex shader"); + + // Create and compile fragment shader + if (![self compileShader:&fragShader + type:GL_FRAGMENT_SHADER + string:fShaderString]) + NSLog(@"Failed to compile fragment shader"); + + glAttachShader(program, vertShader); + glAttachShader(program, fragShader); + } + + return self; +} + +- (id)initWithVertexShaderString:(NSString *)vShaderString + fragmentShaderFilename:(NSString *)fShaderFilename +{ + NSString *fragShaderPathname = TGComponentsPathForResource(fShaderFilename, @"fsh"); + NSString *fragmentShaderString = [NSString stringWithContentsOfFile:fragShaderPathname encoding:NSUTF8StringEncoding error:nil]; + + if ((self = [self initWithVertexShaderString:vShaderString fragmentShaderString:fragmentShaderString])) + { + } + + return self; +} + +- (id)initWithVertexShaderFilename:(NSString *)vShaderFilename + fragmentShaderFilename:(NSString *)fShaderFilename +{ + NSString *vertShaderPathname = TGComponentsPathForResource(vShaderFilename, @"vsh"); + NSString *vertexShaderString = [NSString stringWithContentsOfFile:vertShaderPathname encoding:NSUTF8StringEncoding error:nil]; + + NSString *fragShaderPathname = TGComponentsPathForResource(fShaderFilename, @"fsh"); + NSString *fragmentShaderString = [NSString stringWithContentsOfFile:fragShaderPathname encoding:NSUTF8StringEncoding error:nil]; + + if ((self = [self initWithVertexShaderString:vertexShaderString fragmentShaderString:fragmentShaderString])) + { + } + + return self; +} +// END:init +// START:compile +- (BOOL)compileShader:(GLuint *)shader + type:(GLenum)type + string:(NSString *)shaderString +{ +// CFAbsoluteTime startTime = CFAbsoluteTimeGetCurrent(); + + GLint status; + const GLchar *source; + + source = + (GLchar *)[shaderString UTF8String]; + if (!source) + { + NSLog(@"Failed to load vertex shader"); + return NO; + } + + *shader = glCreateShader(type); + glShaderSource(*shader, 1, &source, NULL); + glCompileShader(*shader); + + glGetShaderiv(*shader, GL_COMPILE_STATUS, &status); + + if (status != GL_TRUE) + { + GLint logLength; + glGetShaderiv(*shader, GL_INFO_LOG_LENGTH, &logLength); + if (logLength > 0) + { + GLchar *log = (GLchar *)malloc(logLength); + glGetShaderInfoLog(*shader, logLength, &logLength, log); + NSLog(@"Shader compile log:\n%s", log); + free(log); + } + } + +// CFAbsoluteTime linkTime = (CFAbsoluteTimeGetCurrent() - startTime); +// NSLog(@"Compiled in %f ms", linkTime * 1000.0); + + return status == GL_TRUE; +} +// END:compile +#pragma mark - +// START:addattribute +- (void)addAttribute:(NSString *)attributeName +{ + if (![attributes containsObject:attributeName]) + { + [attributes addObject:attributeName]; + glBindAttribLocation(program, + (GLuint)[attributes indexOfObject:attributeName], + [attributeName UTF8String]); + } +} +// END:addattribute +// START:indexmethods +- (GLuint)attributeIndex:(NSString *)attributeName +{ + return (GLuint)[attributes indexOfObject:attributeName]; +} +- (GLuint)uniformIndex:(NSString *)uniformName +{ + return glGetUniformLocation(program, [uniformName UTF8String]); +} +// END:indexmethods +#pragma mark - +// START:link +- (BOOL)link +{ +// CFAbsoluteTime startTime = CFAbsoluteTimeGetCurrent(); + + GLint status; + + glLinkProgram(program); + + glGetProgramiv(program, GL_LINK_STATUS, &status); + if (status == GL_FALSE) + return NO; + + if (vertShader) + { + glDeleteShader(vertShader); + vertShader = 0; + } + if (fragShader) + { + glDeleteShader(fragShader); + fragShader = 0; + } + + self.initialized = YES; + +// CFAbsoluteTime linkTime = (CFAbsoluteTimeGetCurrent() - startTime); +// NSLog(@"Linked in %f ms", linkTime * 1000.0); + + return YES; +} +// END:link +// START:use +- (void)use +{ + glUseProgram(program); +} +// END:use +#pragma mark - +// START:privatelog +- (NSString *)logForOpenGLObject:(GLuint)object + infoCallback:(GLInfoFunction)infoFunc + logFunc:(GLLogFunction)logFunc +{ + GLint logLength = 0, charsWritten = 0; + + infoFunc(object, GL_INFO_LOG_LENGTH, &logLength); + if (logLength < 1) + return nil; + + char *logBytes = malloc(logLength); + logFunc(object, logLength, &charsWritten, logBytes); + NSString *log = [[NSString alloc] initWithBytes:logBytes + length:logLength + encoding:NSUTF8StringEncoding]; + free(logBytes); + return log; +} +// END:privatelog +// START:log +- (NSString *)vertexShaderLog +{ + return [self logForOpenGLObject:vertShader + infoCallback:(GLInfoFunction)&glGetProgramiv + logFunc:(GLLogFunction)&glGetProgramInfoLog]; + +} +- (NSString *)fragmentShaderLog +{ + return [self logForOpenGLObject:fragShader + infoCallback:(GLInfoFunction)&glGetProgramiv + logFunc:(GLLogFunction)&glGetProgramInfoLog]; +} +- (NSString *)programLog +{ + return [self logForOpenGLObject:program + infoCallback:(GLInfoFunction)&glGetProgramiv + logFunc:(GLLogFunction)&glGetProgramInfoLog]; +} +// END:log + +- (void)validate +{ + GLint logLength; + + glValidateProgram(program); + glGetProgramiv(program, GL_INFO_LOG_LENGTH, &logLength); + if (logLength > 0) + { + GLchar *log = (GLchar *)malloc(logLength); + glGetProgramInfoLog(program, logLength, &logLength, log); + NSLog(@"Program validate log:\n%s", log); + free(log); + } +} + +#pragma mark - +// START:dealloc +- (void)dealloc +{ + if (vertShader) + glDeleteShader(vertShader); + + if (fragShader) + glDeleteShader(fragShader); + + if (program) + glDeleteProgram(program); + +} +// END:dealloc +@end diff --git a/submodules/LegacyComponents/LegacyComponents/GPUImage.h b/submodules/LegacyComponents/LegacyComponents/GPUImage.h new file mode 100755 index 0000000000..7f2e0b82a2 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/GPUImage.h @@ -0,0 +1,20 @@ +#import "GLProgram.h" + +// Base classes +#import "GPUImageContext.h" +#import "GPUImageOutput.h" +#import "GPUImageView.h" +#import "GPUImageVideoCamera.h" +#import "GPUImageStillCamera.h" +#import "GPUImagePicture.h" +#import "GPUImageRawDataInput.h" +#import "GPUImageRawDataOutput.h" +#import "GPUImageTextureOutput.h" +#import "GPUImageFilterGroup.h" +#import "GPUImageFramebuffer.h" +#import "GPUImageFramebufferCache.h" + +// Filters +#import "GPUImageFilter.h" +#import "GPUImageTwoInputFilter.h" +#import "GPUImageGaussianBlurFilter.h" diff --git a/submodules/LegacyComponents/LegacyComponents/GPUImageContext.h b/submodules/LegacyComponents/LegacyComponents/GPUImageContext.h new file mode 100755 index 0000000000..e70174c73f --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/GPUImageContext.h @@ -0,0 +1,55 @@ +#import "GLProgram.h" +#import "GPUImageFramebuffer.h" +#import "GPUImageFramebufferCache.h" + +#define GPUImageRotationSwapsWidthAndHeight(rotation) ((rotation) == kGPUImageRotateLeft || (rotation) == kGPUImageRotateRight || (rotation) == kGPUImageRotateRightFlipVertical || (rotation) == kGPUImageRotateRightFlipHorizontal) + +typedef enum { kGPUImageNoRotation, kGPUImageRotateLeft, kGPUImageRotateRight, kGPUImageFlipVertical, kGPUImageFlipHorizonal, kGPUImageRotateRightFlipVertical, kGPUImageRotateRightFlipHorizontal, kGPUImageRotate180 } GPUImageRotationMode; + +@interface GPUImageContext : NSObject + +@property(readonly, nonatomic) dispatch_queue_t contextQueue; +@property(readwrite, retain, nonatomic) GLProgram *currentShaderProgram; +@property(readonly, retain, nonatomic) EAGLContext *context; +@property(readonly, nonatomic) CVOpenGLESTextureCacheRef coreVideoTextureCache; +@property(readonly, nonatomic) GPUImageFramebufferCache *framebufferCache; + ++ (void *)contextKey; ++ (GPUImageContext *)sharedImageProcessingContext; ++ (dispatch_queue_t)sharedContextQueue; ++ (GPUImageFramebufferCache *)sharedFramebufferCache; ++ (void)useImageProcessingContext; +- (void)useAsCurrentContext; ++ (void)setActiveShaderProgram:(GLProgram *)shaderProgram; +- (void)setContextShaderProgram:(GLProgram *)shaderProgram; ++ (GLint)maximumTextureSizeForThisDevice; ++ (GLint)maximumTextureUnitsForThisDevice; ++ (GLint)maximumVaryingVectorsForThisDevice; ++ (BOOL)deviceSupportsOpenGLESExtension:(NSString *)extension; ++ (BOOL)deviceSupportsRedTextures; ++ (BOOL)deviceSupportsFramebufferReads; ++ (CGSize)sizeThatFitsWithinATextureForSize:(CGSize)inputSize; + +- (void)presentBufferForDisplay; +- (GLProgram *)programForVertexShaderString:(NSString *)vertexShaderString fragmentShaderString:(NSString *)fragmentShaderString; + +- (void)useSharegroup:(EAGLSharegroup *)sharegroup; + +// Manage fast texture upload ++ (BOOL)supportsFastTextureUpload; + +@end + +@protocol GPUImageInput +- (void)newFrameReadyAtTime:(CMTime)frameTime atIndex:(NSInteger)textureIndex; +- (void)setInputFramebuffer:(GPUImageFramebuffer *)newInputFramebuffer atIndex:(NSInteger)textureIndex; +- (NSInteger)nextAvailableTextureIndex; +- (void)setInputSize:(CGSize)newSize atIndex:(NSInteger)textureIndex; +- (void)setInputRotation:(GPUImageRotationMode)newInputRotation atIndex:(NSInteger)textureIndex; +- (CGSize)maximumOutputSize; +- (void)endProcessing; +- (BOOL)shouldIgnoreUpdatesToThisTarget; +- (BOOL)enabled; +- (BOOL)wantsMonochromeInput; +- (void)setCurrentlyReceivingMonochromeInput:(BOOL)newValue; +@end diff --git a/submodules/LegacyComponents/LegacyComponents/GPUImageContext.m b/submodules/LegacyComponents/LegacyComponents/GPUImageContext.m new file mode 100755 index 0000000000..941c37601c --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/GPUImageContext.m @@ -0,0 +1,313 @@ +#import "GPUImageContext.h" +#import +#import + +#define MAXSHADERPROGRAMSALLOWEDINCACHE 40 + +@interface GPUImageContext() +{ + NSMutableDictionary *shaderProgramCache; + NSMutableArray *shaderProgramUsageHistory; + EAGLSharegroup *_sharegroup; +} + +@end + +@implementation GPUImageContext + +@synthesize context = _context; +@synthesize currentShaderProgram = _currentShaderProgram; +@synthesize contextQueue = _contextQueue; +@synthesize coreVideoTextureCache = _coreVideoTextureCache; +@synthesize framebufferCache = _framebufferCache; + +static void *openGLESContextQueueKey; + +- (id)init +{ + if (!(self = [super init])) + { + return nil; + } + + openGLESContextQueueKey = &openGLESContextQueueKey; + _contextQueue = dispatch_queue_create("com.sunsetlakesoftware.GPUImage.openGLESContextQueue", NULL); + +#if OS_OBJECT_USE_OBJC + dispatch_queue_set_specific(_contextQueue, openGLESContextQueueKey, (__bridge void *)self, NULL); +#endif + shaderProgramCache = [[NSMutableDictionary alloc] init]; + shaderProgramUsageHistory = [[NSMutableArray alloc] init]; + + return self; +} + ++ (void *)contextKey { + return openGLESContextQueueKey; +} + +// Based on Colin Wheeler's example here: http://cocoasamurai.blogspot.com/2011/04/singletons-your-doing-them-wrong.html ++ (GPUImageContext *)sharedImageProcessingContext +{ + static dispatch_once_t pred; + static GPUImageContext *sharedImageProcessingContext = nil; + + dispatch_once(&pred, ^{ + sharedImageProcessingContext = [[[self class] alloc] init]; + }); + return sharedImageProcessingContext; +} + ++ (dispatch_queue_t)sharedContextQueue +{ + return [[self sharedImageProcessingContext] contextQueue]; +} + ++ (GPUImageFramebufferCache *)sharedFramebufferCache +{ + return [[self sharedImageProcessingContext] framebufferCache]; +} + ++ (void)useImageProcessingContext +{ + [[GPUImageContext sharedImageProcessingContext] useAsCurrentContext]; +} + +- (void)useAsCurrentContext +{ + EAGLContext *imageProcessingContext = [self context]; + if ([EAGLContext currentContext] != imageProcessingContext) + { + [EAGLContext setCurrentContext:imageProcessingContext]; + } +} + ++ (void)setActiveShaderProgram:(GLProgram *)shaderProgram +{ + GPUImageContext *sharedContext = [GPUImageContext sharedImageProcessingContext]; + [sharedContext setContextShaderProgram:shaderProgram]; +} + +- (void)setContextShaderProgram:(GLProgram *)shaderProgram +{ + EAGLContext *imageProcessingContext = [self context]; + if ([EAGLContext currentContext] != imageProcessingContext) + { + [EAGLContext setCurrentContext:imageProcessingContext]; + } + + if (self.currentShaderProgram != shaderProgram) + { + self.currentShaderProgram = shaderProgram; + [shaderProgram use]; + } +} + ++ (GLint)maximumTextureSizeForThisDevice +{ + static dispatch_once_t pred; + static GLint maxTextureSize = 0; + + dispatch_once(&pred, ^{ + [self useImageProcessingContext]; + glGetIntegerv(GL_MAX_TEXTURE_SIZE, &maxTextureSize); + }); + + return maxTextureSize; +} + ++ (GLint)maximumTextureUnitsForThisDevice +{ + static dispatch_once_t pred; + static GLint maxTextureUnits = 0; + + dispatch_once(&pred, ^{ + [self useImageProcessingContext]; + glGetIntegerv(GL_MAX_TEXTURE_IMAGE_UNITS, &maxTextureUnits); + }); + + return maxTextureUnits; +} + ++ (GLint)maximumVaryingVectorsForThisDevice +{ + static dispatch_once_t pred; + static GLint maxVaryingVectors = 0; + + dispatch_once(&pred, ^{ + [self useImageProcessingContext]; + glGetIntegerv(GL_MAX_VARYING_VECTORS, &maxVaryingVectors); + }); + + return maxVaryingVectors; +} + ++ (BOOL)deviceSupportsOpenGLESExtension:(NSString *)extension +{ + static dispatch_once_t pred; + static NSArray *extensionNames = nil; + + // Cache extensions for later quick reference, since this won't change for a given device + dispatch_once(&pred, ^{ + [GPUImageContext useImageProcessingContext]; + NSString *extensionsString = [NSString stringWithCString:(const char *)glGetString(GL_EXTENSIONS) encoding:NSASCIIStringEncoding]; + extensionNames = [extensionsString componentsSeparatedByString:@" "]; + }); + + return [extensionNames containsObject:extension]; +} + + +// http://www.khronos.org/registry/gles/extensions/EXT/EXT_texture_rg.txt + ++ (BOOL)deviceSupportsRedTextures +{ + static dispatch_once_t pred; + static BOOL supportsRedTextures = NO; + + dispatch_once(&pred, ^{ + supportsRedTextures = [GPUImageContext deviceSupportsOpenGLESExtension:@"GL_EXT_texture_rg"]; + }); + + return supportsRedTextures; +} + ++ (BOOL)deviceSupportsFramebufferReads +{ + static dispatch_once_t pred; + static BOOL supportsFramebufferReads = NO; + + dispatch_once(&pred, ^{ + supportsFramebufferReads = [GPUImageContext deviceSupportsOpenGLESExtension:@"GL_EXT_shader_framebuffer_fetch"]; + }); + + return supportsFramebufferReads; +} + ++ (CGSize)sizeThatFitsWithinATextureForSize:(CGSize)inputSize +{ + GLint maxTextureSize = [self maximumTextureSizeForThisDevice]; + if ( (inputSize.width < maxTextureSize) && (inputSize.height < maxTextureSize) ) + { + return inputSize; + } + + CGSize adjustedSize; + if (inputSize.width > inputSize.height) + { + adjustedSize.width = (CGFloat)maxTextureSize; + adjustedSize.height = ((CGFloat)maxTextureSize / inputSize.width) * inputSize.height; + } + else + { + adjustedSize.height = (CGFloat)maxTextureSize; + adjustedSize.width = ((CGFloat)maxTextureSize / inputSize.height) * inputSize.width; + } + + return CGSizeMake(floor(adjustedSize.width), floor(adjustedSize.height)); +} + +- (void)presentBufferForDisplay +{ + [self.context presentRenderbuffer:GL_RENDERBUFFER]; +} + +- (GLProgram *)programForVertexShaderString:(NSString *)vertexShaderString fragmentShaderString:(NSString *)fragmentShaderString +{ + NSString *lookupKeyForShaderProgram = [NSString stringWithFormat:@"V: %@ - F: %@", vertexShaderString, fragmentShaderString]; + GLProgram *programFromCache = [shaderProgramCache objectForKey:lookupKeyForShaderProgram]; + + if (programFromCache == nil) + { + programFromCache = [[GLProgram alloc] initWithVertexShaderString:vertexShaderString fragmentShaderString:fragmentShaderString]; + [shaderProgramCache setObject:programFromCache forKey:lookupKeyForShaderProgram]; +// [shaderProgramUsageHistory addObject:lookupKeyForShaderProgram]; +// if ([shaderProgramUsageHistory count] >= MAXSHADERPROGRAMSALLOWEDINCACHE) +// { +// for (NSUInteger currentShaderProgramRemovedFromCache = 0; currentShaderProgramRemovedFromCache < 10; currentShaderProgramRemovedFromCache++) +// { +// NSString *shaderProgramToRemoveFromCache = [shaderProgramUsageHistory objectAtIndex:0]; +// [shaderProgramUsageHistory removeObjectAtIndex:0]; +// [shaderProgramCache removeObjectForKey:shaderProgramToRemoveFromCache]; +// } +// } + } + + return programFromCache; +} + +- (void)useSharegroup:(EAGLSharegroup *)sharegroup +{ + NSAssert(_context == nil, @"Unable to use a share group when the context has already been created. Call this method before you use the context for the first time."); + + _sharegroup = sharegroup; +} + +- (EAGLContext *)createContext +{ + EAGLContext *context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2 sharegroup:_sharegroup]; + NSAssert(context != nil, @"Unable to create an OpenGL ES 2.0 context. The GPUImage framework requires OpenGL ES 2.0 support to work."); + return context; +} + + +#pragma mark - +#pragma mark Manage fast texture upload + ++ (BOOL)supportsFastTextureUpload +{ +#if TARGET_IPHONE_SIMULATOR + return NO; +#else + return (CVOpenGLESTextureCacheCreate != NULL); +#endif +} + +#pragma mark - +#pragma mark Accessors + +- (EAGLContext *)context +{ + if (_context == nil) + { + _context = [self createContext]; + [EAGLContext setCurrentContext:_context]; + + // Set up a few global settings for the image processing pipeline + glDisable(GL_DEPTH_TEST); + } + + return _context; +} + +- (CVOpenGLESTextureCacheRef)coreVideoTextureCache +{ + if (_coreVideoTextureCache == NULL) + { +#if defined(__IPHONE_6_0) + CVReturn err = CVOpenGLESTextureCacheCreate(kCFAllocatorDefault, NULL, [self context], NULL, &_coreVideoTextureCache); +#else + CVReturn err = CVOpenGLESTextureCacheCreate(kCFAllocatorDefault, NULL, (__bridge void *)[self context], NULL, &_coreVideoTextureCache); +#endif + + if (err) + { + NSAssert(NO, @"Error at CVOpenGLESTextureCacheCreate %d", err); + } + + } + + return _coreVideoTextureCache; +} + +- (GPUImageFramebufferCache *)framebufferCache +{ + if (_framebufferCache == nil) + { + _framebufferCache = [[GPUImageFramebufferCache alloc] init]; + } + + return _framebufferCache; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/GPUImageFilter.h b/submodules/LegacyComponents/LegacyComponents/GPUImageFilter.h new file mode 100755 index 0000000000..bb8fa97096 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/GPUImageFilter.h @@ -0,0 +1,139 @@ +#import "GPUImageOutput.h" + +#define STRINGIZE(x) #x +#define STRINGIZE2(x) STRINGIZE(x) +#define SHADER_STRING(text) @ STRINGIZE2(text) + +#define GPUImageHashIdentifier # +#define GPUImageWrappedLabel(x) x +#define GPUImageEscapedHashIdentifier(a) GPUImageWrappedLabel(GPUImageHashIdentifier)a + +extern NSString *const kGPUImageVertexShaderString; +extern NSString *const kGPUImagePassthroughFragmentShaderString; + +struct GPUVector4 { + GLfloat one; + GLfloat two; + GLfloat three; + GLfloat four; +}; +typedef struct GPUVector4 GPUVector4; + +struct GPUVector3 { + GLfloat one; + GLfloat two; + GLfloat three; +}; +typedef struct GPUVector3 GPUVector3; + +struct GPUMatrix4x4 { + GPUVector4 one; + GPUVector4 two; + GPUVector4 three; + GPUVector4 four; +}; +typedef struct GPUMatrix4x4 GPUMatrix4x4; + +struct GPUMatrix3x3 { + GPUVector3 one; + GPUVector3 two; + GPUVector3 three; +}; +typedef struct GPUMatrix3x3 GPUMatrix3x3; + +/** GPUImage's base filter class + + Filters and other subsequent elements in the chain conform to the GPUImageInput protocol, which lets them take in the supplied or processed texture from the previous link in the chain and do something with it. Objects one step further down the chain are considered targets, and processing can be branched by adding multiple targets to a single output or filter. + */ +@interface GPUImageFilter : GPUImageOutput +{ + GPUImageFramebuffer *firstInputFramebuffer; + +@public + GLProgram *filterProgram; +@protected + GLint filterPositionAttribute, filterTextureCoordinateAttribute; + GLint filterInputTextureUniform; + GLfloat backgroundColorRed, backgroundColorGreen, backgroundColorBlue, backgroundColorAlpha; + + BOOL isEndProcessing; + + CGSize currentFilterSize; + GPUImageRotationMode inputRotation; + + BOOL currentlyReceivingMonochromeInput; + + NSMutableDictionary *uniformStateRestorationBlocks; + dispatch_semaphore_t imageCaptureSemaphore; +} + +@property (readonly, nonatomic) GLProgram *program; +@property (nonatomic, readonly) CVPixelBufferRef renderTarget; +@property (readwrite, nonatomic) BOOL preventRendering; +@property (readwrite, nonatomic) BOOL currentlyReceivingMonochromeInput; + +/// @name Initialization and teardown + +/** + Initialize with vertex and fragment shaders + + You make take advantage of the SHADER_STRING macro to write your shaders in-line. + @param vertexShaderString Source code of the vertex shader to use + @param fragmentShaderString Source code of the fragment shader to use + */ +- (id)initWithVertexShaderFromString:(NSString *)vertexShaderString fragmentShaderFromString:(NSString *)fragmentShaderString; + +/** + Initialize with a fragment shader + + You may take advantage of the SHADER_STRING macro to write your shader in-line. + @param fragmentShaderString Source code of fragment shader to use + */ +- (id)initWithFragmentShaderFromString:(NSString *)fragmentShaderString; +/** + Initialize with a fragment shader + @param fragmentShaderFilename Filename of fragment shader to load + */ +- (id)initWithFragmentShaderFromFile:(NSString *)fragmentShaderFilename; +- (void)initializeAttributes; +- (void)setupFilterForSize:(CGSize)filterFrameSize; +- (CGSize)rotatedSize:(CGSize)sizeToRotate forIndex:(NSInteger)textureIndex; +- (CGPoint)rotatedPoint:(CGPoint)pointToRotate forRotation:(GPUImageRotationMode)rotation; + +/// @name Managing the display FBOs +/** Size of the frame buffer object + */ +- (CGSize)sizeOfFBO; + +/// @name Rendering ++ (const GLfloat *)textureCoordinatesForRotation:(GPUImageRotationMode)rotationMode; +- (void)renderToTextureWithVertices:(const GLfloat *)vertices textureCoordinates:(const GLfloat *)textureCoordinates; +- (void)informTargetsAboutNewFrameAtTime:(CMTime)frameTime; +- (CGSize)outputFrameSize; + +/// @name Input parameters +- (void)setBackgroundColorRed:(GLfloat)redComponent green:(GLfloat)greenComponent blue:(GLfloat)blueComponent alpha:(GLfloat)alphaComponent; +- (void)setInteger:(GLint)newInteger forUniformName:(NSString *)uniformName; +- (void)setFloat:(GLfloat)newFloat forUniformName:(NSString *)uniformName; +- (void)setSize:(CGSize)newSize forUniformName:(NSString *)uniformName; +- (void)setPoint:(CGPoint)newPoint forUniformName:(NSString *)uniformName; +- (void)setFloatVec3:(GPUVector3)newVec3 forUniformName:(NSString *)uniformName; +- (void)setFloatVec4:(GPUVector4)newVec4 forUniform:(NSString *)uniformName; +- (void)setFloatArray:(GLfloat *)array length:(GLsizei)count forUniform:(NSString*)uniformName; + +- (void)setMatrix3f:(GPUMatrix3x3)matrix forUniform:(GLint)uniform program:(GLProgram *)shaderProgram; +- (void)setMatrix4f:(GPUMatrix4x4)matrix forUniform:(GLint)uniform program:(GLProgram *)shaderProgram; +- (void)setFloat:(GLfloat)floatValue forUniform:(GLint)uniform program:(GLProgram *)shaderProgram; +- (void)setPoint:(CGPoint)pointValue forUniform:(GLint)uniform program:(GLProgram *)shaderProgram; +- (void)setSize:(CGSize)sizeValue forUniform:(GLint)uniform program:(GLProgram *)shaderProgram; +- (void)setVec3:(GPUVector3)vectorValue forUniform:(GLint)uniform program:(GLProgram *)shaderProgram; +- (void)setVec4:(GPUVector4)vectorValue forUniform:(GLint)uniform program:(GLProgram *)shaderProgram; +- (void)setFloatArray:(GLfloat *)arrayValue length:(GLsizei)arrayLength forUniform:(GLint)uniform program:(GLProgram *)shaderProgram; +- (void)setInteger:(GLint)intValue forUniform:(GLint)uniform program:(GLProgram *)shaderProgram; + +- (void)setAndExecuteUniformStateCallbackAtIndex:(GLint)uniform forProgram:(GLProgram *)shaderProgram toBlock:(dispatch_block_t)uniformStateBlock; +- (void)setUniformsForProgramAtIndex:(NSUInteger)programIndex; + +- (GLint)uniformIndexForName:(NSString *)name; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/GPUImageFilter.m b/submodules/LegacyComponents/LegacyComponents/GPUImageFilter.m new file mode 100755 index 0000000000..1f54c6eb63 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/GPUImageFilter.m @@ -0,0 +1,764 @@ +#import "GPUImageFilter.h" +#import + +#import "LegacyComponentsInternal.h" + +// Hardcode the vertex shader for standard filters, but this can be overridden +NSString *const kGPUImageVertexShaderString = SHADER_STRING +( + attribute vec4 position; + attribute vec4 inputTexCoord; + + varying vec2 texCoord; + + void main() + { + gl_Position = position; + texCoord = inputTexCoord.xy; + } + ); + +#if TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE + +NSString *const kGPUImagePassthroughFragmentShaderString = SHADER_STRING +( + varying highp vec2 texCoord; + + uniform sampler2D sourceImage; + + void main() + { + gl_FragColor = texture2D(sourceImage, texCoord); + } +); + +#else + +NSString *const kGPUImagePassthroughFragmentShaderString = SHADER_STRING +( + varying vec2 texCoord; + + uniform sampler2D sourceImage; + + void main() + { + gl_FragColor = texture2D(sourceImage, texCoord); + } +); +#endif + + +@implementation GPUImageFilter + +@synthesize preventRendering = _preventRendering; +@synthesize currentlyReceivingMonochromeInput; + +#pragma mark - +#pragma mark Initialization and teardown + +- (id)initWithVertexShaderFromString:(NSString *)vertexShaderString fragmentShaderFromString:(NSString *)fragmentShaderString +{ + if (!(self = [super init])) + { + return nil; + } + + uniformStateRestorationBlocks = [NSMutableDictionary dictionaryWithCapacity:10]; + _preventRendering = NO; + currentlyReceivingMonochromeInput = NO; + inputRotation = kGPUImageNoRotation; + backgroundColorRed = 0.0; + backgroundColorGreen = 0.0; + backgroundColorBlue = 0.0; + backgroundColorAlpha = 0.0; + imageCaptureSemaphore = dispatch_semaphore_create(0); + dispatch_semaphore_signal(imageCaptureSemaphore); + + runSynchronouslyOnVideoProcessingQueue(^{ + [GPUImageContext useImageProcessingContext]; + + filterProgram = [[GPUImageContext sharedImageProcessingContext] programForVertexShaderString:vertexShaderString fragmentShaderString:fragmentShaderString]; + + if (!filterProgram.initialized) + { + [self initializeAttributes]; + + if (![filterProgram link]) + { + NSString *progLog = [filterProgram programLog]; + NSLog(@"Program link log: %@", progLog); + NSString *fragLog = [filterProgram fragmentShaderLog]; + NSLog(@"Fragment shader compile log: %@", fragLog); + NSString *vertLog = [filterProgram vertexShaderLog]; + NSLog(@"Vertex shader compile log: %@", vertLog); + filterProgram = nil; + NSAssert(NO, @"Filter shader link failed"); + } + } + + filterPositionAttribute = [filterProgram attributeIndex:@"position"]; + filterTextureCoordinateAttribute = [filterProgram attributeIndex:@"inputTexCoord"]; + filterInputTextureUniform = [filterProgram uniformIndex:@"sourceImage"]; + + [GPUImageContext setActiveShaderProgram:filterProgram]; + + glEnableVertexAttribArray(filterPositionAttribute); + glEnableVertexAttribArray(filterTextureCoordinateAttribute); + }); + + return self; +} + +- (id)initWithFragmentShaderFromString:(NSString *)fragmentShaderString +{ + if (!(self = [self initWithVertexShaderFromString:kGPUImageVertexShaderString fragmentShaderFromString:fragmentShaderString])) + { + return nil; + } + + return self; +} + +- (id)initWithFragmentShaderFromFile:(NSString *)fragmentShaderFilename +{ + NSString *fragmentShaderPathname = TGComponentsPathForResource(fragmentShaderFilename, @"fsh"); + NSString *fragmentShaderString = [NSString stringWithContentsOfFile:fragmentShaderPathname encoding:NSUTF8StringEncoding error:nil]; + + if (!(self = [self initWithFragmentShaderFromString:fragmentShaderString])) + { + return nil; + } + + return self; +} + +- (id)init +{ + if (!(self = [self initWithFragmentShaderFromString:kGPUImagePassthroughFragmentShaderString])) + { + return nil; + } + + return self; +} + +- (void)initializeAttributes +{ + [filterProgram addAttribute:@"position"]; + [filterProgram addAttribute:@"inputTexCoord"]; + + // Override this, calling back to this super method, in order to add new attributes to your vertex shader +} + +- (void)setupFilterForSize:(CGSize)__unused filterFrameSize +{ + // This is where you can override to provide some custom setup, if your filter has a size-dependent element +} + +- (void)dealloc +{ +#if !OS_OBJECT_USE_OBJC + if (imageCaptureSemaphore != NULL) + { + dispatch_release(imageCaptureSemaphore); + } +#endif + +} + +#pragma mark - +#pragma mark Still image processing + +- (void)useNextFrameForImageCapture +{ + usingNextFrameForImageCapture = YES; + + // Set the semaphore high, if it isn't already + if (dispatch_semaphore_wait(imageCaptureSemaphore, DISPATCH_TIME_NOW) != 0) + { + return; + } +} + +- (CGImageRef)newCGImageFromCurrentlyProcessedOutput +{ + // Give it three seconds to process, then abort if they forgot to set up the image capture properly + double timeoutForImageCapture = 3.0; + dispatch_time_t convertedTimeout = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeoutForImageCapture * NSEC_PER_SEC)); + + if (dispatch_semaphore_wait(imageCaptureSemaphore, convertedTimeout) != 0) + { + return NULL; + } + + GPUImageFramebuffer* framebuffer = [self framebufferForOutput]; + + usingNextFrameForImageCapture = NO; + dispatch_semaphore_signal(imageCaptureSemaphore); + + CGImageRef image = [framebuffer newCGImageFromFramebufferContents]; + return image; +} + +#pragma mark - +#pragma mark Managing the display FBOs + +- (CGSize)sizeOfFBO +{ + CGSize outputSize = [self maximumOutputSize]; + if ( (CGSizeEqualToSize(outputSize, CGSizeZero)) || (inputTextureSize.width < outputSize.width) ) + { + return inputTextureSize; + } + else + { + return outputSize; + } +} + +#pragma mark - +#pragma mark Rendering + ++ (const GLfloat *)textureCoordinatesForRotation:(GPUImageRotationMode)rotationMode +{ + static const GLfloat noRotationTextureCoordinates[] = { + 0.0f, 0.0f, + 1.0f, 0.0f, + 0.0f, 1.0f, + 1.0f, 1.0f, + }; + + static const GLfloat rotateLeftTextureCoordinates[] = { + 1.0f, 0.0f, + 1.0f, 1.0f, + 0.0f, 0.0f, + 0.0f, 1.0f, + }; + + static const GLfloat rotateRightTextureCoordinates[] = { + 0.0f, 1.0f, + 0.0f, 0.0f, + 1.0f, 1.0f, + 1.0f, 0.0f, + }; + + static const GLfloat verticalFlipTextureCoordinates[] = { + 0.0f, 1.0f, + 1.0f, 1.0f, + 0.0f, 0.0f, + 1.0f, 0.0f, + }; + + static const GLfloat horizontalFlipTextureCoordinates[] = { + 1.0f, 0.0f, + 0.0f, 0.0f, + 1.0f, 1.0f, + 0.0f, 1.0f, + }; + + static const GLfloat rotateRightVerticalFlipTextureCoordinates[] = { + 0.0f, 0.0f, + 0.0f, 1.0f, + 1.0f, 0.0f, + 1.0f, 1.0f, + }; + + static const GLfloat rotateRightHorizontalFlipTextureCoordinates[] = { + 1.0f, 1.0f, + 1.0f, 0.0f, + 0.0f, 1.0f, + 0.0f, 0.0f, + }; + + static const GLfloat rotate180TextureCoordinates[] = { + 1.0f, 1.0f, + 0.0f, 1.0f, + 1.0f, 0.0f, + 0.0f, 0.0f, + }; + + switch(rotationMode) + { + case kGPUImageNoRotation: return noRotationTextureCoordinates; + case kGPUImageRotateLeft: return rotateLeftTextureCoordinates; + case kGPUImageRotateRight: return rotateRightTextureCoordinates; + case kGPUImageFlipVertical: return verticalFlipTextureCoordinates; + case kGPUImageFlipHorizonal: return horizontalFlipTextureCoordinates; + case kGPUImageRotateRightFlipVertical: return rotateRightVerticalFlipTextureCoordinates; + case kGPUImageRotateRightFlipHorizontal: return rotateRightHorizontalFlipTextureCoordinates; + case kGPUImageRotate180: return rotate180TextureCoordinates; + } +} + +- (void)renderToTextureWithVertices:(const GLfloat *)vertices textureCoordinates:(const GLfloat *)textureCoordinates +{ + if (self.preventRendering) + { + [firstInputFramebuffer unlock]; + return; + } + + [GPUImageContext setActiveShaderProgram:filterProgram]; + + outputFramebuffer = [[GPUImageContext sharedFramebufferCache] fetchFramebufferForSize:[self sizeOfFBO] textureOptions:self.outputTextureOptions onlyTexture:NO]; + [outputFramebuffer activateFramebuffer]; + if (usingNextFrameForImageCapture) + { + [outputFramebuffer lock]; + } + + [self setUniformsForProgramAtIndex:0]; + + glClearColor(backgroundColorRed, backgroundColorGreen, backgroundColorBlue, backgroundColorAlpha); + glClear(GL_COLOR_BUFFER_BIT); + + glActiveTexture(GL_TEXTURE2); + glBindTexture(GL_TEXTURE_2D, [firstInputFramebuffer texture]); + + glUniform1i(filterInputTextureUniform, 2); + + glVertexAttribPointer(filterPositionAttribute, 2, GL_FLOAT, 0, 0, vertices); + glVertexAttribPointer(filterTextureCoordinateAttribute, 2, GL_FLOAT, 0, 0, textureCoordinates); + + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); + + [firstInputFramebuffer unlock]; + + if (usingNextFrameForImageCapture) + { + dispatch_semaphore_signal(imageCaptureSemaphore); + } +} + +- (void)informTargetsAboutNewFrameAtTime:(CMTime)frameTime +{ + if (self.frameProcessingCompletionBlock != NULL) + { + self.frameProcessingCompletionBlock(self, frameTime); + } + + // Get all targets the framebuffer so they can grab a lock on it + for (id currentTarget in targets) + { + if (currentTarget != self.targetToIgnoreForUpdates) + { + NSInteger indexOfObject = [targets indexOfObject:currentTarget]; + NSInteger textureIndex = [[targetTextureIndices objectAtIndex:indexOfObject] integerValue]; + + [self setInputFramebufferForTarget:currentTarget atIndex:textureIndex]; + [currentTarget setInputSize:[self outputFrameSize] atIndex:textureIndex]; + } + } + + // Release our hold so it can return to the cache immediately upon processing + [[self framebufferForOutput] unlock]; + + if (usingNextFrameForImageCapture) + { +// usingNextFrameForImageCapture = NO; + } + else + { + [self removeOutputFramebuffer]; + } + + // Trigger processing last, so that our unlock comes first in serial execution, avoiding the need for a callback + for (id currentTarget in targets) + { + if (currentTarget != self.targetToIgnoreForUpdates) + { + NSInteger indexOfObject = [targets indexOfObject:currentTarget]; + NSInteger textureIndex = [[targetTextureIndices objectAtIndex:indexOfObject] integerValue]; + [currentTarget newFrameReadyAtTime:frameTime atIndex:textureIndex]; + } + } +} + +- (CGSize)outputFrameSize +{ + return inputTextureSize; +} + +#pragma mark - +#pragma mark Input parameters + +- (void)setBackgroundColorRed:(GLfloat)redComponent green:(GLfloat)greenComponent blue:(GLfloat)blueComponent alpha:(GLfloat)alphaComponent +{ + backgroundColorRed = redComponent; + backgroundColorGreen = greenComponent; + backgroundColorBlue = blueComponent; + backgroundColorAlpha = alphaComponent; +} + +- (void)setInteger:(GLint)newInteger forUniformName:(NSString *)uniformName +{ + GLint uniformIndex = [filterProgram uniformIndex:uniformName]; + [self setInteger:newInteger forUniform:uniformIndex program:filterProgram]; +} + +- (void)setFloat:(GLfloat)newFloat forUniformName:(NSString *)uniformName +{ + GLint uniformIndex = [filterProgram uniformIndex:uniformName]; + [self setFloat:newFloat forUniform:uniformIndex program:filterProgram]; +} + +- (void)setSize:(CGSize)newSize forUniformName:(NSString *)uniformName +{ + GLint uniformIndex = [filterProgram uniformIndex:uniformName]; + [self setSize:newSize forUniform:uniformIndex program:filterProgram]; +} + +- (void)setPoint:(CGPoint)newPoint forUniformName:(NSString *)uniformName +{ + GLint uniformIndex = [filterProgram uniformIndex:uniformName]; + [self setPoint:newPoint forUniform:uniformIndex program:filterProgram]; +} + +- (void)setFloatVec3:(GPUVector3)newVec3 forUniformName:(NSString *)uniformName +{ + GLint uniformIndex = [filterProgram uniformIndex:uniformName]; + [self setVec3:newVec3 forUniform:uniformIndex program:filterProgram]; +} + +- (void)setFloatVec4:(GPUVector4)newVec4 forUniform:(NSString *)uniformName +{ + GLint uniformIndex = [filterProgram uniformIndex:uniformName]; + [self setVec4:newVec4 forUniform:uniformIndex program:filterProgram]; +} + +- (void)setFloatArray:(GLfloat *)array length:(GLsizei)count forUniform:(NSString*)uniformName +{ + GLint uniformIndex = [filterProgram uniformIndex:uniformName]; + + [self setFloatArray:array length:count forUniform:uniformIndex program:filterProgram]; +} + +- (void)setMatrix3f:(GPUMatrix3x3)matrix forUniform:(GLint)uniform program:(GLProgram *)shaderProgram +{ + runAsynchronouslyOnVideoProcessingQueue(^{ + [GPUImageContext setActiveShaderProgram:shaderProgram]; + [self setAndExecuteUniformStateCallbackAtIndex:uniform forProgram:shaderProgram toBlock:^{ + glUniformMatrix3fv(uniform, 1, GL_FALSE, (GLfloat *)&matrix); + }]; + }); +} + +- (void)setMatrix4f:(GPUMatrix4x4)matrix forUniform:(GLint)uniform program:(GLProgram *)shaderProgram +{ + runAsynchronouslyOnVideoProcessingQueue(^{ + [GPUImageContext setActiveShaderProgram:shaderProgram]; + [self setAndExecuteUniformStateCallbackAtIndex:uniform forProgram:shaderProgram toBlock:^{ + glUniformMatrix4fv(uniform, 1, GL_FALSE, (GLfloat *)&matrix); + }]; + }); +} + +- (void)setFloat:(GLfloat)floatValue forUniform:(GLint)uniform program:(GLProgram *)shaderProgram +{ + runAsynchronouslyOnVideoProcessingQueue(^{ + [GPUImageContext setActiveShaderProgram:shaderProgram]; + [self setAndExecuteUniformStateCallbackAtIndex:uniform forProgram:shaderProgram toBlock:^{ + glUniform1f(uniform, floatValue); + }]; + }); +} + +- (void)setPoint:(CGPoint)pointValue forUniform:(GLint)uniform program:(GLProgram *)shaderProgram +{ + runAsynchronouslyOnVideoProcessingQueue(^{ + [GPUImageContext setActiveShaderProgram:shaderProgram]; + [self setAndExecuteUniformStateCallbackAtIndex:uniform forProgram:shaderProgram toBlock:^{ + GLfloat positionArray[2]; + positionArray[0] = (GLfloat)pointValue.x; + positionArray[1] = (GLfloat)pointValue.y; + + glUniform2fv(uniform, 1, positionArray); + }]; + }); +} + +- (void)setSize:(CGSize)sizeValue forUniform:(GLint)uniform program:(GLProgram *)shaderProgram +{ + runAsynchronouslyOnVideoProcessingQueue(^{ + [GPUImageContext setActiveShaderProgram:shaderProgram]; + + [self setAndExecuteUniformStateCallbackAtIndex:uniform forProgram:shaderProgram toBlock:^{ + GLfloat sizeArray[2]; + sizeArray[0] = (GLfloat)sizeValue.width; + sizeArray[1] = (GLfloat)sizeValue.height; + + glUniform2fv(uniform, 1, sizeArray); + }]; + }); +} + +- (void)setVec3:(GPUVector3)vectorValue forUniform:(GLint)uniform program:(GLProgram *)shaderProgram +{ + runAsynchronouslyOnVideoProcessingQueue(^{ + [GPUImageContext setActiveShaderProgram:shaderProgram]; + + [self setAndExecuteUniformStateCallbackAtIndex:uniform forProgram:shaderProgram toBlock:^{ + glUniform3fv(uniform, 1, (GLfloat *)&vectorValue); + }]; + }); +} + +- (void)setVec4:(GPUVector4)vectorValue forUniform:(GLint)uniform program:(GLProgram *)shaderProgram +{ + runAsynchronouslyOnVideoProcessingQueue(^{ + [GPUImageContext setActiveShaderProgram:shaderProgram]; + + [self setAndExecuteUniformStateCallbackAtIndex:uniform forProgram:shaderProgram toBlock:^{ + glUniform4fv(uniform, 1, (GLfloat *)&vectorValue); + }]; + }); +} + +- (void)setFloatArray:(GLfloat *)arrayValue length:(GLsizei)arrayLength forUniform:(GLint)uniform program:(GLProgram *)shaderProgram +{ + // Make a copy of the data, so it doesn't get overwritten before async call executes + NSData* arrayData = [NSData dataWithBytes:arrayValue length:arrayLength * sizeof(arrayValue[0])]; + + runAsynchronouslyOnVideoProcessingQueue(^{ + [GPUImageContext setActiveShaderProgram:shaderProgram]; + + [self setAndExecuteUniformStateCallbackAtIndex:uniform forProgram:shaderProgram toBlock:^{ + glUniform1fv(uniform, arrayLength, [arrayData bytes]); + }]; + }); +} + +- (void)setInteger:(GLint)intValue forUniform:(GLint)uniform program:(GLProgram *)shaderProgram +{ + runAsynchronouslyOnVideoProcessingQueue(^{ + [GPUImageContext setActiveShaderProgram:shaderProgram]; + + [self setAndExecuteUniformStateCallbackAtIndex:uniform forProgram:shaderProgram toBlock:^{ + glUniform1i(uniform, intValue); + }]; + }); +} + +- (void)setAndExecuteUniformStateCallbackAtIndex:(GLint)uniform forProgram:(GLProgram *)__unused shaderProgram toBlock:(dispatch_block_t)uniformStateBlock +{ + [uniformStateRestorationBlocks setObject:[uniformStateBlock copy] forKey:[NSNumber numberWithInt:uniform]]; + uniformStateBlock(); +} + +- (void)setUniformsForProgramAtIndex:(NSUInteger)__unused programIndex +{ + [uniformStateRestorationBlocks enumerateKeysAndObjectsUsingBlock:^(__unused id key, id obj, __unused BOOL *stop){ + dispatch_block_t currentBlock = obj; + currentBlock(); + }]; +} + +#pragma mark - +#pragma mark GPUImageInput + +- (void)newFrameReadyAtTime:(CMTime)frameTime atIndex:(NSInteger)__unused textureIndex +{ + static const GLfloat imageVertices[] = { + -1.0f, -1.0f, + 1.0f, -1.0f, + -1.0f, 1.0f, + 1.0f, 1.0f, + }; + + [self renderToTextureWithVertices:imageVertices textureCoordinates:[[self class] textureCoordinatesForRotation:inputRotation]]; + + [self informTargetsAboutNewFrameAtTime:frameTime]; +} + +- (NSInteger)nextAvailableTextureIndex +{ + return 0; +} + +- (void)setInputFramebuffer:(GPUImageFramebuffer *)newInputFramebuffer atIndex:(NSInteger)__unused textureIndex +{ + firstInputFramebuffer = newInputFramebuffer; + [firstInputFramebuffer lock]; +} + +- (CGSize)rotatedSize:(CGSize)sizeToRotate forIndex:(NSInteger)__unused textureIndex +{ + CGSize rotatedSize = sizeToRotate; + + if (GPUImageRotationSwapsWidthAndHeight(inputRotation)) + { + rotatedSize.width = sizeToRotate.height; + rotatedSize.height = sizeToRotate.width; + } + + return rotatedSize; +} + +- (CGPoint)rotatedPoint:(CGPoint)pointToRotate forRotation:(GPUImageRotationMode)rotation +{ + CGPoint rotatedPoint; + switch(rotation) + { + case kGPUImageNoRotation: return pointToRotate; break; + case kGPUImageFlipHorizonal: + { + rotatedPoint.x = 1.0f - (GLfloat)pointToRotate.x; + rotatedPoint.y = (GLfloat)pointToRotate.y; + }; break; + case kGPUImageFlipVertical: + { + rotatedPoint.x = pointToRotate.x; + rotatedPoint.y = 1.0f - pointToRotate.y; + }; break; + case kGPUImageRotateLeft: + { + rotatedPoint.x = 1.0f - pointToRotate.y; + rotatedPoint.y = pointToRotate.x; + }; break; + case kGPUImageRotateRight: + { + rotatedPoint.x = pointToRotate.y; + rotatedPoint.y = 1.0f - pointToRotate.x; + }; break; + case kGPUImageRotateRightFlipVertical: + { + rotatedPoint.x = pointToRotate.y; + rotatedPoint.y = pointToRotate.x; + }; break; + case kGPUImageRotateRightFlipHorizontal: + { + rotatedPoint.x = 1.0f - pointToRotate.y; + rotatedPoint.y = 1.0f - pointToRotate.x; + }; break; + case kGPUImageRotate180: + { + rotatedPoint.x = 1.0f - pointToRotate.x; + rotatedPoint.y = 1.0f - pointToRotate.y; + }; break; + } + + return rotatedPoint; +} + +- (void)setInputSize:(CGSize)newSize atIndex:(NSInteger)textureIndex +{ + if (self.preventRendering) + { + return; + } + + if (overrideInputSize) + { + if (CGSizeEqualToSize(forcedMaximumSize, CGSizeZero)) + { + } + else + { + CGRect insetRect = AVMakeRectWithAspectRatioInsideRect(newSize, CGRectMake(0.0, 0.0, forcedMaximumSize.width, forcedMaximumSize.height)); + inputTextureSize = insetRect.size; + } + } + else + { + CGSize rotatedSize = [self rotatedSize:newSize forIndex:textureIndex]; + + if (CGSizeEqualToSize(rotatedSize, CGSizeZero)) + { + inputTextureSize = rotatedSize; + } + else if (!CGSizeEqualToSize(inputTextureSize, rotatedSize)) + { + inputTextureSize = rotatedSize; + } + } + + [self setupFilterForSize:[self sizeOfFBO]]; +} + +- (void)setInputRotation:(GPUImageRotationMode)newInputRotation atIndex:(NSInteger)__unused textureIndex +{ + inputRotation = newInputRotation; +} + +- (void)forceProcessingAtSize:(CGSize)frameSize +{ + if (CGSizeEqualToSize(frameSize, CGSizeZero)) + { + overrideInputSize = NO; + } + else + { + overrideInputSize = YES; + inputTextureSize = frameSize; + forcedMaximumSize = CGSizeZero; + } +} + +- (void)forceProcessingAtSizeRespectingAspectRatio:(CGSize)frameSize +{ + if (CGSizeEqualToSize(frameSize, CGSizeZero)) + { + overrideInputSize = NO; + inputTextureSize = CGSizeZero; + forcedMaximumSize = CGSizeZero; + } + else + { + overrideInputSize = YES; + forcedMaximumSize = frameSize; + } +} + +- (CGSize)maximumOutputSize +{ + // I'm temporarily disabling adjustments for smaller output sizes until I figure out how to make this work better + return CGSizeZero; + + /* + if (CGSizeEqualToSize(cachedMaximumOutputSize, CGSizeZero)) + { + for (id currentTarget in targets) + { + if ([currentTarget maximumOutputSize].width > cachedMaximumOutputSize.width) + { + cachedMaximumOutputSize = [currentTarget maximumOutputSize]; + } + } + } + + return cachedMaximumOutputSize; + */ +} + +- (void)endProcessing +{ + if (!isEndProcessing) + { + isEndProcessing = YES; + + for (id currentTarget in targets) + { + [currentTarget endProcessing]; + } + } +} + +- (BOOL)wantsMonochromeInput +{ + return NO; +} + +- (GLint)uniformIndexForName:(NSString *)name +{ + return [filterProgram uniformIndex:name]; +} + +#pragma mark - +#pragma mark Accessors + +- (GLProgram *)program +{ + return filterProgram; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/GPUImageFramebuffer.h b/submodules/LegacyComponents/LegacyComponents/GPUImageFramebuffer.h new file mode 100755 index 0000000000..7c71618410 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/GPUImageFramebuffer.h @@ -0,0 +1,58 @@ +#import + +#if TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE +#import +#import +#import +#else +#import +#import +#endif + +#import +#import + + +typedef struct GPUTextureOptions { + GLenum minFilter; + GLenum magFilter; + GLenum wrapS; + GLenum wrapT; + GLenum internalFormat; + GLenum format; + GLenum type; +} GPUTextureOptions; + +@interface GPUImageFramebuffer : NSObject + +@property (nonatomic, readonly) CGSize size; +@property (nonatomic, readonly) GPUTextureOptions textureOptions; +@property (nonatomic, readonly) GLuint texture; +@property (nonatomic, readonly) BOOL missingFramebuffer; + +// Initialization and teardown +- (id)initWithSize:(CGSize)framebufferSize; +- (id)initWithSize:(CGSize)framebufferSize textureOptions:(GPUTextureOptions)fboTextureOptions onlyTexture:(BOOL)onlyGenerateTexture; +- (id)initWithSize:(CGSize)framebufferSize overriddenTexture:(GLuint)inputTexture; + +// Usage +- (void)activateFramebuffer; + +// Reference counting +- (void)lock; +- (void)unlock; +- (void)clearAllLocks; +- (void)disableReferenceCounting; +- (void)enableReferenceCounting; + +// Image capture +- (CGImageRef)newCGImageFromFramebufferContents; +- (void)restoreRenderTarget; + +// Raw data bytes +- (void)lockForReading; +- (void)unlockAfterReading; +- (NSUInteger)bytesPerRow; +- (GLubyte *)byteBuffer; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/GPUImageFramebuffer.m b/submodules/LegacyComponents/LegacyComponents/GPUImageFramebuffer.m new file mode 100755 index 0000000000..ac289c817a --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/GPUImageFramebuffer.m @@ -0,0 +1,437 @@ +#import "GPUImageFramebuffer.h" +#import "GPUImageOutput.h" + +@interface GPUImageFramebuffer() +{ + GLuint framebuffer; +#if TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE + CVPixelBufferRef renderTarget; + CVOpenGLESTextureRef renderTexture; + NSUInteger readLockCount; +#else +#endif + NSUInteger framebufferReferenceCount; + BOOL referenceCountingDisabled; +} + +- (void)generateFramebuffer; +- (void)generateTexture; +- (void)destroyFramebuffer; + +@end + +void dataProviderReleaseCallback (void *info, const void *data, size_t size); +void dataProviderUnlockCallback (void *info, const void *data, size_t size); + +@implementation GPUImageFramebuffer + +#pragma mark - +#pragma mark Initialization and teardown + +- (id)initWithSize:(CGSize)framebufferSize textureOptions:(GPUTextureOptions)fboTextureOptions onlyTexture:(BOOL)onlyGenerateTexture +{ + if (!(self = [super init])) + { + return nil; + } + + _textureOptions = fboTextureOptions; + _size = framebufferSize; + framebufferReferenceCount = 0; + referenceCountingDisabled = NO; + _missingFramebuffer = onlyGenerateTexture; + + if (_missingFramebuffer) + { + runSynchronouslyOnVideoProcessingQueue(^{ + [GPUImageContext useImageProcessingContext]; + [self generateTexture]; + framebuffer = 0; + }); + } + else + { + [self generateFramebuffer]; + } + return self; +} + +- (id)initWithSize:(CGSize)framebufferSize overriddenTexture:(GLuint)inputTexture +{ + if (!(self = [super init])) + { + return nil; + } + + GPUTextureOptions defaultTextureOptions; + defaultTextureOptions.minFilter = GL_LINEAR; + defaultTextureOptions.magFilter = GL_LINEAR; + defaultTextureOptions.wrapS = GL_CLAMP_TO_EDGE; + defaultTextureOptions.wrapT = GL_CLAMP_TO_EDGE; + defaultTextureOptions.internalFormat = GL_RGBA; + defaultTextureOptions.format = GL_BGRA; + defaultTextureOptions.type = GL_UNSIGNED_BYTE; + + _textureOptions = defaultTextureOptions; + _size = framebufferSize; + framebufferReferenceCount = 0; + referenceCountingDisabled = YES; + + _texture = inputTexture; + + return self; +} + +- (id)initWithSize:(CGSize)framebufferSize +{ + GPUTextureOptions defaultTextureOptions; + defaultTextureOptions.minFilter = GL_LINEAR; + defaultTextureOptions.magFilter = GL_LINEAR; + defaultTextureOptions.wrapS = GL_CLAMP_TO_EDGE; + defaultTextureOptions.wrapT = GL_CLAMP_TO_EDGE; + defaultTextureOptions.internalFormat = GL_RGBA; + defaultTextureOptions.format = GL_BGRA; + defaultTextureOptions.type = GL_UNSIGNED_BYTE; + + if (!(self = [self initWithSize:framebufferSize textureOptions:defaultTextureOptions onlyTexture:NO])) + { + return nil; + } + + return self; +} + +- (void)dealloc +{ + [self destroyFramebuffer]; +} + +#pragma mark - +#pragma mark Internal + +- (void)generateTexture +{ + glActiveTexture(GL_TEXTURE1); + glGenTextures(1, &_texture); + glBindTexture(GL_TEXTURE_2D, _texture); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, _textureOptions.minFilter); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, _textureOptions.magFilter); + // This is necessary for non-power-of-two textures + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, _textureOptions.wrapS); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, _textureOptions.wrapT); + + // TODO: Handle mipmaps +} + +- (void)generateFramebuffer +{ + runSynchronouslyOnVideoProcessingQueue(^{ + [GPUImageContext useImageProcessingContext]; + + glGenFramebuffers(1, &framebuffer); + glBindFramebuffer(GL_FRAMEBUFFER, framebuffer); + + // By default, all framebuffers on iOS 5.0+ devices are backed by texture caches, using one shared cache + if ([GPUImageContext supportsFastTextureUpload]) + { +#if TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE + CVOpenGLESTextureCacheRef coreVideoTextureCache = [[GPUImageContext sharedImageProcessingContext] coreVideoTextureCache]; + // Code originally sourced from http://allmybrain.com/2011/12/08/rendering-to-a-texture-with-ios-5-texture-cache-api/ + + CFDictionaryRef empty; // empty value for attr value. + CFMutableDictionaryRef attrs; + empty = CFDictionaryCreate(kCFAllocatorDefault, NULL, NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); // our empty IOSurface properties dictionary + attrs = CFDictionaryCreateMutable(kCFAllocatorDefault, 1, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); + CFDictionarySetValue(attrs, kCVPixelBufferIOSurfacePropertiesKey, empty); + + CVReturn err = CVPixelBufferCreate(kCFAllocatorDefault, (int)_size.width, (int)_size.height, kCVPixelFormatType_32BGRA, attrs, &renderTarget); + if (err) + { + NSLog(@"FBO size: %f, %f", _size.width, _size.height); + NSAssert(NO, @"Error at CVPixelBufferCreate %d", err); + } + + err = CVOpenGLESTextureCacheCreateTextureFromImage (kCFAllocatorDefault, coreVideoTextureCache, renderTarget, + NULL, // texture attributes + GL_TEXTURE_2D, + _textureOptions.internalFormat, // opengl format + (int)_size.width, + (int)_size.height, + _textureOptions.format, // native iOS format + _textureOptions.type, + 0, + &renderTexture); + if (err) + { + NSAssert(NO, @"Error at CVOpenGLESTextureCacheCreateTextureFromImage %d", err); + } + + CFRelease(attrs); + CFRelease(empty); + + glBindTexture(CVOpenGLESTextureGetTarget(renderTexture), CVOpenGLESTextureGetName(renderTexture)); + _texture = CVOpenGLESTextureGetName(renderTexture); + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, _textureOptions.wrapS); + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, _textureOptions.wrapT); + + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, CVOpenGLESTextureGetName(renderTexture), 0); +#endif + } + else + { + [self generateTexture]; + + glBindTexture(GL_TEXTURE_2D, _texture); + + glTexImage2D(GL_TEXTURE_2D, 0, _textureOptions.internalFormat, (int)_size.width, (int)_size.height, 0, _textureOptions.format, _textureOptions.type, 0); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, _texture, 0); + } + + #ifndef NS_BLOCK_ASSERTIONS + GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER); + NSAssert(status == GL_FRAMEBUFFER_COMPLETE, @"Incomplete filter FBO: %d", status); + #endif + + glBindTexture(GL_TEXTURE_2D, 0); + }); +} + +- (void)destroyFramebuffer +{ + runSynchronouslyOnVideoProcessingQueue(^{ + [GPUImageContext useImageProcessingContext]; + + if (framebuffer) + { + glDeleteFramebuffers(1, &framebuffer); + framebuffer = 0; + } + + + if ([GPUImageContext supportsFastTextureUpload] && (!_missingFramebuffer)) + { +#if TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE + if (renderTarget) + { + CFRelease(renderTarget); + renderTarget = NULL; + } + + if (renderTexture) + { + CFRelease(renderTexture); + renderTexture = NULL; + } +#endif + } + else + { + glDeleteTextures(1, &_texture); + } + + }); +} + +#pragma mark - +#pragma mark Usage + +- (void)activateFramebuffer +{ + glBindFramebuffer(GL_FRAMEBUFFER, framebuffer); + glViewport(0, 0, (int)_size.width, (int)_size.height); +} + +#pragma mark - +#pragma mark Reference counting + +- (void)lock +{ + if (referenceCountingDisabled) + { + return; + } + + framebufferReferenceCount++; +} + +- (void)unlock +{ + if (referenceCountingDisabled) + { + return; + } + + NSAssert(framebufferReferenceCount > 0, @"Tried to overrelease a framebuffer, did you forget to call -useNextFrameForImageCapture before using -imageFromCurrentFramebuffer?"); + framebufferReferenceCount--; + if (framebufferReferenceCount < 1) + { + [[GPUImageContext sharedFramebufferCache] returnFramebufferToCache:self]; + } +} + +- (void)clearAllLocks +{ + framebufferReferenceCount = 0; +} + +- (void)disableReferenceCounting +{ + referenceCountingDisabled = YES; +} + +- (void)enableReferenceCounting +{ + referenceCountingDisabled = NO; +} + +#pragma mark - +#pragma mark Image capture + +void dataProviderReleaseCallback (__unused void *info, const void *data, __unused size_t size) +{ + free((void *)data); +} + +void dataProviderUnlockCallback (void *info, __unused const void *data, __unused size_t size) +{ + GPUImageFramebuffer *framebuffer = (__bridge_transfer GPUImageFramebuffer*)info; + + [framebuffer restoreRenderTarget]; + [framebuffer unlock]; + [[GPUImageContext sharedFramebufferCache] removeFramebufferFromActiveImageCaptureList:framebuffer]; +} + +- (CGImageRef)newCGImageFromFramebufferContents +{ + // a CGImage can only be created from a 'normal' color texture + NSAssert(self.textureOptions.internalFormat == GL_RGBA, @"For conversion to a CGImage the output texture format for this filter must be GL_RGBA."); + NSAssert(self.textureOptions.type == GL_UNSIGNED_BYTE, @"For conversion to a CGImage the type of the output texture of this filter must be GL_UNSIGNED_BYTE."); + + __block CGImageRef cgImageFromBytes; + + runSynchronouslyOnVideoProcessingQueue(^{ + [GPUImageContext useImageProcessingContext]; + + NSUInteger totalBytesForImage = (int)_size.width * (int)_size.height * 4; + // It appears that the width of a texture must be padded out to be a multiple of 8 (32 bytes) if reading from it using a texture cache + + GLubyte *rawImagePixels; + + CGDataProviderRef dataProvider = NULL; + if ([GPUImageContext supportsFastTextureUpload]) + { +#if TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE + NSUInteger paddedWidthOfImage = (NSUInteger)(CVPixelBufferGetBytesPerRow(renderTarget) / 4.0); + NSUInteger paddedBytesForImage = paddedWidthOfImage * (int)_size.height * 4; + + glFinish(); + CFRetain(renderTarget); // I need to retain the pixel buffer here and release in the data source callback to prevent its bytes from being prematurely deallocated during a photo write operation + [self lockForReading]; + rawImagePixels = (GLubyte *)CVPixelBufferGetBaseAddress(renderTarget); + dataProvider = CGDataProviderCreateWithData((__bridge_retained void*)self, rawImagePixels, paddedBytesForImage, dataProviderUnlockCallback); + [[GPUImageContext sharedFramebufferCache] addFramebufferToActiveImageCaptureList:self]; // In case the framebuffer is swapped out on the filter, need to have a strong reference to it somewhere for it to hang on while the image is in existence +#else +#endif + } + else + { + [self activateFramebuffer]; + rawImagePixels = (GLubyte *)malloc(totalBytesForImage); + glReadPixels(0, 0, (int)_size.width, (int)_size.height, GL_RGBA, GL_UNSIGNED_BYTE, rawImagePixels); + dataProvider = CGDataProviderCreateWithData(NULL, rawImagePixels, totalBytesForImage, dataProviderReleaseCallback); + [self unlock]; // Don't need to keep this around anymore + } + + CGColorSpaceRef defaultRGBColorSpace = CGColorSpaceCreateDeviceRGB(); + + if ([GPUImageContext supportsFastTextureUpload]) + { +#if TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE + cgImageFromBytes = CGImageCreate((int)_size.width, (int)_size.height, 8, 32, CVPixelBufferGetBytesPerRow(renderTarget), defaultRGBColorSpace, kCGBitmapByteOrder32Little | kCGImageAlphaPremultipliedFirst, dataProvider, NULL, NO, kCGRenderingIntentDefault); +#else +#endif + } + else + { + cgImageFromBytes = CGImageCreate((int)_size.width, (int)_size.height, 8, 32, 4 * (int)_size.width, defaultRGBColorSpace, kCGBitmapByteOrderDefault | kCGImageAlphaPremultipliedLast, dataProvider, NULL, NO, kCGRenderingIntentDefault); + } + + // Capture image with current device orientation + CGDataProviderRelease(dataProvider); + CGColorSpaceRelease(defaultRGBColorSpace); + + }); + + return cgImageFromBytes; +} + +- (void)restoreRenderTarget +{ +#if TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE + [self unlockAfterReading]; + CFRelease(renderTarget); +#else +#endif +} + +#pragma mark - +#pragma mark Raw data bytes + +- (void)lockForReading +{ +#if TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE + if ([GPUImageContext supportsFastTextureUpload]) + { + if (readLockCount == 0) + { + CVPixelBufferLockBaseAddress(renderTarget, 0); + } + readLockCount++; + } +#endif +} + +- (void)unlockAfterReading +{ +#if TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE + if ([GPUImageContext supportsFastTextureUpload]) + { + NSAssert(readLockCount > 0, @"Unbalanced call to -[GPUImageFramebuffer unlockAfterReading]"); + readLockCount--; + if (readLockCount == 0) + { + CVPixelBufferUnlockBaseAddress(renderTarget, 0); + } + } +#endif +} + +- (NSUInteger)bytesPerRow +{ + if ([GPUImageContext supportsFastTextureUpload]) + { +#if TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE + return CVPixelBufferGetBytesPerRow(renderTarget); +#else + return _size.width * 4; // TODO: do more with this on the non-texture-cache side +#endif + } + else + { + return (NSUInteger)_size.width * 4; + } +} + +- (GLubyte *)byteBuffer +{ +#if TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE + [self lockForReading]; + GLubyte * bufferBytes = CVPixelBufferGetBaseAddress(renderTarget); + [self unlockAfterReading]; + return bufferBytes; +#else + return NULL; // TODO: do more with this on the non-texture-cache side +#endif +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/GPUImageFramebufferCache.h b/submodules/LegacyComponents/LegacyComponents/GPUImageFramebufferCache.h new file mode 100755 index 0000000000..e56a345663 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/GPUImageFramebufferCache.h @@ -0,0 +1,15 @@ +#import +#import +#import "GPUImageFramebuffer.h" + +@interface GPUImageFramebufferCache : NSObject + +// Framebuffer management +- (GPUImageFramebuffer *)fetchFramebufferForSize:(CGSize)framebufferSize textureOptions:(GPUTextureOptions)textureOptions onlyTexture:(BOOL)onlyTexture; +- (GPUImageFramebuffer *)fetchFramebufferForSize:(CGSize)framebufferSize onlyTexture:(BOOL)onlyTexture; +- (void)returnFramebufferToCache:(GPUImageFramebuffer *)framebuffer; +- (void)purgeAllUnassignedFramebuffers; +- (void)addFramebufferToActiveImageCaptureList:(GPUImageFramebuffer *)framebuffer; +- (void)removeFramebufferFromActiveImageCaptureList:(GPUImageFramebuffer *)framebuffer; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/GPUImageFramebufferCache.m b/submodules/LegacyComponents/LegacyComponents/GPUImageFramebufferCache.m new file mode 100755 index 0000000000..8b9e7f1b30 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/GPUImageFramebufferCache.m @@ -0,0 +1,189 @@ +#import "GPUImageFramebufferCache.h" +#import "GPUImageContext.h" +#import "GPUImageOutput.h" + +#if TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE +#import +#else +#endif + +@interface GPUImageFramebufferCache() +{ +// NSCache *framebufferCache; + NSMutableDictionary *framebufferCache; + NSMutableDictionary *framebufferTypeCounts; + NSMutableArray *activeImageCaptureList; // Where framebuffers that may be lost by a filter, but which are still needed for a UIImage, etc., are stored + id memoryWarningObserver; + + dispatch_queue_t framebufferCacheQueue; +} + +- (NSString *)hashForSize:(CGSize)size textureOptions:(GPUTextureOptions)textureOptions onlyTexture:(BOOL)onlyTexture; + +@end + + +@implementation GPUImageFramebufferCache + +#pragma mark - +#pragma mark Initialization and teardown + +- (id)init +{ + if (!(self = [super init])) + { + return nil; + } + +#if TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE + __weak GPUImageFramebufferCache *weakSelf = self; + memoryWarningObserver = [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidReceiveMemoryWarningNotification object:nil queue:nil usingBlock:^(__unused NSNotification *note) + { + __strong GPUImageFramebufferCache *strongSelf = weakSelf; + if (strongSelf != nil) + [strongSelf purgeAllUnassignedFramebuffers]; + }]; +#else +#endif + +// framebufferCache = [[NSCache alloc] init]; + framebufferCache = [[NSMutableDictionary alloc] init]; + framebufferTypeCounts = [[NSMutableDictionary alloc] init]; + activeImageCaptureList = [[NSMutableArray alloc] init]; + framebufferCacheQueue = dispatch_queue_create("com.sunsetlakesoftware.GPUImage.framebufferCacheQueue", NULL); + + return self; +} + +- (void)dealloc +{ + #if TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE + [[NSNotificationCenter defaultCenter] removeObserver:self]; + #endif +} + +#pragma mark - +#pragma mark Framebuffer management + +- (NSString *)hashForSize:(CGSize)size textureOptions:(GPUTextureOptions)textureOptions onlyTexture:(BOOL)onlyTexture +{ + if (onlyTexture) + { + return [NSString stringWithFormat:@"%.1fx%.1f-%d:%d:%d:%d:%d:%d:%d-NOFB", size.width, size.height, textureOptions.minFilter, textureOptions.magFilter, textureOptions.wrapS, textureOptions.wrapT, textureOptions.internalFormat, textureOptions.format, textureOptions.type]; + } + else + { + return [NSString stringWithFormat:@"%.1fx%.1f-%d:%d:%d:%d:%d:%d:%d", size.width, size.height, textureOptions.minFilter, textureOptions.magFilter, textureOptions.wrapS, textureOptions.wrapT, textureOptions.internalFormat, textureOptions.format, textureOptions.type]; + } +} + +- (GPUImageFramebuffer *)fetchFramebufferForSize:(CGSize)framebufferSize textureOptions:(GPUTextureOptions)textureOptions onlyTexture:(BOOL)onlyTexture +{ + __block GPUImageFramebuffer *framebufferFromCache = nil; +// dispatch_sync(framebufferCacheQueue, ^{ + runSynchronouslyOnVideoProcessingQueue(^{ + NSString *lookupHash = [self hashForSize:framebufferSize textureOptions:textureOptions onlyTexture:onlyTexture]; + NSNumber *numberOfMatchingTexturesInCache = [framebufferTypeCounts objectForKey:lookupHash]; + NSInteger numberOfMatchingTextures = [numberOfMatchingTexturesInCache integerValue]; + + if ([numberOfMatchingTexturesInCache integerValue] < 1) + { + // Nothing in the cache, create a new framebuffer to use + framebufferFromCache = [[GPUImageFramebuffer alloc] initWithSize:framebufferSize textureOptions:textureOptions onlyTexture:onlyTexture]; + } + else + { + // Something found, pull the old framebuffer and decrement the count + NSInteger currentTextureID = (numberOfMatchingTextures - 1); + while ((framebufferFromCache == nil) && (currentTextureID >= 0)) + { + NSString *textureHash = [NSString stringWithFormat:@"%@-%ld", lookupHash, (long)currentTextureID]; + framebufferFromCache = [framebufferCache objectForKey:textureHash]; + // Test the values in the cache first, to see if they got invalidated behind our back + if (framebufferFromCache != nil) + { + // Withdraw this from the cache while it's in use + [framebufferCache removeObjectForKey:textureHash]; + } + currentTextureID--; + } + + currentTextureID++; + + [framebufferTypeCounts setObject:[NSNumber numberWithInteger:currentTextureID] forKey:lookupHash]; + + if (framebufferFromCache == nil) + { + framebufferFromCache = [[GPUImageFramebuffer alloc] initWithSize:framebufferSize textureOptions:textureOptions onlyTexture:onlyTexture]; + } + } + }); + + [framebufferFromCache lock]; + return framebufferFromCache; +} + +- (GPUImageFramebuffer *)fetchFramebufferForSize:(CGSize)framebufferSize onlyTexture:(BOOL)onlyTexture +{ + GPUTextureOptions defaultTextureOptions; + defaultTextureOptions.minFilter = GL_LINEAR; + defaultTextureOptions.magFilter = GL_LINEAR; + defaultTextureOptions.wrapS = GL_CLAMP_TO_EDGE; + defaultTextureOptions.wrapT = GL_CLAMP_TO_EDGE; + defaultTextureOptions.internalFormat = GL_RGBA; + defaultTextureOptions.format = GL_BGRA; + defaultTextureOptions.type = GL_UNSIGNED_BYTE; + + return [self fetchFramebufferForSize:framebufferSize textureOptions:defaultTextureOptions onlyTexture:onlyTexture]; +} + +- (void)returnFramebufferToCache:(GPUImageFramebuffer *)framebuffer +{ + [framebuffer clearAllLocks]; + +// dispatch_async(framebufferCacheQueue, ^{ + runAsynchronouslyOnVideoProcessingQueue(^{ + CGSize framebufferSize = framebuffer.size; + GPUTextureOptions framebufferTextureOptions = framebuffer.textureOptions; + NSString *lookupHash = [self hashForSize:framebufferSize textureOptions:framebufferTextureOptions onlyTexture:framebuffer.missingFramebuffer]; + NSNumber *numberOfMatchingTexturesInCache = [framebufferTypeCounts objectForKey:lookupHash]; + NSInteger numberOfMatchingTextures = [numberOfMatchingTexturesInCache integerValue]; + + NSString *textureHash = [NSString stringWithFormat:@"%@-%ld", lookupHash, (long)numberOfMatchingTextures]; + +// [framebufferCache setObject:framebuffer forKey:textureHash cost:round(framebufferSize.width * framebufferSize.height * 4.0)]; + [framebufferCache setObject:framebuffer forKey:textureHash]; + [framebufferTypeCounts setObject:[NSNumber numberWithInteger:(numberOfMatchingTextures + 1)] forKey:lookupHash]; + }); +} + +- (void)purgeAllUnassignedFramebuffers +{ + runAsynchronouslyOnVideoProcessingQueue(^{ +// dispatch_async(framebufferCacheQueue, ^{ + [framebufferCache removeAllObjects]; + [framebufferTypeCounts removeAllObjects]; +#if TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE + CVOpenGLESTextureCacheFlush([[GPUImageContext sharedImageProcessingContext] coreVideoTextureCache], 0); +#else +#endif + }); +} + +- (void)addFramebufferToActiveImageCaptureList:(GPUImageFramebuffer *)framebuffer +{ + runAsynchronouslyOnVideoProcessingQueue(^{ +// dispatch_async(framebufferCacheQueue, ^{ + [activeImageCaptureList addObject:framebuffer]; + }); +} + +- (void)removeFramebufferFromActiveImageCaptureList:(GPUImageFramebuffer *)framebuffer +{ + runAsynchronouslyOnVideoProcessingQueue(^{ +// dispatch_async(framebufferCacheQueue, ^{ + [activeImageCaptureList removeObject:framebuffer]; + }); +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/GPUImageOutput.h b/submodules/LegacyComponents/LegacyComponents/GPUImageOutput.h new file mode 100755 index 0000000000..c8f3178004 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/GPUImageOutput.h @@ -0,0 +1,121 @@ +#import "GPUImageContext.h" +#import "GPUImageFramebuffer.h" + +#if TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE +#import +#else +// For now, just redefine this on the Mac +typedef NS_ENUM(NSInteger, UIImageOrientation) { + UIImageOrientationUp, // default orientation + UIImageOrientationDown, // 180 deg rotation + UIImageOrientationLeft, // 90 deg CCW + UIImageOrientationRight, // 90 deg CW + UIImageOrientationUpMirrored, // as above but image mirrored along other axis. horizontal flip + UIImageOrientationDownMirrored, // horizontal flip + UIImageOrientationLeftMirrored, // vertical flip + UIImageOrientationRightMirrored, // vertical flip +}; +#endif + +void runOnMainQueueWithoutDeadlocking(void (^block)(void)); +void runSynchronouslyOnVideoProcessingQueue(void (^block)(void)); +void runAsynchronouslyOnVideoProcessingQueue(void (^block)(void)); +void runSynchronouslyOnContextQueue(GPUImageContext *context, void (^block)(void)); +void runAsynchronouslyOnContextQueue(GPUImageContext *context, void (^block)(void)); +void reportAvailableMemoryForGPUImage(NSString *tag); + +/** GPUImage's base source object + + Images or frames of video are uploaded from source objects, which are subclasses of GPUImageOutput. These include: + + - GPUImageVideoCamera (for live video from an iOS camera) + - GPUImageStillCamera (for taking photos with the camera) + - GPUImagePicture (for still images) + - GPUImageMovie (for movies) + + Source objects upload still image frames to OpenGL ES as textures, then hand those textures off to the next objects in the processing chain. + */ +@interface GPUImageOutput : NSObject +{ + GPUImageFramebuffer *outputFramebuffer; + + NSMutableArray *targets, *targetTextureIndices; + + CGSize inputTextureSize, cachedMaximumOutputSize, forcedMaximumSize; + + BOOL overrideInputSize; + + BOOL allTargetsWantMonochromeData; + BOOL usingNextFrameForImageCapture; +} + +@property(readwrite, nonatomic) BOOL shouldSmoothlyScaleOutput; +@property(readwrite, nonatomic) BOOL shouldIgnoreUpdatesToThisTarget; +@property(readwrite, nonatomic, unsafe_unretained) id targetToIgnoreForUpdates; +@property(nonatomic, copy) void(^frameProcessingCompletionBlock)(GPUImageOutput*, CMTime); +@property(nonatomic) BOOL enabled; +@property(readwrite, nonatomic) GPUTextureOptions outputTextureOptions; + +/// @name Managing targets +- (void)setInputFramebufferForTarget:(id)target atIndex:(NSInteger)inputTextureIndex; +- (GPUImageFramebuffer *)framebufferForOutput; +- (void)removeOutputFramebuffer; +- (void)notifyTargetsAboutNewOutputTexture; + +- (CGSize)inputTextureSize; + +/** Returns an array of the current targets. + */ +- (NSArray*)targets; + +/** Adds a target to receive notifications when new frames are available. + + The target will be asked for its next available texture. + + See [GPUImageInput newFrameReadyAtTime:] + + @param newTarget Target to be added + */ +- (void)addTarget:(id)newTarget; + +/** Adds a target to receive notifications when new frames are available. + + See [GPUImageInput newFrameReadyAtTime:] + + @param newTarget Target to be added + */ +- (void)addTarget:(id)newTarget atTextureLocation:(NSInteger)textureLocation; + +/** Removes a target. The target will no longer receive notifications when new frames are available. + + @param targetToRemove Target to be removed + */ +- (void)removeTarget:(id)targetToRemove; + +/** Removes all targets. + */ +- (void)removeAllTargets; + +/// @name Manage the output texture + +- (void)forceProcessingAtSize:(CGSize)frameSize; +- (void)forceProcessingAtSizeRespectingAspectRatio:(CGSize)frameSize; + +/// @name Still image processing + +- (void)useNextFrameForImageCapture; +- (CGImageRef)newCGImageFromCurrentlyProcessedOutput; + +// Platform-specific image output methods +// If you're trying to use these methods, remember that you need to set -useNextFrameForImageCapture before running -processImage or running video and calling any of these methods, or you will get a nil image +#if TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE +- (UIImage *)imageFromCurrentFramebuffer; +- (UIImage *)imageFromCurrentFramebufferWithOrientation:(UIImageOrientation)imageOrientation; +#else +- (NSImage *)imageFromCurrentFramebuffer; +- (NSImage *)imageFromCurrentFramebufferWithOrientation:(UIImageOrientation)imageOrientation; +#endif + +- (BOOL)providesMonochromeOutput; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/GPUImageOutput.m b/submodules/LegacyComponents/LegacyComponents/GPUImageOutput.m new file mode 100755 index 0000000000..2d873f7243 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/GPUImageOutput.m @@ -0,0 +1,362 @@ +#import "GPUImageOutput.h" +//#import "GPUImagePicture.h" +#import + +void runOnMainQueueWithoutDeadlocking(void (^block)(void)) +{ + if ([NSThread isMainThread]) + { + block(); + } + else + { + dispatch_sync(dispatch_get_main_queue(), block); + } +} + +void runSynchronouslyOnVideoProcessingQueue(void (^block)(void)) +{ + dispatch_queue_t videoProcessingQueue = [GPUImageContext sharedContextQueue]; +#if !OS_OBJECT_USE_OBJC + if (dispatch_get_current_queue() == videoProcessingQueue) +#else + if (dispatch_get_specific([GPUImageContext contextKey])) +#endif + { + block(); + }else + { + dispatch_sync(videoProcessingQueue, block); + } +} + +void runAsynchronouslyOnVideoProcessingQueue(void (^block)(void)) +{ + dispatch_queue_t videoProcessingQueue = [GPUImageContext sharedContextQueue]; + +#if !OS_OBJECT_USE_OBJC + if (dispatch_get_current_queue() == videoProcessingQueue) +#else + if (dispatch_get_specific([GPUImageContext contextKey])) +#endif + { + block(); + }else + { + dispatch_async(videoProcessingQueue, block); + } +} + +void runSynchronouslyOnContextQueue(GPUImageContext *context, void (^block)(void)) +{ + dispatch_queue_t videoProcessingQueue = [context contextQueue]; +#if !OS_OBJECT_USE_OBJC + if (dispatch_get_current_queue() == videoProcessingQueue) +#else + if (dispatch_get_specific([GPUImageContext contextKey])) +#endif + { + block(); + }else + { + dispatch_sync(videoProcessingQueue, block); + } +} + +void runAsynchronouslyOnContextQueue(GPUImageContext *context, void (^block)(void)) +{ + dispatch_queue_t videoProcessingQueue = [context contextQueue]; + +#if !OS_OBJECT_USE_OBJC + if (dispatch_get_current_queue() == videoProcessingQueue) +#else + if (dispatch_get_specific([GPUImageContext contextKey])) +#endif + { + block(); + }else + { + dispatch_async(videoProcessingQueue, block); + } +} + +void reportAvailableMemoryForGPUImage(NSString *tag) +{ + if (!tag) + tag = @"Default"; + + struct task_basic_info info; + + mach_msg_type_number_t size = sizeof(info); + + kern_return_t kerr = task_info(mach_task_self(), + + TASK_BASIC_INFO, + + (task_info_t)&info, + + &size); + if( kerr == KERN_SUCCESS ) { + NSLog(@"%@ - Memory used: %u", tag, (unsigned int)info.resident_size); //in bytes + } else { + NSLog(@"%@ - Error: %s", tag, mach_error_string(kerr)); + } +} + +@implementation GPUImageOutput + +@synthesize shouldSmoothlyScaleOutput = _shouldSmoothlyScaleOutput; +@synthesize shouldIgnoreUpdatesToThisTarget = _shouldIgnoreUpdatesToThisTarget; +@synthesize targetToIgnoreForUpdates = _targetToIgnoreForUpdates; +@synthesize frameProcessingCompletionBlock = _frameProcessingCompletionBlock; +@synthesize enabled = _enabled; +@synthesize outputTextureOptions = _outputTextureOptions; + +#pragma mark - +#pragma mark Initialization and teardown + +- (id)init +{ + if (!(self = [super init])) + { + return nil; + } + + targets = [[NSMutableArray alloc] init]; + targetTextureIndices = [[NSMutableArray alloc] init]; + _enabled = YES; + allTargetsWantMonochromeData = YES; + usingNextFrameForImageCapture = NO; + + // set default texture options + _outputTextureOptions.minFilter = GL_LINEAR; + _outputTextureOptions.magFilter = GL_LINEAR; + _outputTextureOptions.wrapS = GL_CLAMP_TO_EDGE; + _outputTextureOptions.wrapT = GL_CLAMP_TO_EDGE; + _outputTextureOptions.internalFormat = GL_RGBA; + _outputTextureOptions.format = GL_BGRA; + _outputTextureOptions.type = GL_UNSIGNED_BYTE; + + return self; +} + +- (void)dealloc +{ + [self removeAllTargets]; +} + +#pragma mark - +#pragma mark Managing targets + +- (void)setInputFramebufferForTarget:(id)target atIndex:(NSInteger)inputTextureIndex +{ + [target setInputFramebuffer:[self framebufferForOutput] atIndex:inputTextureIndex]; +} + +- (GPUImageFramebuffer *)framebufferForOutput +{ + return outputFramebuffer; +} + +- (void)removeOutputFramebuffer +{ + outputFramebuffer = nil; +} + +- (void)notifyTargetsAboutNewOutputTexture +{ + for (id currentTarget in targets) + { + NSInteger indexOfObject = [targets indexOfObject:currentTarget]; + NSInteger textureIndex = [[targetTextureIndices objectAtIndex:indexOfObject] integerValue]; + + [self setInputFramebufferForTarget:currentTarget atIndex:textureIndex]; + } +} + +- (NSArray*)targets +{ + return [NSArray arrayWithArray:targets]; +} + +- (void)addTarget:(id)newTarget +{ + NSInteger nextAvailableTextureIndex = [newTarget nextAvailableTextureIndex]; + [self addTarget:newTarget atTextureLocation:nextAvailableTextureIndex]; + + if ([newTarget shouldIgnoreUpdatesToThisTarget]) + { + _targetToIgnoreForUpdates = newTarget; + } +} + +- (void)addTarget:(id)newTarget atTextureLocation:(NSInteger)textureLocation +{ + if (newTarget == nil || [targets containsObject:newTarget]) + { + return; + } + + cachedMaximumOutputSize = CGSizeZero; + runSynchronouslyOnVideoProcessingQueue(^{ + [self setInputFramebufferForTarget:newTarget atIndex:textureLocation]; + [targets addObject:newTarget]; + [targetTextureIndices addObject:[NSNumber numberWithInteger:textureLocation]]; + + allTargetsWantMonochromeData = allTargetsWantMonochromeData && [newTarget wantsMonochromeInput]; + }); +} + +- (void)removeTarget:(id)targetToRemove +{ + if(![targets containsObject:targetToRemove]) + { + return; + } + + if (_targetToIgnoreForUpdates == targetToRemove) + { + _targetToIgnoreForUpdates = nil; + } + + cachedMaximumOutputSize = CGSizeZero; + + NSInteger indexOfObject = [targets indexOfObject:targetToRemove]; + NSInteger textureIndexOfTarget = [[targetTextureIndices objectAtIndex:indexOfObject] integerValue]; + + runSynchronouslyOnVideoProcessingQueue(^{ + [targetToRemove setInputSize:CGSizeZero atIndex:textureIndexOfTarget]; + [targetToRemove setInputRotation:kGPUImageNoRotation atIndex:textureIndexOfTarget]; + + [targetTextureIndices removeObjectAtIndex:indexOfObject]; + [targets removeObject:targetToRemove]; + [targetToRemove endProcessing]; + }); +} + +- (void)removeAllTargets +{ + cachedMaximumOutputSize = CGSizeZero; + runSynchronouslyOnVideoProcessingQueue(^{ + for (id targetToRemove in targets) + { + NSInteger indexOfObject = [targets indexOfObject:targetToRemove]; + NSInteger textureIndexOfTarget = [[targetTextureIndices objectAtIndex:indexOfObject] integerValue]; + + [targetToRemove setInputSize:CGSizeZero atIndex:textureIndexOfTarget]; + [targetToRemove setInputRotation:kGPUImageNoRotation atIndex:textureIndexOfTarget]; + } + [targets removeAllObjects]; + [targetTextureIndices removeAllObjects]; + + allTargetsWantMonochromeData = YES; + }); +} + +#pragma mark - +#pragma mark Manage the output texture + +- (void)forceProcessingAtSize:(CGSize)__unused frameSize +{ + +} + +- (void)forceProcessingAtSizeRespectingAspectRatio:(CGSize)__unused frameSize +{ +} + +#pragma mark - +#pragma mark Still image processing + +- (void)useNextFrameForImageCapture +{ + +} + +- (CGImageRef)newCGImageFromCurrentlyProcessedOutput +{ + return nil; +} + +- (BOOL)providesMonochromeOutput +{ + return NO; +} + +#pragma mark - +#pragma mark Platform-specific image output methods + +#if TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE + +- (UIImage *)imageFromCurrentFramebuffer +{ + UIDeviceOrientation deviceOrientation = [[UIDevice currentDevice] orientation]; + UIImageOrientation imageOrientation = UIImageOrientationLeft; + switch (deviceOrientation) + { + case UIDeviceOrientationPortrait: + imageOrientation = UIImageOrientationUp; + break; + case UIDeviceOrientationPortraitUpsideDown: + imageOrientation = UIImageOrientationDown; + break; + case UIDeviceOrientationLandscapeLeft: + imageOrientation = UIImageOrientationLeft; + break; + case UIDeviceOrientationLandscapeRight: + imageOrientation = UIImageOrientationRight; + break; + default: + imageOrientation = UIImageOrientationUp; + break; + } + + return [self imageFromCurrentFramebufferWithOrientation:imageOrientation]; +} + +- (UIImage *)imageFromCurrentFramebufferWithOrientation:(UIImageOrientation)imageOrientation +{ + CGImageRef cgImageFromBytes = [self newCGImageFromCurrentlyProcessedOutput]; + UIImage *finalImage = [UIImage imageWithCGImage:cgImageFromBytes scale:1.0 orientation:imageOrientation]; + CGImageRelease(cgImageFromBytes); + + return finalImage; +} + +- (CGSize)inputTextureSize +{ + return inputTextureSize; +} + +#else + +- (NSImage *)imageFromCurrentFramebuffer; +{ + return [self imageFromCurrentFramebufferWithOrientation:UIImageOrientationLeft]; +} + +- (NSImage *)imageFromCurrentFramebufferWithOrientation:(UIImageOrientation)imageOrientation; +{ + CGImageRef cgImageFromBytes = [self newCGImageFromCurrentlyProcessedOutput]; + NSImage *finalImage = [[NSImage alloc] initWithCGImage:cgImageFromBytes size:NSZeroSize]; + CGImageRelease(cgImageFromBytes); + + return finalImage; +} + +- (NSImage *)imageByFilteringImage:(NSImage *)imageToFilter; +{ + CGImageRef image = [self newCGImageByFilteringCGImage:[imageToFilter CGImageForProposedRect:NULL context:[NSGraphicsContext currentContext] hints:nil]]; + NSImage *processedImage = [[NSImage alloc] initWithCGImage:image size:NSZeroSize]; + CGImageRelease(image); + return processedImage; +} + +- (CGImageRef)newCGImageByFilteringImage:(NSImage *)imageToFilter +{ + return [self newCGImageByFilteringCGImage:[imageToFilter CGImageForProposedRect:NULL context:[NSGraphicsContext currentContext] hints:nil]]; +} + +#endif + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/GPUImageTwoInputFilter.h b/submodules/LegacyComponents/LegacyComponents/GPUImageTwoInputFilter.h new file mode 100755 index 0000000000..da3a1345e5 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/GPUImageTwoInputFilter.h @@ -0,0 +1,21 @@ +#import "GPUImageFilter.h" + +extern NSString *const kGPUImageTwoInputTextureVertexShaderString; + +@interface GPUImageTwoInputFilter : GPUImageFilter +{ + GPUImageFramebuffer *secondInputFramebuffer; + + GLint filterSecondTextureCoordinateAttribute; + GLint filterInputTextureUniform2; + GPUImageRotationMode inputRotation2; + CMTime firstFrameTime, secondFrameTime; + + BOOL hasSetFirstTexture, hasReceivedFirstFrame, hasReceivedSecondFrame, firstFrameWasVideo, secondFrameWasVideo; + BOOL firstFrameCheckDisabled, secondFrameCheckDisabled; +} + +- (void)disableFirstFrameCheck; +- (void)disableSecondFrameCheck; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/GPUImageTwoInputFilter.m b/submodules/LegacyComponents/LegacyComponents/GPUImageTwoInputFilter.m new file mode 100755 index 0000000000..41dea616f9 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/GPUImageTwoInputFilter.m @@ -0,0 +1,264 @@ +#import "GPUImageTwoInputFilter.h" + +NSString *const kGPUImageTwoInputTextureVertexShaderString = SHADER_STRING +( + attribute vec4 position; + attribute vec4 inputTexCoord; + attribute vec4 inputTexCoord2; + + varying vec2 texCoord; + varying vec2 texCoord2; + + void main() + { + gl_Position = position; + texCoord = inputTexCoord.xy; + texCoord2 = inputTexCoord2.xy; + } +); + + +@implementation GPUImageTwoInputFilter + +#pragma mark - +#pragma mark Initialization and teardown + +- (id)initWithFragmentShaderFromString:(NSString *)fragmentShaderString +{ + if (!(self = [self initWithVertexShaderFromString:kGPUImageTwoInputTextureVertexShaderString fragmentShaderFromString:fragmentShaderString])) + { + return nil; + } + + return self; +} + +- (id)initWithVertexShaderFromString:(NSString *)vertexShaderString fragmentShaderFromString:(NSString *)fragmentShaderString +{ + if (!(self = [super initWithVertexShaderFromString:vertexShaderString fragmentShaderFromString:fragmentShaderString])) + { + return nil; + } + + inputRotation2 = kGPUImageNoRotation; + + hasSetFirstTexture = NO; + + hasReceivedFirstFrame = NO; + hasReceivedSecondFrame = NO; + firstFrameWasVideo = NO; + secondFrameWasVideo = NO; + firstFrameCheckDisabled = NO; + secondFrameCheckDisabled = NO; + + firstFrameTime = kCMTimeInvalid; + secondFrameTime = kCMTimeInvalid; + + runSynchronouslyOnVideoProcessingQueue(^{ + [GPUImageContext useImageProcessingContext]; + filterSecondTextureCoordinateAttribute = [filterProgram attributeIndex:@"inputTexCoord2"]; + + filterInputTextureUniform2 = [filterProgram uniformIndex:@"inputImageTexture2"]; + glEnableVertexAttribArray(filterSecondTextureCoordinateAttribute); + }); + + return self; +} + +- (void)initializeAttributes +{ + [super initializeAttributes]; + [filterProgram addAttribute:@"inputTexCoord2"]; +} + +- (void)disableFirstFrameCheck +{ + firstFrameCheckDisabled = YES; +} + +- (void)disableSecondFrameCheck +{ + secondFrameCheckDisabled = YES; +} + +#pragma mark - +#pragma mark Rendering + +- (void)renderToTextureWithVertices:(const GLfloat *)vertices textureCoordinates:(const GLfloat *)textureCoordinates +{ + if (self.preventRendering) + { + [firstInputFramebuffer unlock]; + [secondInputFramebuffer unlock]; + return; + } + + [GPUImageContext setActiveShaderProgram:filterProgram]; + outputFramebuffer = [[GPUImageContext sharedFramebufferCache] fetchFramebufferForSize:[self sizeOfFBO] textureOptions:self.outputTextureOptions onlyTexture:NO]; + [outputFramebuffer activateFramebuffer]; + if (usingNextFrameForImageCapture) + { + [outputFramebuffer lock]; + } + + [self setUniformsForProgramAtIndex:0]; + + glClearColor(backgroundColorRed, backgroundColorGreen, backgroundColorBlue, backgroundColorAlpha); + glClear(GL_COLOR_BUFFER_BIT); + + glActiveTexture(GL_TEXTURE2); + glBindTexture(GL_TEXTURE_2D, [firstInputFramebuffer texture]); + glUniform1i(filterInputTextureUniform, 2); + + glActiveTexture(GL_TEXTURE3); + glBindTexture(GL_TEXTURE_2D, [secondInputFramebuffer texture]); + glUniform1i(filterInputTextureUniform2, 3); + + glVertexAttribPointer(filterPositionAttribute, 2, GL_FLOAT, 0, 0, vertices); + glVertexAttribPointer(filterTextureCoordinateAttribute, 2, GL_FLOAT, 0, 0, textureCoordinates); + glVertexAttribPointer(filterSecondTextureCoordinateAttribute, 2, GL_FLOAT, 0, 0, [[self class] textureCoordinatesForRotation:inputRotation2]); + + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); + + [firstInputFramebuffer unlock]; + [secondInputFramebuffer unlock]; + if (usingNextFrameForImageCapture) + { + dispatch_semaphore_signal(imageCaptureSemaphore); + } +} + +#pragma mark - +#pragma mark GPUImageInput + +- (NSInteger)nextAvailableTextureIndex +{ + if (hasSetFirstTexture) + { + return 1; + } + else + { + return 0; + } +} + +- (void)setInputFramebuffer:(GPUImageFramebuffer *)newInputFramebuffer atIndex:(NSInteger)textureIndex +{ + if (textureIndex == 0) + { + firstInputFramebuffer = newInputFramebuffer; + hasSetFirstTexture = YES; + [firstInputFramebuffer lock]; + } + else + { + secondInputFramebuffer = newInputFramebuffer; + [secondInputFramebuffer lock]; + } +} + +- (void)setInputSize:(CGSize)newSize atIndex:(NSInteger)textureIndex +{ + if (textureIndex == 0) + { + [super setInputSize:newSize atIndex:textureIndex]; + + if (CGSizeEqualToSize(newSize, CGSizeZero)) + { + hasSetFirstTexture = NO; + } + } +} + +- (void)setInputRotation:(GPUImageRotationMode)newInputRotation atIndex:(NSInteger)textureIndex +{ + if (textureIndex == 0) + { + inputRotation = newInputRotation; + } + else + { + inputRotation2 = newInputRotation; + } +} + +- (CGSize)rotatedSize:(CGSize)sizeToRotate forIndex:(NSInteger)textureIndex +{ + CGSize rotatedSize = sizeToRotate; + + GPUImageRotationMode rotationToCheck; + if (textureIndex == 0) + { + rotationToCheck = inputRotation; + } + else + { + rotationToCheck = inputRotation2; + } + + if (GPUImageRotationSwapsWidthAndHeight(rotationToCheck)) + { + rotatedSize.width = sizeToRotate.height; + rotatedSize.height = sizeToRotate.width; + } + + return rotatedSize; +} + +- (void)newFrameReadyAtTime:(CMTime)frameTime atIndex:(NSInteger)textureIndex +{ + // You can set up infinite update loops, so this helps to short circuit them + if (hasReceivedFirstFrame && hasReceivedSecondFrame) + { + return; + } + + BOOL updatedMovieFrameOppositeStillImage = NO; + + if (textureIndex == 0) + { + hasReceivedFirstFrame = YES; + firstFrameTime = frameTime; + if (secondFrameCheckDisabled) + { + hasReceivedSecondFrame = YES; + } + + if (!CMTIME_IS_INDEFINITE(frameTime)) + { + if CMTIME_IS_INDEFINITE(secondFrameTime) + { + updatedMovieFrameOppositeStillImage = YES; + } + } + } + else + { + hasReceivedSecondFrame = YES; + secondFrameTime = frameTime; + if (firstFrameCheckDisabled) + { + hasReceivedFirstFrame = YES; + } + + if (!CMTIME_IS_INDEFINITE(frameTime)) + { + if CMTIME_IS_INDEFINITE(firstFrameTime) + { + updatedMovieFrameOppositeStillImage = YES; + } + } + } + + // || (hasReceivedFirstFrame && secondFrameCheckDisabled) || (hasReceivedSecondFrame && firstFrameCheckDisabled) + if ((hasReceivedFirstFrame && hasReceivedSecondFrame)) + { + CMTime passOnFrameTime = (!CMTIME_IS_INDEFINITE(firstFrameTime)) ? firstFrameTime : secondFrameTime; + [super newFrameReadyAtTime:passOnFrameTime atIndex:0]; // Bugfix when trying to record: always use time from first input (unless indefinite, in which case use the second input) + hasReceivedFirstFrame = NO; + hasReceivedSecondFrame = NO; + } +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/HPGrowingTextView.h b/submodules/LegacyComponents/LegacyComponents/HPGrowingTextView.h new file mode 100644 index 0000000000..f5f84d3e67 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/HPGrowingTextView.h @@ -0,0 +1,116 @@ + + +// +// HPTextView.h +// +// Created by Hans Pinckaers on 29-06-10. +// +// MIT License +// +// Copyright (c) 2011 Hans Pinckaers +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#import + +@class HPGrowingTextView; +@class HPTextViewInternal; + +extern NSString *TGMentionUidAttributeName; +extern NSString *TGMentionBoldAttributeName; +@class TGMessageEntity; + +@class TGKeyCommandController; + +@protocol HPGrowingTextViewDelegate + +@optional + +- (BOOL)growingTextViewShouldBeginEditing:(HPGrowingTextView *)growingTextView; +- (void)growingTextViewDidBeginEditing:(HPGrowingTextView *)growingTextView; +- (void)growingTextViewDidEndEditing:(HPGrowingTextView *)growingTextView; +- (BOOL)growingTextViewEnabled:(HPGrowingTextView *)growingTextView; + +- (BOOL)growingTextView:(HPGrowingTextView *)growingTextView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text; +- (void)growingTextViewDidChange:(HPGrowingTextView *)growingTextView afterSetText:(bool)afterSetText afterPastingText:(bool)afterPastingText; + +- (void)growingTextView:(HPGrowingTextView *)growingTextView willChangeHeight:(CGFloat)height duration:(NSTimeInterval)duration animationCurve:(int)animationCurve; + +- (void)growingTextViewDidChangeSelection:(HPGrowingTextView *)growingTextView; +- (BOOL)growingTextViewShouldReturn:(HPGrowingTextView *)growingTextView; + +- (void)growingTextView:(HPGrowingTextView *)growingTextView didPasteImages:(NSArray *)images andText:(NSString *)text; +- (void)growingTextView:(HPGrowingTextView *)growingTextView didPasteData:(NSData *)data; + +- (void)growingTextView:(HPGrowingTextView *)growingTextView receivedReturnKeyCommandWithModifierFlags:(UIKeyModifierFlags)flags; + +@end + +@interface TGAttributedTextRange : NSObject + +@property (nonatomic, strong, readonly) id attachment; + +- (instancetype)initWithAttachment:(id)attachment; + +@end + +@interface HPGrowingTextView : UIView + +@property (nonatomic, strong) UIView *placeholderView; +@property (nonatomic, assign) bool showPlaceholderWhenFocussed; + +@property (nonatomic) int minNumberOfLines; +@property (nonatomic) int maxNumberOfLines; +@property (nonatomic) CGFloat maxHeight; +@property (nonatomic) CGFloat minHeight; +@property (nonatomic) BOOL animateHeightChange; +@property (nonatomic) NSTimeInterval animationDuration; +@property (nonatomic, strong) HPTextViewInternal *internalTextView; +@property (nonatomic, assign) bool disableFormatting; + +@property (nonatomic) bool oneTimeLongAnimation; + +@property (nonatomic, weak) id delegate; +@property (nonatomic, strong) NSString *text; +@property (nonatomic, strong) NSAttributedString *attributedText; +@property (nonatomic, strong) UIFont *font; +@property (nonatomic, strong) UIColor *textColor; +@property (nonatomic, strong) UIColor *accentColor; +@property (nonatomic) NSTextAlignment textAlignment; + +@property (nonatomic, readonly) bool ignoreChangeNotification; + +@property (nonatomic, assign) bool receiveKeyCommands; + +- (instancetype)initWithKeyCommandController:(TGKeyCommandController *)keyCommandController; + +- (void)refreshHeight:(bool)textChanged; +- (void)notifyHeight; + +- (void)setText:(NSString *)newText animated:(bool)animated; +- (void)setAttributedText:(NSAttributedString *)newText animated:(bool)animated; +- (void)setAttributedText:(NSAttributedString *)newText keepFormatting:(bool)keepFormatting animated:(bool)animated; +- (void)selectRange:(NSRange)range force:(bool)force; + +- (NSString *)textWithEntities:(__autoreleasing NSArray **)entities; + ++ (void)replaceMention:(NSString *)mention inputField:(HPGrowingTextView *)inputField username:(bool)username userId:(int32_t)userId; ++ (void)replaceHashtag:(NSString *)hashtag inputField:(HPGrowingTextView *)inputField; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/HPGrowingTextView.m b/submodules/LegacyComponents/LegacyComponents/HPGrowingTextView.m new file mode 100644 index 0000000000..c31e3cbc5e --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/HPGrowingTextView.m @@ -0,0 +1,918 @@ +#import "HPGrowingTextView.h" + +#import "LegacyComponentsInternal.h" +#import "TGFont.h" + +#import "HPTextViewInternal.h" + +#import "TGInputTextTag.h" + +#import "TGMessage.h" + +#import "TGColor.h" + +NSString *TGMentionUidAttributeName = @"TGMentionUidAttributeName"; +NSString *TGMentionBoldAttributeName = @"TGMentionBoldAttributeName"; + +@implementation TGAttributedTextRange + +- (instancetype)initWithAttachment:(id)attachment { + self = [super init]; + if (self != nil) { + _attachment = attachment; + } + return self; +} + +@end + +@interface HPGrowingTextView () +{ + UIColor *_intrinsicTextColor; + UIFont *_intrinsicTextFont; + + __weak TGKeyCommandController *_keyCommandController; +} + +@end + +@implementation HPGrowingTextView + +- (instancetype)initWithKeyCommandController:(TGKeyCommandController *)keyCommandController +{ + self = [super initWithFrame:CGRectZero]; + if (self != nil) + { + _keyCommandController = keyCommandController; + + [self commonInitialiser]; + } + return self; +} + +- (NSDictionary *)defaultAttributes { + if (_intrinsicTextFont == nil) { + return @{NSFontAttributeName: TGSystemFontOfSize(17)}; + } else { + if (_intrinsicTextColor) + return @{NSFontAttributeName: _intrinsicTextFont, NSForegroundColorAttributeName: _intrinsicTextColor}; + else + return @{NSFontAttributeName: _intrinsicTextFont}; + } +} + +- (void)commonInitialiser +{ + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^ + { + [HPTextViewInternal addTextViewMethods]; + }); + + CGRect frame = self.frame; + frame.origin = CGPointZero; + _internalTextView = [[HPTextViewInternal alloc] initWithKeyCommandController:_keyCommandController]; + _internalTextView.frame = frame; + _internalTextView.delegate = self; + _internalTextView.contentInset = UIEdgeInsetsZero; + _internalTextView.showsHorizontalScrollIndicator = NO; + _internalTextView.attributedText = [[NSAttributedString alloc] initWithString:@"-" attributes:[self defaultAttributes]]; + _internalTextView.scrollsToTop = false; + if (iosMajorVersion() >= 7) { + _internalTextView.textContainer.layoutManager.allowsNonContiguousLayout = true; + _internalTextView.allowsEditingTextAttributes = true; + } + [self addSubview:_internalTextView]; + + _minHeight = _internalTextView.frame.size.height; + _minNumberOfLines = 1; + + _animateHeightChange = true; + _animationDuration = 0.1f; + + _internalTextView.attributedText = [[NSAttributedString alloc] initWithString:@"" attributes:[self defaultAttributes]]; +} + +- (void)setDisableFormatting:(bool)disableFormatting +{ + _disableFormatting = disableFormatting; + if (iosMajorVersion() >= 7) + _internalTextView.allowsEditingTextAttributes = !disableFormatting; +} + +- (void)setFrame:(CGRect)frame +{ + [super setFrame:frame]; + + frame.origin = CGPointZero; + _internalTextView.frame = frame; +} + +- (CGSize)sizeThatFits:(CGSize)size +{ + if (self.attributedText.length == 0) + size.height = _minHeight; + + return size; +} + +- (void)setMaxNumberOfLines:(int)maxNumberOfLines +{ + if (maxNumberOfLines == 0 && _maxHeight > 0) // the user specified a maxHeight themselves. + return; + + // Use internalTextView for height calculations, thanks to Gwynne + NSAttributedString *saveText = _internalTextView.attributedText; + NSMutableAttributedString *newText = [[NSMutableAttributedString alloc] initWithString:@"-" attributes:[self defaultAttributes]]; + + _internalTextView.delegate = nil; + _internalTextView.hidden = YES; + + for (int i = 1; i < maxNumberOfLines; ++i) { + [newText appendAttributedString:[[NSAttributedString alloc] initWithString:@"\n|W|"]]; + } + + _internalTextView.attributedText = newText; + + _maxHeight = [self measureHeight]; + + _internalTextView.attributedText = saveText; + _internalTextView.hidden = NO; + _internalTextView.delegate = self; + + [self sizeToFit]; + + _maxNumberOfLines = maxNumberOfLines; +} + +- (void)setMaxHeight:(CGFloat)maxHeight +{ + _maxHeight = maxHeight; + _maxNumberOfLines = 0; +} + +- (void)setMinNumberOfLines:(int)minNumberOfLines +{ + if (minNumberOfLines == 0 && _minHeight > 0) // the user specified a minHeight themselves. + return; + + // Use internalTextView for height calculations, thanks to Gwynne + NSAttributedString *saveText = _internalTextView.attributedText; + NSMutableAttributedString *newText = [[NSMutableAttributedString alloc] initWithString:@"-" attributes:[self defaultAttributes]]; + + _internalTextView.delegate = nil; + _internalTextView.hidden = YES; + + for (int i = 1; i < minNumberOfLines; ++i) { + [newText appendAttributedString:[[NSAttributedString alloc] initWithString:@"\n|W|"]]; + } + + _internalTextView.attributedText = newText; + + _minHeight = [self measureHeight]; + + _internalTextView.attributedText = saveText; + _internalTextView.hidden = NO; + _internalTextView.delegate = self; + + [self sizeToFit]; + + _minNumberOfLines = minNumberOfLines; +} + +- (void)setMinHeight:(CGFloat)minHeight +{ + _minHeight = minHeight; + _minNumberOfLines = 0; +} + +- (void)textViewDidChange:(UITextView *)__unused textView +{ + [self refreshAttributes]; + + [self refreshHeight:true]; + if (self.showPlaceholderWhenFocussed) + _placeholderView.hidden = [_internalTextView hasText]; +} + +- (void)textViewDidChangeSelection:(UITextView *)__unused textView +{ + id delegate = _delegate; + + if ([delegate respondsToSelector:@selector(growingTextViewDidChangeSelection:)]) + [delegate growingTextViewDidChangeSelection:self]; + + //_internalTextView.typingAttributes = [self defaultAttributes]; + + if ([_internalTextView selectedRange].length == 0) { + //[self refreshAttributes]; + } +} + +- (void)refreshHeight:(bool)textChanged +{ + CGFloat newSizeH = [self measureHeight]; //size of content, so we can set the frame of self + + if(newSizeH < _minHeight || !_internalTextView.hasText) + newSizeH = _minHeight; //not smalles than minHeight + + if (_internalTextView.frame.size.height > _maxHeight) + newSizeH = _maxHeight; // not taller than maxHeight + + id delegate = _delegate; + + if (ABS(_internalTextView.frame.size.height - newSizeH) > FLT_EPSILON || _oneTimeLongAnimation) + { + // [fixed] Pasting too much text into the view failed to fire the height change, + // thanks to Gwynne + + if (newSizeH > _maxHeight && _internalTextView.frame.size.height <= _maxHeight) + newSizeH = _maxHeight; + + if (newSizeH <= _maxHeight) + { + if (_animateHeightChange && !_internalTextView.isPasting) + { + NSTimeInterval currentAnimationDuration = 0.12; + if (_oneTimeLongAnimation) + { + _oneTimeLongAnimation = false; + currentAnimationDuration = 0.3; + if (iosMajorVersion() < 7) + currentAnimationDuration *= 0.7; + } + + [UIView animateWithDuration:currentAnimationDuration delay:0 options:(UIViewAnimationOptionAllowUserInteraction| UIViewAnimationOptionBeginFromCurrentState) animations:^ + { + [self resizeTextView:newSizeH]; + } completion:nil]; + + if ([delegate respondsToSelector:@selector(growingTextView:willChangeHeight:duration:animationCurve:)]) + [delegate growingTextView:self willChangeHeight:newSizeH duration:currentAnimationDuration animationCurve:0]; + } + else + { + [self resizeTextView:newSizeH]; + + if ([delegate respondsToSelector:@selector(growingTextView:willChangeHeight:duration:animationCurve:)]) + [delegate growingTextView:self willChangeHeight:newSizeH duration:0.0 animationCurve:0]; + } + } + + // scroll to caret (needed on iOS7) + if (iosMajorVersion() >= 7) + { + /*NSRange range = _internalTextView.selectedRange; + [_internalTextView _scrollRangeToVisible:range animated:false]; + + CGRect r = [_internalTextView caretRectForPosition:_internalTextView.selectedTextRange.end]; + CGFloat frameHeight = _internalTextView.frame.size.height; + CGFloat caretY = MAX(r.origin.y - frameHeight + r.size.height + 8, 0); + if (r.origin.y != INFINITY) + { + CGPoint contentOffset = _internalTextView.contentOffset; + contentOffset.y = caretY; + _internalTextView.contentOffset = contentOffset; + }*/ + } + } + + if (textChanged && [delegate respondsToSelector:@selector(growingTextViewDidChange:afterSetText:afterPastingText:)]) { + [delegate growingTextViewDidChange:self afterSetText:_ignoreChangeNotification afterPastingText:_internalTextView.isPasting]; + } + + _oneTimeLongAnimation = false; +} + +- (void)notifyHeight { + id delegate = _delegate; + if ([delegate respondsToSelector:@selector(growingTextView:willChangeHeight:duration:animationCurve:)]) + [delegate growingTextView:self willChangeHeight:_internalTextView.frame.size.height duration:0.0 animationCurve:0]; +} + +// Code from apple developer forum - @Steve Krulewitz, @Mark Marszal, @Eric Silverberg +- (CGFloat)measureHeight +{ + if (iosMajorVersion() >= 7) + { + CGRect frame = _internalTextView.bounds; + CGSize fudgeFactor = CGSizeMake(10.0, 17.0); + + frame.size.height -= fudgeFactor.height; + frame.size.width -= fudgeFactor.width; + + frame.size.width -= _internalTextView.textContainerInset.right; + + NSMutableAttributedString *textToMeasure = [[NSMutableAttributedString alloc] initWithAttributedString:_internalTextView.attributedText]; + if ([textToMeasure.string hasSuffix:@"\n"]) + { + [textToMeasure appendAttributedString:[[NSAttributedString alloc] initWithString:@"-"]]; + } + [textToMeasure removeAttribute:NSFontAttributeName range:NSMakeRange(0, textToMeasure.length)]; + if (_intrinsicTextFont != nil) { + [textToMeasure addAttribute:NSFontAttributeName value:_intrinsicTextFont range:NSMakeRange(0, textToMeasure.length)]; + } + + // NSString class method: boundingRectWithSize:options:attributes:context is + // available only on ios7.0 sdk. + CGRect size = [textToMeasure boundingRectWithSize:CGSizeMake(CGRectGetWidth(frame), MAXFLOAT) + options:NSStringDrawingUsesLineFragmentOrigin + //attributes:attributes + context:nil]; + + return CGFloor(CGRectGetHeight(size) + fudgeFactor.height); + } + else + { + return CGFloor(self.internalTextView.contentSize.height); + } +} + +- (void)resizeTextView:(CGFloat)newSizeH +{ + CGRect internalTextViewFrame = self.frame; + internalTextViewFrame.size.height = CGFloor(newSizeH); + self.frame = internalTextViewFrame; + + internalTextViewFrame.origin = CGPointZero; + if(!CGRectEqualToRect(_internalTextView.frame, internalTextViewFrame)) + _internalTextView.frame = internalTextViewFrame; + + //[_internalTextView textViewEnsureSelectionVisible]; +} + +- (BOOL)becomeFirstResponder +{ + return [_internalTextView becomeFirstResponder]; +} + +- (BOOL)resignFirstResponder +{ + return [_internalTextView resignFirstResponder]; +} + +- (BOOL)isFirstResponder +{ + return [_internalTextView isFirstResponder]; +} + +- (BOOL)canBecomeFirstResponder +{ + return [_internalTextView canBecomeFirstResponder]; +} + +- (void)setText:(NSString *)newText +{ + [self setText:newText animated:true]; +} + +- (void)setAttributedText:(NSAttributedString *)attributedText { + [self setAttributedText:attributedText animated:true]; +} + +- (NSAttributedString *)attributedText { + return _internalTextView.attributedText; +} + +- (void)setText:(NSString *)newText animated:(bool)animated { + [self setAttributedText:[[NSAttributedString alloc] initWithString:newText == nil ? @"" : newText attributes:[self defaultAttributes]] animated:animated]; +} + +- (void)setAttributedText:(NSAttributedString *)newText animated:(bool)animated { + [self setAttributedText:newText keepFormatting:false animated:animated]; +} + +- (void)setAttributedText:(NSAttributedString *)newText keepFormatting:(bool)keepFormatting animated:(bool)animated { + NSMutableAttributedString *fixedFontString = [[NSMutableAttributedString alloc] initWithAttributedString:newText]; + if (!keepFormatting) + { + [fixedFontString removeAttribute:NSFontAttributeName range:NSMakeRange(0, fixedFontString.length)]; + [fixedFontString addAttribute:NSFontAttributeName value:_intrinsicTextFont == nil ? [UIFont systemFontOfSize:17.0f] : _intrinsicTextFont range:NSMakeRange(0, fixedFontString.length)]; + } + + _internalTextView.attributedText = fixedFontString; + _internalTextView.typingAttributes = [self defaultAttributes]; + [self refreshAttributes]; + + _placeholderView.hidden = fixedFontString.length != 0 || [_internalTextView isFirstResponder]; + + // include this line to analyze the height of the textview. + // fix from Ankit Thakur + + bool previousAnimateHeightChange = _animateHeightChange; + _animateHeightChange = animated; + _ignoreChangeNotification = true; + [self performSelector:@selector(textViewDidChange:) withObject:_internalTextView]; + _ignoreChangeNotification = false; + _animateHeightChange = previousAnimateHeightChange; +} + +- (void)selectRange:(NSRange)range force:(bool)force { + if (range.length != 0 || force) { + UITextPosition *startPosition = [_internalTextView positionFromPosition:_internalTextView.beginningOfDocument offset:range.location]; + UITextPosition *endPosition = [_internalTextView positionFromPosition:_internalTextView.beginningOfDocument offset:range.location + range.length]; + UITextRange *selection = [_internalTextView textRangeFromPosition:startPosition toPosition:endPosition]; + _internalTextView.selectedTextRange = selection; + } +} + +-(NSString *)text +{ + return _internalTextView.text; +} + +- (void)setFont:(UIFont *)afont +{ + _internalTextView.font = afont; + _intrinsicTextFont = afont; + + [self setMaxNumberOfLines:_maxNumberOfLines]; + [self setMinNumberOfLines:_minNumberOfLines]; +} + +- (UIFont *)font +{ + return _intrinsicTextFont; +} + +- (void)setTextColor:(UIColor *)color +{ + _internalTextView.textColor = color; + _intrinsicTextColor = color; +} + +- (UIColor *)textColor +{ + return _internalTextView.textColor; +} + +- (UIColor *)accentColor +{ + if (_accentColor != nil) + return _accentColor; + + return TGAccentColor(); +} + +- (void)setTextAlignment:(NSTextAlignment)aligment +{ + _internalTextView.textAlignment = aligment; +} + +- (NSTextAlignment)textAlignment +{ + return _internalTextView.textAlignment; +} + +#pragma mark - + +- (BOOL)textViewShouldBeginEditing:(UITextView *)__unused textView +{ + id delegate = _delegate; + + if ([delegate respondsToSelector:@selector(growingTextViewShouldBeginEditing:)]) + return [delegate growingTextViewShouldBeginEditing:self]; + + return true; +} + +- (void)textViewDidBeginEditing:(UITextView *)__unused textView +{ + id delegate = _delegate; + + if ([delegate respondsToSelector:@selector(growingTextViewDidBeginEditing:)]) + [delegate growingTextViewDidBeginEditing:self]; + + if (!self.showPlaceholderWhenFocussed && ![_internalTextView hasText]) + _placeholderView.hidden = true; +} + +- (void)textViewDidEndEditing:(UITextView *)__unused textView +{ + id delegate = _delegate; + + if ([delegate respondsToSelector:@selector(growingTextViewDidEndEditing:)]) + [delegate growingTextViewDidEndEditing:self]; + + if (!self.showPlaceholderWhenFocussed) + _placeholderView.hidden = [_internalTextView hasText]; +} + +- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)atext +{ + id delegate = _delegate; + + if ([delegate respondsToSelector:@selector(growingTextViewEnabled:)]) { + if (![delegate growingTextViewEnabled:self]) { + return false; + } + } + + if (![textView hasText] && [atext isEqualToString:@""]) + return NO; + + if ([atext isEqualToString:@"\n"]) + { + id delegate = _delegate; + + if ([delegate respondsToSelector:@selector(growingTextViewShouldReturn:)]) + return (BOOL)[delegate performSelector:@selector(growingTextViewShouldReturn:) withObject:self]; + } + + if (atext.length == 0) { + if (range.location != 0 && range.length == 1) { + NSAttributedString *string = self.attributedText; + if ([string attributesAtIndex:range.location effectiveRange:nil][NSAttachmentAttributeName] != nil) { + NSMutableAttributedString *mutableString = [[NSMutableAttributedString alloc] initWithAttributedString:string]; + [mutableString replaceCharactersInRange:NSMakeRange(range.location - 1, 1) withString:@""]; + NSRange badRange = [mutableString.string rangeOfString:@"\uFFFC"]; + if (badRange.location != NSNotFound) + [mutableString replaceCharactersInRange:badRange withString:@""]; + self.attributedText = mutableString; + + return false; + } + } + } else if (range.length == 0) { + NSAttributedString *string = self.attributedText; + if (range.location != 0 && [string attributesAtIndex:range.location - 1 effectiveRange:nil][NSAttachmentAttributeName] != nil) { + NSMutableAttributedString *mutableString = [[NSMutableAttributedString alloc] initWithAttributedString:string]; + [mutableString replaceCharactersInRange:NSMakeRange(range.location - 1, 0) withString:atext]; + NSRange badRange = [mutableString.string rangeOfString:@"\uFFFC"]; + if (badRange.location != NSNotFound) + [mutableString replaceCharactersInRange:badRange withString:@""]; + self.attributedText = mutableString; + + return false; + } + } + + return true; +} + +- (void)keyCommandPressed:(UIKeyCommand *)keyCommand +{ + id delegate = _delegate; + + if ([delegate respondsToSelector:@selector(growingTextView:receivedReturnKeyCommandWithModifierFlags:)]) + [delegate growingTextView:self receivedReturnKeyCommandWithModifierFlags:keyCommand.modifierFlags]; +} + +- (NSArray *)keyCommands +{ + if (!self.receiveKeyCommands) + return nil; + + return @ + [ + [UIKeyCommand keyCommandWithInput:@"\r" modifierFlags:0 action:@selector(keyCommandPressed:)], + [UIKeyCommand keyCommandWithInput:@"\r" modifierFlags:UIKeyModifierAlternate action:@selector(keyCommandPressed:)] + ]; +} + +- (void)refreshAttributes { + if (iosMajorVersion() < 7) { + return; + } + + self.internalTextView.typingAttributes = [self defaultAttributes]; + + NSAttributedString *string = self.attributedText; + if (string.length == 0) { + return; + } + + CGPoint contentOffset = _internalTextView.contentOffset; + [_internalTextView setScrollEnabled:false]; + _internalTextView.disableContentOffsetAnimation = true; + _internalTextView.freezeContentOffset = true; + + //NSMutableAttributedString *mutableString = [[NSMutableAttributedString alloc] initWithAttributedString:string]; + //[mutableString removeAttribute:NSForegroundColorAttributeName range:NSMakeRange(0, string.length)]; + [_internalTextView.textStorage removeAttribute:NSForegroundColorAttributeName range:NSMakeRange(0, string.length)]; + + if (_intrinsicTextColor != nil) { + //[mutableString addAttribute:NSForegroundColorAttributeName value:_intrinsicTextColor == nil ? [UIColor blackColor] : _intrinsicTextColor range:NSMakeRange(0, string.length)]; + [_internalTextView.textStorage addAttribute:NSForegroundColorAttributeName value:_intrinsicTextColor == nil ? [UIColor blackColor] : _intrinsicTextColor range:NSMakeRange(0, string.length)]; + } + + __block NSMutableArray *inputTextTags = [[NSMutableArray alloc] init]; + [string enumerateAttribute:TGMentionUidAttributeName inRange:NSMakeRange(0, string.length) options:0 usingBlock:^(__unused id value, NSRange range, __unused BOOL *stop) { + if ([value isKindOfClass:[TGInputTextTag class]]) { + [inputTextTags addObject:[[TGInputTextTagAndRange alloc] initWithTag:value range:range]]; + } + }]; + + [string enumerateAttribute:TGMentionBoldAttributeName inRange:NSMakeRange(0, string.length) options:0 usingBlock:^(__unused id value, NSRange range, __unused BOOL *stop) { + if ([value isKindOfClass:[TGInputTextTag class]]) { + [inputTextTags addObject:[[TGInputTextTagAndRange alloc] initWithTag:value range:range]]; + } + }]; + + if (inputTextTags != nil) { + /*if (mutableString == nil) { + mutableString = [[NSMutableAttributedString alloc] initWithAttributedString:string]; + }*/ + + static NSCharacterSet *alphanumericSet = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + alphanumericSet = [NSCharacterSet alphanumericCharacterSet]; + }); + + NSMutableSet *removeTags = [[NSMutableSet alloc] init]; + for (NSInteger i = 0; i < ((NSInteger)inputTextTags.count); i++) { + TGInputTextTagAndRange *tagAndRange = inputTextTags[i]; + if ([removeTags containsObject:@(tagAndRange.tag.uniqueId)]) { + [inputTextTags removeObjectAtIndex:i]; + //[mutableString removeAttribute:TGMentionUidAttributeName range:tagAndRange.range]; + [_internalTextView.textStorage removeAttribute:TGMentionUidAttributeName range:tagAndRange.range]; + + i--; + } else { + NSInteger j = tagAndRange.range.location; + while (j < (NSInteger)(tagAndRange.range.location + tagAndRange.range.length)) { + unichar c = [string.string characterAtIndex:j]; + if (c != ' ') { + break; + } + j++; + } + + if (j != (NSInteger)tagAndRange.range.location) { + NSRange updatedRange = NSMakeRange(j, tagAndRange.range.location + tagAndRange.range.length - j); + //[mutableString removeAttribute:TGMentionUidAttributeName range:tagAndRange.range]; + [_internalTextView.textStorage removeAttribute:TGMentionUidAttributeName range:tagAndRange.range]; + + //[mutableString addAttribute:TGMentionUidAttributeName value:tagAndRange.tag range:updatedRange]; + [_internalTextView.textStorage addAttribute:TGMentionUidAttributeName value:tagAndRange.tag range:updatedRange]; + + inputTextTags[i] = [[TGInputTextTagAndRange alloc] initWithTag:tagAndRange.tag range:updatedRange]; + + i--; + } else { + NSInteger j = tagAndRange.range.location; + while (j >= 0) { + unichar c = [string.string characterAtIndex:j]; + if (![alphanumericSet characterIsMember:c]) { + break; + } + j--; + } + j++; + + if (j < ((NSInteger)tagAndRange.range.location)) { + NSRange updatedRange = NSMakeRange(j, tagAndRange.range.location + tagAndRange.range.length - j); + //[mutableString removeAttribute:TGMentionUidAttributeName range:tagAndRange.range]; + [_internalTextView.textStorage removeAttribute:TGMentionUidAttributeName range:tagAndRange.range]; + + //[mutableString addAttribute:TGMentionUidAttributeName value:tagAndRange.tag range:updatedRange]; + [_internalTextView.textStorage addAttribute:TGMentionUidAttributeName value:tagAndRange.tag range:updatedRange]; + + inputTextTags[i] = [[TGInputTextTagAndRange alloc] initWithTag:tagAndRange.tag range:updatedRange]; + + i--; + } else { + TGInputTextTagAndRange *nextTagAndRange = nil; + if (i != ((NSInteger)inputTextTags.count) - 1) { + nextTagAndRange = inputTextTags[i + 1]; + } + + if (nextTagAndRange == nil || nextTagAndRange.tag.uniqueId != tagAndRange.tag.uniqueId) { + NSInteger candidateStart = tagAndRange.range.location + tagAndRange.range.length; + NSInteger candidateEnd = nextTagAndRange == nil ? string.length : nextTagAndRange.range.location; + NSInteger j = candidateStart; + while (j < candidateEnd) { + unichar c = [string.string characterAtIndex:j]; + static NSCharacterSet *alphanumericSet = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + alphanumericSet = [NSCharacterSet alphanumericCharacterSet]; + }); + if (![alphanumericSet characterIsMember:c]) { + break; + } + j++; + } + + if (j == candidateStart) { + [removeTags addObject:@(tagAndRange.tag.uniqueId)]; + //[mutableString addAttribute:NSForegroundColorAttributeName value:TGAccentColor() range:tagAndRange.range]; + [_internalTextView.textStorage addAttribute:NSForegroundColorAttributeName value:self.accentColor range:tagAndRange.range]; + } else { + //[mutableString removeAttribute:TGMentionUidAttributeName range:tagAndRange.range]; + [_internalTextView.textStorage removeAttribute:TGMentionUidAttributeName range:tagAndRange.range]; + + NSRange updatedRange = NSMakeRange(tagAndRange.range.location, j - tagAndRange.range.location); + //[mutableString addAttribute:TGMentionUidAttributeName value:tagAndRange.tag range:updatedRange]; + [_internalTextView.textStorage addAttribute:TGMentionUidAttributeName value:tagAndRange.tag range:updatedRange]; + inputTextTags[i] = [[TGInputTextTagAndRange alloc] initWithTag:tagAndRange.tag range:updatedRange]; + + i--; + } + } else { + NSInteger candidateStart = tagAndRange.range.location + tagAndRange.range.length; + NSInteger candidateEnd = nextTagAndRange.range.location; + NSInteger j = candidateStart; + while (j < candidateEnd) { + unichar c = [string.string characterAtIndex:j]; + if (![alphanumericSet characterIsMember:c] && c != ' ') { + break; + } + j++; + } + + if (j == candidateEnd) { + //[mutableString removeAttribute:TGMentionUidAttributeName range:tagAndRange.range]; + [_internalTextView.textStorage removeAttribute:TGMentionUidAttributeName range:tagAndRange.range]; + + //[mutableString removeAttribute:TGMentionUidAttributeName range:nextTagAndRange.range]; + [_internalTextView.textStorage removeAttribute:TGMentionUidAttributeName range:nextTagAndRange.range]; + + NSRange updatedRange = NSMakeRange(tagAndRange.range.location, nextTagAndRange.range.location + nextTagAndRange.range.length - tagAndRange.range.location); + + //[mutableString addAttribute:TGMentionUidAttributeName value:tagAndRange.tag range:updatedRange]; + [_internalTextView.textStorage addAttribute:TGMentionUidAttributeName value:tagAndRange.tag range:updatedRange]; + + inputTextTags[i] = [[TGInputTextTagAndRange alloc] initWithTag:tagAndRange.tag range:updatedRange]; + [inputTextTags removeObjectAtIndex:i + 1]; + + i--; + } else if (j != candidateStart) { + //[mutableString removeAttribute:TGMentionUidAttributeName range:tagAndRange.range]; + [_internalTextView.textStorage removeAttribute:TGMentionUidAttributeName range:tagAndRange.range]; + + NSRange updatedRange = NSMakeRange(tagAndRange.range.location, j - tagAndRange.range.location); + //[mutableString addAttribute:TGMentionUidAttributeName value:tagAndRange.tag range:updatedRange]; + [_internalTextView.textStorage addAttribute:TGMentionUidAttributeName value:tagAndRange.tag range:updatedRange]; + + inputTextTags[i] = [[TGInputTextTagAndRange alloc] initWithTag:tagAndRange.tag range:updatedRange]; + + i--; + } else { + [removeTags addObject:@(tagAndRange.tag.uniqueId)]; + //[mutableString addAttribute:NSForegroundColorAttributeName value:TGAccentColor() range:tagAndRange.range]; + [_internalTextView.textStorage addAttribute:NSForegroundColorAttributeName value:self.accentColor range:tagAndRange.range]; + } + } + } + } + } + } + } + + _internalTextView.freezeContentOffset = false; + [_internalTextView setContentOffset:contentOffset]; + _internalTextView.disableContentOffsetAnimation = false; + [_internalTextView setScrollEnabled:true]; + + /*if (mutableString != nil && ![mutableString isEqualToAttributedString:_internalTextView.attributedText])*/ { + /*[_internalTextView.textStorage removeAttribute:NSForegroundColorAttributeName range:NSMakeRange(0, string.length)]; + [_internalTextView.textStorage addAttribute:NSForegroundColorAttributeName value:TGAccentColor() range:NSMakeRange(string.length - 1, 1)]; + + return; + + UITextRange *previousRange = [_internalTextView selectedTextRange]; + UITextPosition *selStartPos = previousRange.start; + NSInteger previousIdx = [_internalTextView offsetFromPosition:_internalTextView.beginningOfDocument toPosition:selStartPos]; + + _internalTextView.attributedText = mutableString; + + UITextPosition *textPosition = [_internalTextView positionFromPosition:_internalTextView.beginningOfDocument offset:MIN(previousIdx, (NSInteger)mutableString.length)]; + [_internalTextView setSelectedTextRange:[_internalTextView textRangeFromPosition:textPosition toPosition:textPosition]];*/ + } +} + +- (NSString *)textWithEntities:(__autoreleasing NSArray ** _Nullable)entities { + NSAttributedString *string = self.attributedText; + + NSMutableArray *result = [[NSMutableArray alloc] init]; + [string enumerateAttribute:TGMentionUidAttributeName inRange:NSMakeRange(0, string.length) options:0 usingBlock:^(__unused id value, NSRange range, __unused BOOL *stop) { + if ([value isKindOfClass:[TGInputTextTag class]]) { + [result addObject:[[TGMessageEntityMentionName alloc] initWithRange:range userId:[((TGInputTextTag *)value).attachment intValue]]]; + } + }]; + + if (iosMajorVersion() >= 7) { + [string enumerateAttribute:NSFontAttributeName inRange:NSMakeRange(0, string.length) options:0 usingBlock:^(UIFont *font, NSRange range, __unused BOOL *stop) { + NSString *fontDescription = font.description; + if ([fontDescription rangeOfString:@"font-weight: bold"].location != NSNotFound) { + [result addObject:[[TGMessageEntityBold alloc] initWithRange:range]]; + } else if ([fontDescription rangeOfString:@"font-style: italic"].location != NSNotFound) { + [result addObject:[[TGMessageEntityItalic alloc] initWithRange:range]]; + } + }]; + } + + if (entities) { + *entities = result; + } + + return string.string; +} + ++ (void)replaceMention:(NSString *)mention inputField:(HPGrowingTextView *)inputField username:(bool)username userId:(int32_t)userId +{ + NSString *replacementText = [mention stringByAppendingString:@" "]; + + NSMutableAttributedString *text = inputField.internalTextView.attributedText == nil ? [[NSMutableAttributedString alloc] init] : [[NSMutableAttributedString alloc] initWithAttributedString:inputField.internalTextView.attributedText]; + + UITextRange *selRange = inputField.internalTextView.selectedTextRange; + UITextPosition *selStartPos = selRange.start; + NSInteger idx = [inputField.internalTextView offsetFromPosition:inputField.internalTextView.beginningOfDocument toPosition:selStartPos]; + idx--; + NSRange candidateMentionRange = NSMakeRange(NSNotFound, 0); + + if (idx >= 0 && idx < (int)text.length) + { + for (NSInteger i = idx; i >= 0; i--) + { + unichar c = [text.string characterAtIndex:i]; + if (c == '@') + { + if (i == idx) + candidateMentionRange = NSMakeRange(i + 1, 0); + else + candidateMentionRange = NSMakeRange(i + 1, idx - i); + break; + } + + if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_')) + break; + } + } + + if (candidateMentionRange.location != NSNotFound) + { + if (!username) { + candidateMentionRange.location -= 1; + candidateMentionRange.length += 1; + + [text replaceCharactersInRange:candidateMentionRange withString:replacementText]; + + static int64_t nextId = 0; + nextId++; + [text addAttributes:@{TGMentionUidAttributeName: [[TGInputTextTag alloc] initWithUniqueId:nextId left:true attachment:@(userId)]} range:NSMakeRange(candidateMentionRange.location, replacementText.length - 1)]; + } else { + [text replaceCharactersInRange:candidateMentionRange withString:replacementText]; + } + + [inputField setAttributedText:text]; + UITextPosition *textPosition = [inputField.internalTextView positionFromPosition:inputField.internalTextView.beginningOfDocument offset:candidateMentionRange.location + replacementText.length]; + [inputField.internalTextView setSelectedTextRange:[inputField.internalTextView textRangeFromPosition:textPosition toPosition:textPosition]]; + } +} + ++ (void)replaceHashtag:(NSString *)hashtag inputField:(HPGrowingTextView *)inputField +{ + if (inputField.attributedText == nil) { + return; + } + + static NSCharacterSet *characterSet = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^ + { + characterSet = [NSCharacterSet alphanumericCharacterSet]; + }); + + NSString *replacementText = [hashtag stringByAppendingString:@" "]; + + NSMutableAttributedString *text = [[NSMutableAttributedString alloc] initWithAttributedString:inputField.attributedText]; + + UITextRange *selRange = inputField.internalTextView.selectedTextRange; + UITextPosition *selStartPos = selRange.start; + NSInteger idx = [inputField.internalTextView offsetFromPosition:inputField.internalTextView.beginningOfDocument toPosition:selStartPos]; + idx--; + NSRange candidateHashtagRange = NSMakeRange(NSNotFound, 0); + NSString *string = text.string; + + if (idx >= 0 && idx < (int)text.length) + { + for (NSInteger i = idx; i >= 0; i--) + { + unichar c = [string characterAtIndex:i]; + if (c == '#') + { + if (i == idx) + candidateHashtagRange = NSMakeRange(i + 1, 0); + else + candidateHashtagRange = NSMakeRange(i + 1, idx - i); + break; + } + + if (c == ' ' || (![characterSet characterIsMember:c] && c != '_')) + break; + } + } + + if (candidateHashtagRange.location != NSNotFound) + { + [text replaceCharactersInRange:candidateHashtagRange withString:replacementText]; + [inputField setAttributedText:text]; + UITextPosition *textPosition = [inputField.internalTextView positionFromPosition:inputField.internalTextView.beginningOfDocument offset:candidateHashtagRange.location + replacementText.length]; + [inputField.internalTextView setSelectedTextRange:[inputField.internalTextView textRangeFromPosition:textPosition toPosition:textPosition]]; + } +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/HPTextViewInternal.h b/submodules/LegacyComponents/LegacyComponents/HPTextViewInternal.h new file mode 100644 index 0000000000..f837bd957e --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/HPTextViewInternal.h @@ -0,0 +1,61 @@ + + +// +// HPTextViewInternal.h +// +// Created by Hans Pinckaers on 29-06-10. +// +// MIT License +// +// Copyright (c) 2011 Hans Pinckaers +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#import + +#import + +@class TGKeyCommandController; + +@interface HPTextViewInternal : UITextView + +@property (nonatomic) bool isPasting; + +@property (nonatomic) bool freezeContentOffset; +@property (nonatomic) bool disableContentOffsetAnimation; + +@property (nonatomic, strong) TGWeakDelegate *responderStateDelegate; + +@property (nonatomic) bool enableFirstResponder; + +- (instancetype)initWithKeyCommandController:(TGKeyCommandController *)keyCommandController; + ++ (void)addTextViewMethods; + +- (void)textViewEnsureSelectionVisible; + +@end + +@protocol HPTextViewInternalDelegate + +@required + +- (void)hpTextViewChangedResponderState:(bool)firstResponder; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/HPTextViewInternal.m b/submodules/LegacyComponents/LegacyComponents/HPTextViewInternal.m new file mode 100644 index 0000000000..7515e8a770 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/HPTextViewInternal.m @@ -0,0 +1,318 @@ +#import "HPTextViewInternal.h" + +#import "LegacyComponentsInternal.h" +#import "TGHacks.h" +#import "FreedomUIKit.h" + +#import "HPGrowingTextView.h" + +#import +#import + +#import + +#import "TGKeyCommandController.h" + +@interface HPTextViewInternal () { + __weak TGKeyCommandController *_keyCommandController; +} + +@end + +@implementation HPTextViewInternal + +- (instancetype)initWithKeyCommandController:(TGKeyCommandController *)keyCommandController { + self = [super initWithFrame:CGRectZero]; + if (self != nil) { + _keyCommandController = keyCommandController; + } + return self; +} + ++ (void)addTextViewMethods +{ + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^ + { + InjectInstanceMethodFromAnotherClass([HPTextViewInternal class], [HPTextViewInternal class], @selector(textViewAdjustScrollRange:animated:), NSSelectorFromString(TGEncodeText(@"`tdspmmSbohfUpWjtjcmf;bojnbufe;", -1))); + }); +} + +- (void)setText:(NSString *)text +{ + BOOL originalValue = self.scrollEnabled; + //If one of GrowingTextView's superviews is a scrollView, and self.scrollEnabled == NO, + //setting the text programatically will cause UIKit to search upwards until it finds a scrollView with scrollEnabled==yes + //then scroll it erratically. Setting scrollEnabled temporarily to YES prevents this. + [self setScrollEnabled:YES]; + [super setText:text]; + [self setScrollEnabled:originalValue]; +} + +- (void)setAttributedText:(NSAttributedString *)attributedText { + BOOL originalValue = self.scrollEnabled; + //If one of GrowingTextView's superviews is a scrollView, and self.scrollEnabled == NO, + //setting the text programatically will cause UIKit to search upwards until it finds a scrollView with scrollEnabled==yes + //then scroll it erratically. Setting scrollEnabled temporarily to YES prevents this. + [self setScrollEnabled:YES]; + [super setAttributedText:attributedText]; + [self setScrollEnabled:originalValue]; +} + +- (void)setScrollable:(BOOL)isScrollable +{ + [super setScrollEnabled:isScrollable]; +} + +- (void)textViewAdjustScrollRange:(NSRange)range animated:(BOOL)animated +{ + static SEL selector = NULL; + static void (*impl)(id, SEL, NSRange, BOOL) = NULL; + + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^ + { + Method method = class_getInstanceMethod([UITextView class], selector); + if (method != NULL) + impl = (void (*)(id, SEL, NSRange, BOOL))method_getImplementation(method); + }); + + animated = false; + + if (impl != NULL) + impl(self, selector, range, animated); +} + +- (void)scrollRectToVisible:(CGRect)__unused rect animated:(BOOL)__unused animated +{ + +} + +- (void)setContentOffset:(CGPoint)contentOffset animated:(BOOL)animated +{ + if (_freezeContentOffset) + return; + + [super setContentOffset:contentOffset animated:_disableContentOffsetAnimation ? false : animated]; +} + +- (void)setFrame:(CGRect)frame +{ + [super setFrame:frame]; +} + +-(void)setContentOffset:(CGPoint)s +{ + if (_freezeContentOffset) + return; + + [super setContentOffset:s]; +} + +- (void)textViewEnsureSelectionVisible +{ + if (iosMajorVersion() >= 9) { + dispatch_async(dispatch_get_main_queue(), ^{ + CGRect caretFrame = [self caretRectForPosition:self.selectedTextRange.end]; + if (caretFrame.origin.x < CGFLOAT_MAX && caretFrame.origin.y < CGFLOAT_MAX && !CGRectIsInfinite(caretFrame)) + { + UIEdgeInsets implicitInset = UIEdgeInsetsMake(8, 0, 8, 0); + + caretFrame.origin.y -= implicitInset.top; + caretFrame.size.height += implicitInset.top + implicitInset.bottom; + caretFrame.origin.y = CGFloor(caretFrame.origin.y * 2.0f) / 2.0f; + caretFrame.size.height = CGFloor(caretFrame.size.height * 2.0f) / 2.0f; + + CGFloat frameHeight = self.frame.size.height; + CGPoint contentOffset = self.contentOffset; + + if (caretFrame.origin.y < contentOffset.y) + contentOffset.y = caretFrame.origin.y; + if (caretFrame.origin.y + caretFrame.size.height > contentOffset.y + frameHeight) + contentOffset.y = caretFrame.origin.y + caretFrame.size.height - frameHeight; + contentOffset.y = MAX(0, contentOffset.y); + + if (!CGPointEqualToPoint(contentOffset, self.contentOffset)) + self.contentOffset = contentOffset; + } + }); + } else { + CGRect caretFrame = [self caretRectForPosition:self.selectedTextRange.end]; + if (caretFrame.origin.x < CGFLOAT_MAX && caretFrame.origin.y < CGFLOAT_MAX && !CGRectIsInfinite(caretFrame)) + { + UIEdgeInsets implicitInset = UIEdgeInsetsMake(8, 0, 8, 0); + + caretFrame.origin.y -= implicitInset.top; + caretFrame.size.height += implicitInset.top + implicitInset.bottom; + caretFrame.origin.y = CGFloor(caretFrame.origin.y * 2.0f) / 2.0f; + caretFrame.size.height = CGFloor(caretFrame.size.height * 2.0f) / 2.0f; + + CGFloat frameHeight = self.frame.size.height; + CGPoint contentOffset = self.contentOffset; + + if (caretFrame.origin.y < contentOffset.y) + contentOffset.y = caretFrame.origin.y; + if (caretFrame.origin.y + caretFrame.size.height > contentOffset.y + frameHeight) + contentOffset.y = caretFrame.origin.y + caretFrame.size.height - frameHeight; + contentOffset.y = MAX(0, contentOffset.y); + + if (!CGPointEqualToPoint(contentOffset, self.contentOffset)) + self.contentOffset = contentOffset; + } + } +} + +- (void)setContentSize:(CGSize)contentSize +{ + [super setContentSize:contentSize]; + + [self textViewEnsureSelectionVisible]; +} + +- (BOOL)canBecomeFirstResponder +{ + if (!_enableFirstResponder) + return false; + return true; +} + +- (BOOL)becomeFirstResponder +{ + if (!_enableFirstResponder) + return false; + + __block BOOL result = false; + freedomUIKitTest4(^ + { + if ([self.delegate respondsToSelector:@selector(textViewShouldBeginEditing:)] && ![self.delegate textViewShouldBeginEditing:self]) + result = false; + else + result = [super becomeFirstResponder]; + }); + + if (result) + { + id delegate = _responderStateDelegate.object; + if (delegate != nil && [delegate conformsToProtocol:@protocol(HPTextViewInternalDelegate)]) + { + [(id)delegate hpTextViewChangedResponderState:true]; + } + } + return result; +} + +- (BOOL)resignFirstResponder +{ + __block BOOL result = false; + freedomUIKitTest4(^ + { + result = [super resignFirstResponder]; + }); + + if (result) + { + id delegate = _responderStateDelegate.object; + if (delegate != nil && [delegate conformsToProtocol:@protocol(HPTextViewInternalDelegate)]) + { + [(id)delegate hpTextViewChangedResponderState:false]; + } + } + return result; +} + +- (BOOL)canPerformAction:(SEL)action withSender:(id)sender +{ + if (action == @selector(paste:)) + return true; + + if (action == @selector(toggleUnderline:)) { + return false; + } + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wundeclared-selector" + static SEL promptForReplaceSelector; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + promptForReplaceSelector = NSSelectorFromString(@"_promptForReplace:"); + }); + if (action == promptForReplaceSelector) { + return false; + } +#pragma clang diagnostic pop + + return [super canPerformAction:action withSender:sender]; +} + +- (id)targetForAction:(SEL)action withSender:(id)__unused sender +{ + if (action == @selector(processKeyCommand:)) { + TGKeyCommandController *keyCommandController = _keyCommandController; + return [keyCommandController targetForAction:action withSender:sender]; + } + + return [super targetForAction:action withSender:sender]; +} + +- (void)paste:(id)sender +{ + UIPasteboard *pasteBoard = [UIPasteboard generalPasteboard]; + + NSData *gifData = [pasteBoard dataForPasteboardType:@"com.compuserve.gif"]; + if (gifData != nil) + { + id delegate = self.delegate; + if ([delegate isKindOfClass:[HPGrowingTextView class]]) + { + HPGrowingTextView *textView = delegate; + NSObject *textViewDelegate = (NSObject *)textView.delegate; + if ([textViewDelegate respondsToSelector:@selector(growingTextView:didPasteData:)]) + [textViewDelegate growingTextView:textView didPasteData:gifData]; + } + } + else + { + NSMutableArray *images = [NSMutableArray arrayWithCapacity:1]; + NSString *text = nil; + + for (NSDictionary *item in pasteBoard.items) { + if (item[(__bridge NSString *)kUTTypeJPEG] != nil) { + [images addObject:item[(__bridge NSString *)kUTTypeJPEG]]; + } else if (item[(__bridge NSString *)kUTTypePNG] != nil) { + [images addObject:item[(__bridge NSString *)kUTTypePNG]]; + } else if (item[(__bridge NSString *)kUTTypeGIF] != nil) { + [images addObject:item[(__bridge NSString *)kUTTypeGIF]]; + } else if (item[(__bridge NSString *)kUTTypeURL] != nil) { + id url = item[(__bridge NSString *)kUTTypeURL]; + if ([url respondsToSelector:@selector(characterAtIndex:)]) { + text = url; + } else if ([url isKindOfClass:[NSURL class]]) { + text = ((NSURL *)url).absoluteString; + } + } + } + + if (images.count != 0) + { + id delegate = self.delegate; + if ([delegate isKindOfClass:[HPGrowingTextView class]]) + { + HPGrowingTextView *textView = delegate; + NSObject *textViewDelegate = (NSObject *)textView.delegate; + if ([textViewDelegate respondsToSelector:@selector(growingTextView:didPasteImages:andText:)]) + [textViewDelegate growingTextView:textView didPasteImages:images andText:text]; + } + } + else + { + _isPasting = true; + bool previousAllowsEditingTextAttributes = self.allowsEditingTextAttributes; + self.allowsEditingTextAttributes = false; + [super paste:sender]; + self.allowsEditingTextAttributes = previousAllowsEditingTextAttributes; + _isPasting = false; + } + } +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/Info.plist b/submodules/LegacyComponents/LegacyComponents/Info.plist new file mode 100644 index 0000000000..fbe1e6b314 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSPrincipalClass + + + diff --git a/submodules/LegacyComponents/LegacyComponents/JNWSpringAnimation.h b/submodules/LegacyComponents/LegacyComponents/JNWSpringAnimation.h new file mode 100644 index 0000000000..9a7a0088f5 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/JNWSpringAnimation.h @@ -0,0 +1,85 @@ +/* + Copyright (c) 2013, Jonathan Willing. All rights reserved. + Licensed under the MIT license + + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + documentation files (the "Software"), to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and + to permit persons to whom the Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED + TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + IN THE SOFTWARE. + */ + +#import + +// Adds the ability to add spring to your animations. +// +// Duration cannot be set directly. Instead, duration is +// a side effect of changing the various properties below. +// +// Although JNWSpringAnimation is a subclass of CAKeyframeAnimation, +// it should be treated as if it were a subclass of CABasicAnimation. +@interface JNWSpringAnimation : CAKeyframeAnimation + +// The dedicated initializer for the animation. +// +// Not all layer properties can be animated. The following are compatible: +// - position +// - position.{x, y} +// - cornerRadius +// - shadowRadius +// - bounds +// - bounds.size +// - transform.translation.{x, y, z} +// - transform.rotation.{x, y, z} +// - transform.scale.{x, y, z} +// - transform.translation +// - transform (** experimental, only performs linear interpolation on all components **) ++ (instancetype)animationWithKeyPath:(NSString *)path; + +// A damped spring can be modeled with the following equation: +// F = - kx - bv +// where k is the spring constant, x is the distance from equilibrium, +// and b is the coefficient of damping. +// +// Under the hood, a damped harmonic oscillation equation is used to +// provide the same results as the data obtained from Hooke's law. + +// The spring constant. +// +// Defaults to 300. +@property (nonatomic, assign) CGFloat stiffness; +@property (nonatomic, assign) CGFloat durationFactor; + +// The coefficient of damping. +// +// Defaults to 30. +@property (nonatomic, assign) CGFloat damping; + +// The mass of the object. +// +// Defaults to 5. +@property (nonatomic, assign) CGFloat mass; + +// Equivalent to CABasicAnimation's counterparts. +// +// Both must be non-nil. +@property (nonatomic, strong) id fromValue; +@property (nonatomic, strong) id toValue; +@property (nonatomic, strong) id initialVelocity; + +// The duration, which is derived from the stiffness, damping, mass, and values. +// +// Note that this property will only return a non-zero value if both the `fromValue` +// and the `toValue` properties have both been set. +// +// Defaults to 0 if no from or to values have been set. +@property (nonatomic, assign, readonly) CFTimeInterval duration; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/JNWSpringAnimation.m b/submodules/LegacyComponents/LegacyComponents/JNWSpringAnimation.m new file mode 100644 index 0000000000..0ec19caaca --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/JNWSpringAnimation.m @@ -0,0 +1,295 @@ +#import "JNWSpringAnimation.h" +#import "NSValue+JNWAdditions.h" + +#import "TGHacks.h" + +static const CGFloat JNWSpringAnimationDefaultMass = 5.f; +static const CGFloat JNWSpringAnimationDefaultDamping = 30.f; +static const CGFloat JNWSpringAnimationDefaultStiffness = 300.f; +static const CGFloat JNWSpringAnimationKeyframeStep = 0.001f; +static const CGFloat JNWSpringAnimationMinimumThreshold = 0.0001f; + +@interface JNWSpringAnimation() +@property (nonatomic, copy) NSArray *interpolatedValues; +@property (nonatomic, assign) BOOL needsRecalculation; +@end + +@implementation JNWSpringAnimation + +#pragma mark Initialization + ++ (instancetype)animationWithKeyPath:(NSString *)path { + return [super animationWithKeyPath:path]; +} + +- (id)init { + self = [super init]; + _mass = JNWSpringAnimationDefaultMass; + _damping = JNWSpringAnimationDefaultDamping; + _stiffness = JNWSpringAnimationDefaultStiffness; + _durationFactor = 1.0f; + _needsRecalculation = NO; + return self; +} + +// Since animations are copied before they are added onto the layer, we +// take this opportunity to override the copy method and actually +// calculate the key frames, and move those over to the new animation. +- (id)copyWithZone:(NSZone *)zone { + JNWSpringAnimation *copy = [super copyWithZone:zone]; + + copy.interpolatedValues = self.interpolatedValues; + copy.duration = self.interpolatedValues.count * JNWSpringAnimationKeyframeStep * TGAnimationSpeedFactor() * _durationFactor; + copy.fromValue = self.fromValue; + copy.stiffness = self.stiffness; + copy.toValue = self.toValue; + copy.damping = self.damping; + copy.mass = self.mass; + + copy.needsRecalculation = NO; + + return copy; +} + +#pragma mark API + +- (void)setToValue:(id)toValue { + _toValue = toValue; + self.needsRecalculation = YES; +} + +- (void)setFromValue:(id)fromValue { + _fromValue = fromValue; + self.needsRecalculation = YES; +} + +- (void)setStiffness:(CGFloat)stiffness { + _stiffness = stiffness; + self.needsRecalculation = YES; +} + +- (void)setMass:(CGFloat)mass { + _mass = mass; + self.needsRecalculation = YES; +} + +- (NSArray *)values { + return self.interpolatedValues; +} + +- (void)setDamping:(CGFloat)damping { + if (damping <= 0) { + NSLog(@"[%@] LOGIC ERROR. `damping` should be > 0.0f to avoid an infinite spring calculation", NSStringFromClass([self class])); + damping = 1.0f; + } + _damping = damping; + self.needsRecalculation = YES; +} + +- (CFTimeInterval)duration { + if (self.fromValue != nil && self.toValue != nil) { + return self.interpolatedValues.count * JNWSpringAnimationKeyframeStep * TGAnimationSpeedFactor() * _durationFactor; + } + + return 0.f; +} + +- (NSArray *)interpolatedValues { + if (self.needsRecalculation || _interpolatedValues == nil) { + [self calculateInterpolatedValues]; + } + + return _interpolatedValues; +} + +#pragma mark Interpolation + +- (void)calculateInterpolatedValues { + NSAssert(self.fromValue != nil && self.toValue != nil, @"fromValue and or toValue must not be nil."); + + JNWValueType fromType = [self.fromValue jnw_type]; + JNWValueType toType = [self.toValue jnw_type]; + NSAssert(fromType == toType, @"fromValue and toValue must be of the same type."); + NSAssert(fromType != JNWValueTypeUnknown, @"Type of value could not be determined. Please ensure the value types are supported."); + + NSArray *values = nil; + + if (fromType == JNWValueTypeNumber) { + values = [self valuesFromNumbers:@[self.fromValue] toNumbers:@[self.toValue] map:^id(CGFloat *values, NSUInteger __unused count) { + return @(values[0]); + }]; + } else if (fromType == JNWValueTypePoint) { + CGPoint f = [self.fromValue jnw_pointValue]; + CGPoint t = [self.toValue jnw_pointValue]; + values = [self valuesFromNumbers:@[@(f.x), @(f.y)] + toNumbers:@[@(t.x), @(t.y)] + map:^id(CGFloat *values, __unused NSUInteger count) { + return [NSValue jnw_valueWithPoint:CGPointMake(values[0], values[1])]; + }]; + } else if (fromType == JNWValueTypeSize) { + CGSize f = [self.fromValue jnw_sizeValue]; + CGSize t = [self.toValue jnw_sizeValue]; + values = [self valuesFromNumbers:@[@(f.width), @(f.height)] + toNumbers:@[@(t.width), @(t.height)] + map:^id(CGFloat *values, __unused NSUInteger count) { + return [NSValue jnw_valueWithSize:CGSizeMake(values[0], values[1])]; + }]; + } else if (fromType == JNWValueTypeRect) { // note that CA will not animate the `frame` property + CGRect f = [self.fromValue jnw_rectValue]; + CGRect t = [self.toValue jnw_rectValue]; + values = [self valuesFromNumbers:@[@(f.origin.x), @(f.origin.y), @(f.size.width), @(f.size.height)] + toNumbers:@[@(t.origin.x), @(t.origin.y), @(t.size.width), @(t.size.height)] + map:^id(CGFloat *values, __unused NSUInteger count) { + return [NSValue jnw_valueWithRect:CGRectMake(values[0], values[1], values[2], values[3])]; + }]; + } else if (fromType == JNWValueTypeAffineTransform) { + CGAffineTransform f = [self.fromValue jnw_affineTransformValue]; + CGAffineTransform t = [self.toValue jnw_affineTransformValue]; + + values = [self valuesFromNumbers:@[@(f.a), @(f.b), @(f.c), @(f.d), @(f.tx), @(f.ty)] + toNumbers:@[@(t.a), @(t.b), @(t.c), @(t.d), @(t.tx), @(t.ty)] + map:^id(CGFloat *values, __unused NSUInteger count) { + CGAffineTransform transform; + transform.a = values[0]; + transform.b = values[1]; + transform.c = values[2]; + transform.d = values[3]; + transform.tx = values[4]; + transform.ty = values[5]; + + return [NSValue jnw_valueWithAffineTransform:transform]; + }]; + } else if (fromType == JNWValueTypeTransform3D) { + CATransform3D f = [self.fromValue CATransform3DValue]; + CATransform3D t = [self.toValue CATransform3DValue]; + + values = [self valuesFromNumbers:@[@(f.m11), @(f.m12), @(f.m13), @(f.m14), @(f.m21), @(f.m22), @(f.m23), @(f.m24), @(f.m31), @(f.m32), @(f.m33), @(f.m34), @(f.m41), @(f.m42), @(f.m43), @(f.m44) ] + toNumbers:@[@(t.m11), @(t.m12), @(t.m13), @(t.m14), @(t.m21), @(t.m22), @(t.m23), @(t.m24), @(t.m31), @(t.m32), @(t.m33), @(t.m34), @(t.m41), @(t.m42), @(t.m43), @(t.m44) ] + map:^id(CGFloat *values, __unused NSUInteger count) { + CATransform3D transform = CATransform3DIdentity; + transform.m11 = values[0]; + transform.m12 = values[1]; + transform.m13 = values[2]; + transform.m14 = values[3]; + transform.m21 = values[4]; + transform.m22 = values[5]; + transform.m23 = values[6]; + transform.m24 = values[7]; + transform.m31 = values[8]; + transform.m32 = values[9]; + transform.m33 = values[10]; + transform.m34 = values[11]; + transform.m41 = values[12]; + transform.m42 = values[13]; + transform.m43 = values[14]; + transform.m44 = values[15]; + + return [NSValue valueWithCATransform3D:transform]; + }]; + } + + self.interpolatedValues = values; + self.needsRecalculation = NO; +} + +- (NSArray *)valuesFromNumbers:(NSArray *)fromNumbers toNumbers:(NSArray *)toNumbers map:(id (^)(CGFloat *values, NSUInteger count))map { + NSAssert(fromNumbers.count == toNumbers.count, @"count of from and to numbers must be equal"); + + NSUInteger count = fromNumbers.count; + + // This will never happen, but this is peformed in order to shush the analyzer. + if (count < 1) { + return [NSArray array]; + } + + CGFloat *distances = calloc(count, sizeof(CGFloat)); + CGFloat *thresholds = calloc(count, sizeof(CGFloat)); + for (NSUInteger i = 0; i < count; i++) { + distances[i] = [toNumbers[i] floatValue] - [fromNumbers[i] floatValue]; + thresholds[i] = JNWSpringAnimationThreshold(ABS(distances[i])); + } + + CFTimeInterval step = JNWSpringAnimationKeyframeStep; + CFTimeInterval elapsed = 0; + + CGFloat *stepValues = calloc(count, sizeof(CGFloat)); + CGFloat *stepProposedValues = calloc(count, sizeof(CGFloat)); + + NSMutableArray *valuesMapped = [NSMutableArray array]; + while (YES) { + BOOL thresholdReached = YES; + + for (NSUInteger i = 0; i < count; i++) { + stepProposedValues[i] = JNWAbsolutePosition(distances[i], (CGFloat)elapsed, 0, self.damping, self.mass, self.stiffness, [fromNumbers[i] floatValue]); + + if (thresholdReached) + thresholdReached = JNWThresholdReached(stepValues[i], stepProposedValues[i], [toNumbers[i] floatValue], thresholds[i]); + } + + if (thresholdReached) + break; + + for (NSUInteger i = 0; i < count; i++) { + stepValues[i] = stepProposedValues[i]; + } + + [valuesMapped addObject:map(stepValues, count)]; + elapsed += step; + } + + free(distances); + free(thresholds); + free(stepValues); + free(stepProposedValues); + + return valuesMapped; +} + +BOOL JNWThresholdReached(CGFloat previousValue, CGFloat proposedValue, CGFloat finalValue, CGFloat threshold) { + CGFloat previousDifference = ABS(proposedValue - previousValue); + CGFloat finalDifference = ABS(previousValue - finalValue); + if (previousDifference <= threshold && finalDifference <= threshold) { + return YES; + } + return NO; +} + +BOOL JNWCalculationsAreComplete(CGFloat value1, CGFloat proposedValue1, CGFloat to1, CGFloat value2, CGFloat proposedValue2, CGFloat to2, CGFloat value3, CGFloat proposedValue3, CGFloat to3) { + return ((fabs(proposedValue1 - value1) < JNWSpringAnimationKeyframeStep) && (fabs(value1 - to1) < JNWSpringAnimationKeyframeStep) + && (fabs(proposedValue2 - value2) < JNWSpringAnimationKeyframeStep) && (fabs(value2 - to2) < JNWSpringAnimationKeyframeStep) + && (fabs(proposedValue3 - value3) < JNWSpringAnimationKeyframeStep) && (fabs(value3 - to3) < JNWSpringAnimationKeyframeStep)); +} + +#pragma mark Damped Harmonic Oscillation + + +CGFloat JNWAngularFrequency(CGFloat k, CGFloat m, CGFloat b) { + CGFloat w0 = (CGFloat)sqrt(k / m); + CGFloat frequency = (CGFloat)sqrt(pow(w0, 2) - (pow(b, 2) / (4*pow(m, 2)))); + if (isnan(frequency)) frequency = 0; + return frequency; +} + +CGFloat JNWRelativePosition(CGFloat A, CGFloat t, CGFloat phi, CGFloat b, CGFloat m, CGFloat k) { + if (A == 0) return A; + CGFloat ex = (-b / (2 * m) * t); + CGFloat freq = JNWAngularFrequency(k, m, b); + return (CGFloat)(A * exp(ex) * cos(freq * t + phi)); +} + +CGFloat JNWAbsolutePosition(CGFloat A, CGFloat t, CGFloat phi, CGFloat b, CGFloat m, CGFloat k, CGFloat from) { + return from + A - JNWRelativePosition(A, t, phi, b, m, k); +} + +// This feels a bit hacky. I'm sure there's a better way to accomplish this. +CGFloat JNWSpringAnimationThreshold(CGFloat magnitude) { + return JNWSpringAnimationMinimumThreshold * magnitude; +} + +#pragma mark Description + +- (NSString *)description { + return [NSString stringWithFormat:@"<%@: %p> mass: %f, damping: %f, stiffness: %f, keyPath: %@, toValue: %@, fromValue %@", self.class, self, self.mass, self.damping, self.stiffness, self.keyPath, self.toValue, self.fromValue]; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/LegacyComponents.h b/submodules/LegacyComponents/LegacyComponents/LegacyComponents.h new file mode 100644 index 0000000000..0ed018398f --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/LegacyComponents.h @@ -0,0 +1,341 @@ +#import + +//! Project version number for LegacyComponents. +FOUNDATION_EXPORT double LegacyComponentsVersionNumber; + +//! Project version string for LegacyComponents. +FOUNDATION_EXPORT const unsigned char LegacyComponentsVersionString[]; + + +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import + +#import +#import +#import +#import +#import +#import +#import +#import +#import + +#import +#import +#import +#import +#import + +#import +#import +#import +#import +#import +#import +#import +#import + +#import +#import +#import +#import +#import + +#import +#import +#import +#import +#import +#import + +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import + +#import +#import +#import +#import +#import +#import +#import +#import + +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import + +#import +#import + +#import +#import +#import +#import +#import +#import + +#import +#import +#import + +#import + +#import +#import +#import + +#import +#import +#import +#import + +#import +#import +#import +#import +#import +#import +#import +#import + +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import + +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import + +#import +#import +#import +#import +#import + +#import +#import +#import +#import +#import +#import +#import + +#import +#import + +#import +#import +#import +#import +#import + +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import + +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import + +#import +#import + +#import +#import +#import +#import +#import +#import +#import + +#import + +#import +#import +#import +#import + +#import +#import +#import +#import + +#import + +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import diff --git a/submodules/LegacyComponents/LegacyComponents/LegacyComponentsAccessChecker.h b/submodules/LegacyComponents/LegacyComponents/LegacyComponentsAccessChecker.h new file mode 100644 index 0000000000..68cebadbfa --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/LegacyComponentsAccessChecker.h @@ -0,0 +1,39 @@ +#import + +typedef enum { + TGPhotoAccessIntentRead, + TGPhotoAccessIntentSave, + TGPhotoAccessIntentCustomWallpaper +} TGPhotoAccessIntent; + +typedef enum { + TGMicrophoneAccessIntentVoice, + TGMicrophoneAccessIntentVideo, + TGMicrophoneAccessIntentCall, + TGMicrophoneAccessIntentVideoMessage +} TGMicrophoneAccessIntent; + +typedef enum { + TGCameraAccessIntentDefault, + TGCameraAccessIntentVideoMessage +} TGCameraAccessIntent; + +typedef enum { + TGLocationAccessIntentSend, + TGLocationAccessIntentTracking, + TGLocationAccessIntentLiveLocation +} TGLocationAccessIntent; + +@protocol LegacyComponentsAccessChecker + +- (bool)checkAddressBookAuthorizationStatusWithAlertDismissComlpetion:(void (^)(void))alertDismissCompletion; + +- (bool)checkPhotoAuthorizationStatusForIntent:(TGPhotoAccessIntent)intent alertDismissCompletion:(void (^)(void))alertDismissCompletion; + +- (bool)checkMicrophoneAuthorizationStatusForIntent:(TGMicrophoneAccessIntent)intent alertDismissCompletion:(void (^)(void))alertDismissCompletion; + +- (bool)checkCameraAuthorizationStatusForIntent:(TGCameraAccessIntent)intent alertDismissCompletion:(void (^)(void))alertDismissCompletion; + +- (bool)checkLocationAuthorizationStatusForIntent:(TGLocationAccessIntent)intent alertDismissComlpetion:(void (^)(void))alertDismissCompletion; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/LegacyComponentsContext.h b/submodules/LegacyComponents/LegacyComponents/LegacyComponentsContext.h new file mode 100644 index 0000000000..c3bcf6a7e9 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/LegacyComponentsContext.h @@ -0,0 +1,80 @@ +#import +#import + +@class TGKeyCommandController; +@class SSignal; +@class TGOverlayControllerWindow; + +typedef enum { + LegacyComponentsActionSheetActionTypeGeneric, + LegacyComponentsActionSheetActionTypeDestructive, + LegacyComponentsActionSheetActionTypeCancel +} LegacyComponentsActionSheetActionType; + +@interface LegacyComponentsActionSheetAction : NSObject + +@property (nonatomic, strong, readonly) NSString *title; +@property (nonatomic, strong, readonly) NSString *action; +@property (nonatomic, readonly) LegacyComponentsActionSheetActionType type; + +- (instancetype)initWithTitle:(NSString *)title action:(NSString *)action; +- (instancetype)initWithTitle:(NSString *)title action:(NSString *)action type:(LegacyComponentsActionSheetActionType)type; + +@end + + +@protocol LegacyComponentsContext; + +@protocol LegacyComponentsOverlayWindowManager + +- (id)context; +- (void)bindController:(UIViewController *)controller; +- (bool)managesWindow; +- (void)setHidden:(bool)hidden window:(UIWindow *)window; + +@end + +@protocol LegacyComponentsContext + +- (UIEdgeInsets)safeAreaInset; +- (CGRect)fullscreenBounds; +- (TGKeyCommandController *)keyCommandController; +- (CGRect)statusBarFrame; +- (bool)isStatusBarHidden; +- (void)setStatusBarHidden:(BOOL)hidden withAnimation:(UIStatusBarAnimation)animation; +- (void)forceSetStatusBarHidden:(BOOL)hidden withAnimation:(UIStatusBarAnimation)animation; +- (UIStatusBarStyle)statusBarStyle; +- (void)setStatusBarStyle:(UIStatusBarStyle)statusBarStyle animated:(BOOL)animated; +- (void)forceStatusBarAppearanceUpdate; +- (bool)prefersLightStatusBar; + +@required + +- (CGFloat)applicationStatusBarAlpha; +- (void)setApplicationStatusBarAlpha:(CGFloat)alpha; + +- (void)animateApplicationStatusBarAppearance:(int)statusBarAnimation delay:(NSTimeInterval)delay duration:(NSTimeInterval)duration completion:(void (^)())completion; +- (void)animateApplicationStatusBarAppearance:(int)statusBarAnimation duration:(NSTimeInterval)duration completion:(void (^)())completion; + +- (void)animateApplicationStatusBarStyleTransitionWithDuration:(NSTimeInterval)duration; + +- (bool)rootCallStatusBarHidden; + +- (bool)currentlyInSplitView; + +- (UIUserInterfaceSizeClass)currentSizeClass; +- (UIUserInterfaceSizeClass)currentHorizontalSizeClass; +- (UIUserInterfaceSizeClass)currentVerticalSizeClass; +- (SSignal *)sizeClassSignal; + +- (bool)canOpenURL:(NSURL *)url; +- (void)openURL:(NSURL *)url; + +- (NSDictionary *)serverMediaDataForAssetUrl:(NSString *)url; + +- (void)presentActionSheet:(NSArray *)actions view:(UIView *)view completion:(void (^)(LegacyComponentsActionSheetAction *))completion; +- (void)presentActionSheet:(NSArray *)actions view:(UIView *)view sourceRect:(CGRect (^)(void))sourceRect completion:(void (^)(LegacyComponentsActionSheetAction *))completion ; + +- (id)makeOverlayWindowManager; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/LegacyComponentsContext.m b/submodules/LegacyComponents/LegacyComponents/LegacyComponentsContext.m new file mode 100644 index 0000000000..4305eeb76d --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/LegacyComponentsContext.m @@ -0,0 +1,19 @@ +#import "LegacyComponentsContext.h" + +@implementation LegacyComponentsActionSheetAction + +- (instancetype)initWithTitle:(NSString *)title action:(NSString *)action { + return [self initWithTitle:title action:action type:LegacyComponentsActionSheetActionTypeGeneric]; +} + +- (instancetype)initWithTitle:(NSString *)title action:(NSString *)action type:(LegacyComponentsActionSheetActionType)type { + self = [super init]; + if (self != nil) { + _title = title; + _action = action; + _type = type; + } + return self; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/LegacyComponentsGlobals.h b/submodules/LegacyComponents/LegacyComponents/LegacyComponentsGlobals.h new file mode 100644 index 0000000000..6ebb6de398 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/LegacyComponentsGlobals.h @@ -0,0 +1,112 @@ +#import +#import + +#import +#import + +@class SSignal; +@class SThreadPool; +@protocol SDisposable; +@class TGLocalization; +@class UIViewController; +@class TGWallpaperInfo; +@class TGMemoryImageCache; +@class TGImageMediaAttachment; +@class TGNavigationBarPallete; +@class TGMenuSheetPallete; +@class TGMediaAssetsPallete; +@class TGImageBorderPallete; +@class TGCheckButtonPallete; + +typedef enum { + TGAudioSessionTypePlayVoice, + TGAudioSessionTypePlayMusic, + TGAudioSessionTypePlayVideo, + TGAudioSessionTypePlayEmbedVideo, + TGAudioSessionTypePlayAndRecord, + TGAudioSessionTypePlayAndRecordHeadphones, + TGAudioSessionTypeCall +} TGAudioSessionType; + +@protocol LegacyComponentsGlobalsProvider + +- (TGLocalization *)effectiveLocalization; +- (void)log:(NSString *)string; +- (NSArray *)applicationWindows; +- (UIWindow *)applicationStatusBarWindow; +- (UIWindow *)applicationKeyboardWindow; +- (UIApplication *)applicationInstance; + +- (UIInterfaceOrientation)applicationStatusBarOrientation; +- (CGRect)statusBarFrame; +- (bool)isStatusBarHidden; +- (void)setStatusBarHidden:(BOOL)hidden withAnimation:(UIStatusBarAnimation)animation; +- (UIStatusBarStyle)statusBarStyle; +- (void)setStatusBarStyle:(UIStatusBarStyle)statusBarStyle animated:(BOOL)animated; +- (void)forceStatusBarAppearanceUpdate; + +- (bool)canOpenURL:(NSURL *)url; +- (void)openURL:(NSURL *)url; +- (void)openURLNative:(NSURL *)url; + +- (void)disableUserInteractionFor:(NSTimeInterval)timeInterval; +- (void)setIdleTimerDisabled:(bool)value; + +- (void)pauseMusicPlayback; + +- (NSString *)dataStoragePath; +- (NSString *)dataCachePath; + +- (id)accessChecker; + +- (SSignal *)stickerPacksSignal; +- (SSignal *)maskStickerPacksSignal; +- (SSignal *)recentStickerMasksSignal; + +- (id)requestAudioSession:(TGAudioSessionType)type interrupted:(void (^)())interrupted; + +- (TGWallpaperInfo *)currentWallpaperInfo; +- (UIImage *)currentWallpaperImage; + +- (SThreadPool *)sharedMediaImageProcessingThreadPool; +- (TGMemoryImageCache *)sharedMediaMemoryImageCache; +- (SSignal *)squarePhotoThumbnail:(TGImageMediaAttachment *)imageAttachment ofSize:(CGSize)size threadPool:(SThreadPool *)threadPool memoryCache:(TGMemoryImageCache *)memoryCache pixelProcessingBlock:(void (^)(void *, int, int, int))pixelProcessingBlock downloadLargeImage:(bool)downloadLargeImage placeholder:(SSignal *)placeholder; + +- (NSString *)localDocumentDirectoryForLocalDocumentId:(int64_t)localDocumentId version:(int32_t)version; +- (NSString *)localDocumentDirectoryForDocumentId:(int64_t)documentId version:(int32_t)version; + +- (SSignal *)jsonForHttpLocation:(NSString *)httpLocation; +- (SSignal *)dataForHttpLocation:(NSString *)httpLocation; + +- (NSOperation *)makeHTTPRequestOperationWithRequest:(NSURLRequest *)request; + +- (void)pausePictureInPicturePlayback; +- (void)resumePictureInPicturePlayback; +- (void)maybeReleaseVolumeOverlay; + +- (TGNavigationBarPallete *)navigationBarPallete; +- (TGMenuSheetPallete *)menuSheetPallete; +- (TGMediaAssetsPallete *)mediaAssetsPallete; +- (TGCheckButtonPallete *)checkButtonPallete; + +@optional +- (TGImageBorderPallete *)imageBorderPallete; + +@end + +@interface LegacyComponentsGlobals : NSObject + ++ (void)setProvider:(id)provider; ++ (id)provider; + +@end + +#ifdef __cplusplus +extern "C" { +#endif +UIImage *TGComponentsImageNamed(NSString *name); +NSString *TGComponentsPathForResource(NSString *name, NSString *type); +#ifdef __cplusplus +} +#endif + diff --git a/submodules/LegacyComponents/LegacyComponents/LegacyComponentsGlobals.m b/submodules/LegacyComponents/LegacyComponents/LegacyComponentsGlobals.m new file mode 100644 index 0000000000..e73cd7291c --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/LegacyComponentsGlobals.m @@ -0,0 +1,19 @@ +#import + +#import "LegacyComponentsInternal.h" + +#import "TGLocalization.h" + +static id _provider; + +@implementation LegacyComponentsGlobals + ++ (void)setProvider:(id)provider { + _provider = provider; +} + ++ (id)provider { + return _provider; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/LegacyComponentsInternal.h b/submodules/LegacyComponents/LegacyComponents/LegacyComponentsInternal.h new file mode 100644 index 0000000000..0fac5b8aa4 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/LegacyComponentsInternal.h @@ -0,0 +1,60 @@ +#import "LegacyComponentsGlobals.h" + +@class TGLocalization; + +#ifdef __cplusplus +extern "C" { +#endif + +TGLocalization *legacyEffectiveLocalization(); +NSString *TGLocalized(NSString *s); +bool TGObjectCompare(id obj1, id obj2); +bool TGStringCompare(NSString *s1, NSString *s2); +void TGLegacyLog(NSString *format, ...); +int iosMajorVersion(); +int iosMinorVersion(); + +NSString *TGEncodeText(NSString *string, int key); + +void TGDispatchOnMainThread(dispatch_block_t block); +void TGDispatchAfter(double delay, dispatch_queue_t queue, dispatch_block_t block); + +int deviceMemorySize(); +int cpuCoreCount(); + +#define UIColorRGB(rgb) ([[UIColor alloc] initWithRed:(((rgb >> 16) & 0xff) / 255.0f) green:(((rgb >> 8) & 0xff) / 255.0f) blue:(((rgb) & 0xff) / 255.0f) alpha:1.0f]) +#define UIColorRGBA(rgb,a) ([[UIColor alloc] initWithRed:(((rgb >> 16) & 0xff) / 255.0f) green:(((rgb >> 8) & 0xff) / 255.0f) blue:(((rgb) & 0xff) / 255.0f) alpha:a]) + +#define TGRestrictedToMainThread {if(![[NSThread currentThread] isMainThread]) TGLegacyLog(@"***** Warning: main thread-bound operation is running in background! *****");} + +#define TG_TIMESTAMP_DEFINE(s) CFAbsoluteTime tg_timestamp_##s = CFAbsoluteTimeGetCurrent(); int tg_timestamp_line_##s = __LINE__; +#define TG_TIMESTAMP_MEASURE(s) { CFAbsoluteTime tg_timestamp_current_time = CFAbsoluteTimeGetCurrent(); TGLegacyLog(@"%s %d-%d: %f ms", #s, tg_timestamp_line_##s, __LINE__, (tg_timestamp_current_time - tg_timestamp_##s) * 1000.0); tg_timestamp_##s = tg_timestamp_current_time; tg_timestamp_line_##s = __LINE__; } + +#ifdef __LP64__ +# define CGFloor floor +#else +# define CGFloor floorf +#endif + +#ifdef __LP64__ +# define CGRound round +# define CGCeil ceil +# define CGPow pow +# define CGSin sin +# define CGCos cos +# define CGSqrt sqrt +#else +# define CGRound roundf +# define CGCeil ceilf +# define CGPow powf +# define CGSin sinf +# define CGCos cosf +# define CGSqrt sqrtf +#endif + +#define CGEven(x) ((((int)x) & 1) ? (x + 1) : x) +#define CGOdd(x) ((((int)x) & 1) ? x : (x + 1)) + +#ifdef __cplusplus +} +#endif diff --git a/submodules/LegacyComponents/LegacyComponents/LegacyComponentsInternal.m b/submodules/LegacyComponents/LegacyComponents/LegacyComponentsInternal.m new file mode 100644 index 0000000000..5ef77d899a --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/LegacyComponentsInternal.m @@ -0,0 +1,187 @@ +#import "LegacyComponentsInternal.h" + +#import "TGLocalization.h" + +#import + +#import + +TGLocalization *legacyEffectiveLocalization() { + return [[LegacyComponentsGlobals provider] effectiveLocalization]; +} + +NSString *TGLocalized(NSString *s) { + return [legacyEffectiveLocalization() get:s]; +} + +bool TGObjectCompare(id obj1, id obj2) { + if (obj1 == nil && obj2 == nil) + return true; + + return [obj1 isEqual:obj2]; +} + +bool TGStringCompare(NSString *s1, NSString *s2) { + if (s1.length == 0 && s2.length == 0) + return true; + + if ((s1 == nil) != (s2 == nil)) + return false; + + return s1 == nil || [s1 isEqualToString:s2]; +} + +void TGLegacyLog(NSString *format, ...) +{ + va_list L; + va_start(L, format); + NSString *string = [[NSString alloc] initWithFormat:format arguments:L]; + va_end(L); + [[LegacyComponentsGlobals provider] log:string]; +} + +int iosMajorVersion() +{ + static bool initialized = false; + static int version = 7; + if (!initialized) + { + switch ([[[UIDevice currentDevice] systemVersion] intValue]) + { + case 4: + version = 4; + break; + case 5: + version = 5; + break; + case 6: + version = 6; + break; + case 7: + version = 7; + break; + case 8: + version = 8; + break; + case 9: + version = 9; + break; + case 10: + version = 10; + break; + case 11: + version = 11; + break; + case 12: + version = 12; + break; + default: + version = 9; + break; + } + + initialized = true; + } + return version; +} + +int iosMinorVersion() +{ + static bool initialized = false; + static int version = 0; + if (!initialized) + { + NSString *versionString = [[UIDevice currentDevice] systemVersion]; + NSRange range = [versionString rangeOfString:@"."]; + if (range.location != NSNotFound) + version = [[versionString substringFromIndex:range.location + 1] intValue]; + + initialized = true; + } + return version; +} + +NSString *TGEncodeText(NSString *string, int key) +{ + NSMutableString *result = [[NSMutableString alloc] init]; + + for (int i = 0; i < (int)[string length]; i++) + { + unichar c = [string characterAtIndex:i]; + c += key; + [result appendString:[NSString stringWithCharacters:&c length:1]]; + } + + return result; +} + +int deviceMemorySize() +{ + static int memorySize = 0; + if (memorySize == 0) + { + size_t len; + __int64_t nmem; + + len = sizeof(nmem); + sysctlbyname("hw.memsize", &nmem, &len, NULL, 0); + memorySize = (int)(nmem / (1024 * 1024)); + } + return memorySize; +} + +int cpuCoreCount() +{ + static int count = 0; + if (count == 0) + { + size_t len; + unsigned int ncpu; + + len = sizeof(ncpu); + sysctlbyname("hw.ncpu", &ncpu, &len, NULL, 0); + count = ncpu; + } + + return count; +} + + +void TGDispatchOnMainThread(dispatch_block_t block) +{ + if ([NSThread isMainThread]) + block(); + else + dispatch_async(dispatch_get_main_queue(), block); +} + +void TGDispatchAfter(double delay, dispatch_queue_t queue, dispatch_block_t block) +{ + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)((delay) * NSEC_PER_SEC)), queue, block); +} + +static NSBundle *frameworkBundle() { + static NSBundle *currentBundle = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + currentBundle = [NSBundle bundleForClass:[LegacyComponentsGlobals class]]; + NSString *updatedPath = [[currentBundle bundlePath] stringByAppendingPathComponent:@"LegacyComponentsResources.bundle"]; + currentBundle = [NSBundle bundleWithPath:updatedPath]; + }); + return currentBundle; +} + +UIImage *TGComponentsImageNamed(NSString *name) { + if (iosMajorVersion() < 8) + return [UIImage imageNamed:[NSString stringWithFormat:@"LegacyComponentsResources.bundle/%@", name]]; + + UIImage *image = [UIImage imageNamed:name inBundle:frameworkBundle() compatibleWithTraitCollection:nil]; + if (image == nil) { + assert(true); + } + return image; +} + +NSString *TGComponentsPathForResource(NSString *name, NSString *type) { + return [frameworkBundle() pathForResource:name ofType:type]; +} diff --git a/submodules/LegacyComponents/LegacyComponents/LegacyHTTPRequestOperation.h b/submodules/LegacyComponents/LegacyComponents/LegacyHTTPRequestOperation.h new file mode 100644 index 0000000000..c44d028bcd --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/LegacyHTTPRequestOperation.h @@ -0,0 +1,11 @@ +#import + +@protocol LegacyHTTPRequestOperation + +@property (nonatomic, retain) NSOutputStream *outputStream; + +- (void)setCompletionBlockWithSuccess:(void (^)(NSOperation *operation, id responseObject))success + failure:(void (^)(NSOperation *operation, NSError *error))failure; +- (void)setDownloadProgressBlock:(void (^)(NSInteger bytesRead, NSInteger totalBytesRead, NSInteger totalBytesExpectedToRead))block; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/NSDictionary+CBExtensions.h b/submodules/LegacyComponents/LegacyComponents/NSDictionary+CBExtensions.h new file mode 100755 index 0000000000..285d40b3ba --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/NSDictionary+CBExtensions.h @@ -0,0 +1,16 @@ +// +// NSDictionary+Extensions.h +// Coub +// +// Created by Konstantin Anoshkin on 8.10.13. +// Copyright 2013 Coub. All rights reserved. +// + +#import + + +@interface NSDictionary (CBDictionaryExtensions) + +- (NSString *) coubURIFromVersionTemplateWithPreferredSubstitutions: (NSArray *) preferredVersions; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/NSDictionary+CBExtensions.m b/submodules/LegacyComponents/LegacyComponents/NSDictionary+CBExtensions.m new file mode 100755 index 0000000000..affd8281b4 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/NSDictionary+CBExtensions.m @@ -0,0 +1,38 @@ +// +// NSDictionary+Extensions.m +// Coub +// +// Created by Konstantin Anoshkin on 8.10.13. +// Copyright 2013 Coub. All rights reserved. +// + +#import "NSDictionary+CBExtensions.h" + + +@implementation NSDictionary (CBDictionaryExtensions) + + +- (NSString *) coubURIFromVersionTemplateWithPreferredSubstitutions: (NSArray *) preferredVersions +{ + NSString *urlTemplate = self[@"template"]; + if (urlTemplate) { + NSArray *availableVersions = self[@"versions"]; + __block NSString *bestVersion = nil; + [preferredVersions enumerateObjectsUsingBlock: ^(NSString *version, __unused NSUInteger idx, BOOL *stop) { + if ([availableVersions containsObject: version]) { + bestVersion = version; + *stop = YES; + } + }]; + if (bestVersion) { + //KALog(@"%@", [urlTemplate stringByReplacingOccurrencesOfString: @"%{version}" withString: bestVersion]); + return [urlTemplate stringByReplacingOccurrencesOfString: @"%{version}" withString: bestVersion]; + } else { + //KAObjectLogError(@"Could not find appropriate URI version: {%@}", [preferredVersions componentsJoinedByString: @", "]); + } + } + return nil; +} + + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/NSInputStream+TL.h b/submodules/LegacyComponents/LegacyComponents/NSInputStream+TL.h new file mode 100644 index 0000000000..aacf17f8e8 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/NSInputStream+TL.h @@ -0,0 +1,22 @@ + + +#import + +@interface NSInputStream (TL) + +- (int32_t)readInt32; +- (int32_t)readInt32:(bool *)failed __attribute__((nonnull(1))); +- (int64_t)readInt64; +- (int64_t)readInt64:(bool *)failed __attribute__((nonnull(1))); +- (double)readDouble; +- (double)readDouble:(bool *)failed __attribute__((nonnull(1))); +- (NSData *)readData:(int)length; +- (NSData *)readData:(int)length failed:(bool *)failed __attribute__((nonnull(2))); +- (NSMutableData *)readMutableData:(int)length; +- (NSMutableData *)readMutableData:(int)length failed:(bool *)failed __attribute__((nonnull(2))); +- (NSString *)readString; +- (NSString *)readString:(bool *)failed __attribute__((nonnull(1))); +- (NSData *)readBytes; +- (NSData *)readBytes:(bool *)failed __attribute__((nonnull(1))); + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/NSInputStream+TL.m b/submodules/LegacyComponents/LegacyComponents/NSInputStream+TL.m new file mode 100644 index 0000000000..df5388730f --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/NSInputStream+TL.m @@ -0,0 +1,390 @@ +#import "NSInputStream+TL.h" + +#import "LegacyComponentsInternal.h" + +#import + +static inline int roundUpInput(int numToRound, int multiple) +{ + if (multiple == 0) + { + return numToRound; + } + + int remainder = numToRound % multiple; + if (remainder == 0) + { + return numToRound; + } + + return numToRound + multiple - remainder; +} + +@implementation NSInputStream (TL) + +- (int32_t)readInt32 +{ + int32_t value = 0; + + if ([self read:(uint8_t *)&value maxLength:4] != 4) + { + TGLegacyLog(@"***** Couldn't read int32"); + + @throw [[NSException alloc] initWithName:@"NSInputStream+TLException" reason:@"readInt32 end of stream" userInfo:@{}]; + } + +#if __BYTE_ORDER == __LITTLE_ENDIAN +#elif __BYTE_ORDER == __BIG_ENDIAN +# error "Big endian is not implemented" +#else +# error "Unknown byte order" +#endif + + return value; +} + +- (int32_t)readInt32:(bool *)failed +{ + int32_t value = 0; + + if ([self read:(uint8_t *)&value maxLength:4] != 4) + { + *failed = true; + return 0; + } + +#if __BYTE_ORDER == __LITTLE_ENDIAN +#elif __BYTE_ORDER == __BIG_ENDIAN +# error "Big endian is not implemented" +#else +# error "Unknown byte order" +#endif + + return value; +} + +- (int64_t)readInt64 +{ + int64_t value = 0; + + if ([self read:(uint8_t *)&value maxLength:8] != 8) + { + TGLegacyLog(@"***** Couldn't read int64"); + } + +#if __BYTE_ORDER == __LITTLE_ENDIAN +#elif __BYTE_ORDER == __BIG_ENDIAN +# error "Big endian is not implemented" +#else +# error "Unknown byte order" +#endif + + return value; +} + +- (int64_t)readInt64:(bool *)failed +{ + int64_t value = 0; + + if ([self read:(uint8_t *)&value maxLength:8] != 8) + { + *failed = true; + return 0; + } + +#if __BYTE_ORDER == __LITTLE_ENDIAN +#elif __BYTE_ORDER == __BIG_ENDIAN +# error "Big endian is not implemented" +#else +# error "Unknown byte order" +#endif + + return value; +} + +- (double)readDouble +{ + double value = 0.0; + + if ([self read:(uint8_t *)&value maxLength:8] != 8) + { + TGLegacyLog(@"***** Couldn't read double"); + } + +#if __BYTE_ORDER == __LITTLE_ENDIAN +#elif __BYTE_ORDER == __BIG_ENDIAN +# error "Big endian is not implemented" +#else +# error "Unknown byte order" +#endif + + return value; +} + +- (double)readDouble:(bool *)failed +{ + double value = 0.0; + + if ([self read:(uint8_t *)&value maxLength:8] != 8) + { + *failed = true; + return 0.0; + } + +#if __BYTE_ORDER == __LITTLE_ENDIAN +#elif __BYTE_ORDER == __BIG_ENDIAN +# error "Big endian is not implemented" +#else +# error "Unknown byte order" +#endif + + return value; +} + +- (NSData *)readData:(int)length +{ + uint8_t *bytes = (uint8_t *)malloc(length); + int readLen = (int)[self read:bytes maxLength:length]; + if (readLen != length) + { + TGLegacyLog(@"***** Couldn't read %d bytes", length); + } + NSData *data = [[NSData alloc] initWithBytesNoCopy:bytes length:length freeWhenDone:true]; + return data; +} + +- (NSData *)readData:(int)length failed:(bool *)failed +{ + uint8_t *bytes = (uint8_t *)malloc(length); + int readLen = (int)[self read:bytes maxLength:length]; + if (readLen != length) + { + free(bytes); + *failed = true; + return nil; + } + NSData *data = [[NSData alloc] initWithBytesNoCopy:bytes length:length freeWhenDone:true]; + return data; +} + +- (NSMutableData *)readMutableData:(int)length +{ + uint8_t *bytes = (uint8_t *)malloc(length); + int readLen = (int)[self read:bytes maxLength:length]; + if (readLen != length) + { + TGLegacyLog(@"***** Couldn't read %d bytes", length); + } + NSMutableData *data = [[NSMutableData alloc] initWithBytesNoCopy:bytes length:length freeWhenDone:true]; + return data; +} + +- (NSMutableData *)readMutableData:(int)length failed:(bool *)failed +{ + uint8_t *bytes = (uint8_t *)malloc(length); + int readLen = (int)[self read:bytes maxLength:length]; + if (readLen != length) + { + free(bytes); + *failed = true; + return nil; + } + NSMutableData *data = [[NSMutableData alloc] initWithBytesNoCopy:bytes length:length freeWhenDone:true]; + return data; +} + +- (NSString *)readString +{ + uint8_t tmp = 0; + [self read:&tmp maxLength:1]; + + int paddingBytes = 0; + + int32_t length = tmp; + if (length == 254) + { + length = 0; + [self read:((uint8_t *)&length) + 1 maxLength:3]; + length >>= 8; + +#if __BYTE_ORDER == __LITTLE_ENDIAN +#elif __BYTE_ORDER == __BIG_ENDIAN +# error "Big endian is not implemented" +#else +# error "Unknown byte order" +#endif + + paddingBytes = roundUpInput(length, 4) - length; + } + else + { + paddingBytes = roundUpInput(length + 1, 4) - (length + 1); + } + + NSString *string = nil; + + if (length > 0) + { + uint8_t *bytes = (uint8_t *)malloc(length); + int readLen = (int)[self read:bytes maxLength:length]; + if (readLen != length) + { + TGLegacyLog(@"***** Couldn't read %d bytes", length); + } + + string = [[NSString alloc] initWithBytesNoCopy:bytes length:length encoding:NSUTF8StringEncoding freeWhenDone:true]; + } + else + { + string = @""; + } + + for (int i = 0; i < paddingBytes; i++) + [self read:&tmp maxLength:1]; + + return string; +} + +- (NSString *)readString:(bool *)failed +{ + uint8_t tmp = 0; + [self read:&tmp maxLength:1]; + + int paddingBytes = 0; + + int32_t length = tmp; + if (length == 254) + { + length = 0; + [self read:((uint8_t *)&length) + 1 maxLength:3]; + length >>= 8; + +#if __BYTE_ORDER == __LITTLE_ENDIAN +#elif __BYTE_ORDER == __BIG_ENDIAN +# error "Big endian is not implemented" +#else +# error "Unknown byte order" +#endif + + paddingBytes = roundUpInput(length, 4) - length; + } + else + { + paddingBytes = roundUpInput(length + 1, 4) - (length + 1); + } + + NSString *string = nil; + + if (length > 0) + { + uint8_t *bytes = (uint8_t *)malloc(length); + int readLen = (int)[self read:bytes maxLength:length]; + if (readLen != length) + { + free(bytes); + *failed = true; + return nil; + } + + string = [[NSString alloc] initWithBytesNoCopy:bytes length:length encoding:NSUTF8StringEncoding freeWhenDone:true]; + } + else + { + string = @""; + } + + for (int i = 0; i < paddingBytes; i++) + [self read:&tmp maxLength:1]; + + return string; +} + +- (NSData *)readBytes +{ + uint8_t tmp = 0; + [self read:&tmp maxLength:1]; + + int paddingBytes = 0; + + int32_t length = tmp; + if (length == 254) + { + length = 0; + [self read:((uint8_t *)&length) + 1 maxLength:3]; + length >>= 8; + +#if __BYTE_ORDER == __LITTLE_ENDIAN +#elif __BYTE_ORDER == __BIG_ENDIAN +# error "Big endian is not implemented" +#else +# error "Unknown byte order" +#endif + + paddingBytes = roundUpInput(length, 4) - length; + } + else + { + paddingBytes = roundUpInput(length + 1, 4) - (length + 1); + } + + uint8_t *bytes = (uint8_t *)malloc(length); + int readLen = (int)[self read:bytes maxLength:length]; + if (readLen != length) + { + TGLegacyLog(@"***** Couldn't read %d bytes", length); + } + + NSData *result = [NSData dataWithBytesNoCopy:bytes length:length freeWhenDone:true]; + + for (int i = 0; i < paddingBytes; i++) + [self read:&tmp maxLength:1]; + + return result; +} + +- (NSData *)readBytes:(bool *)failed +{ + uint8_t tmp = 0; + [self read:&tmp maxLength:1]; + + int paddingBytes = 0; + + int32_t length = tmp; + if (length == 254) + { + length = 0; + [self read:((uint8_t *)&length) + 1 maxLength:3]; + length >>= 8; + +#if __BYTE_ORDER == __LITTLE_ENDIAN +#elif __BYTE_ORDER == __BIG_ENDIAN +# error "Big endian is not implemented" +#else +# error "Unknown byte order" +#endif + + paddingBytes = roundUpInput(length, 4) - length; + } + else + { + paddingBytes = roundUpInput(length + 1, 4) - (length + 1); + } + + uint8_t *bytes = (uint8_t *)malloc(length); + int readLen = (int)[self read:bytes maxLength:length]; + if (readLen != length) + { + free(bytes); + *failed = true; + return nil; + } + + NSData *result = [NSData dataWithBytesNoCopy:bytes length:length freeWhenDone:true]; + + for (int i = 0; i < paddingBytes; i++) + [self read:&tmp maxLength:1]; + + return result; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/NSMutableArray+STKAudioPlayer.h b/submodules/LegacyComponents/LegacyComponents/NSMutableArray+STKAudioPlayer.h new file mode 100755 index 0000000000..12ca8396b4 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/NSMutableArray+STKAudioPlayer.h @@ -0,0 +1,17 @@ +// +// NSMutableArray+STKAudioPlayer.h +// StreamingKit +// +// Created by Thong Nguyen on 30/01/2014. +// Copyright (c) 2014 Thong Nguyen. All rights reserved. +// + +#import + +@interface NSMutableArray (STKAudioPlayer) +-(void) enqueue:(id)obj; +-(void) skipQueue:(id)obj; +-(void) skipQueueWithQueue:(NSMutableArray*)queue; +-(id) dequeue; +-(id) peek; +@end diff --git a/submodules/LegacyComponents/LegacyComponents/NSMutableArray+STKAudioPlayer.m b/submodules/LegacyComponents/LegacyComponents/NSMutableArray+STKAudioPlayer.m new file mode 100755 index 0000000000..493868f45d --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/NSMutableArray+STKAudioPlayer.m @@ -0,0 +1,50 @@ +// +// NSMutableArray+STKAudioPlayer.m +// StreamingKit +// +// Created by Thong Nguyen on 30/01/2014. +// Copyright (c) 2014 Thong Nguyen. All rights reserved. +// + +#import "NSMutableArray+STKAudioPlayer.h" + +@implementation NSMutableArray (STKAudioPlayer) + +-(void) enqueue:(id)obj +{ + [self insertObject:obj atIndex:0]; +} + +-(void) skipQueue:(id)obj +{ + [self addObject:obj]; +} + +-(void) skipQueueWithQueue:(NSMutableArray*)queue +{ + for (id item in queue) + { + [self addObject:item]; + } +} + +-(id) dequeue +{ + if ([self count] == 0) + { + return nil; + } + + id retval = [self lastObject]; + + [self removeLastObject]; + + return retval; +} + +-(id) peek +{ + return [self lastObject]; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/NSObject+TGLock.h b/submodules/LegacyComponents/LegacyComponents/NSObject+TGLock.h new file mode 100644 index 0000000000..e2a6f21efa --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/NSObject+TGLock.h @@ -0,0 +1,17 @@ + + +#import + +#import + +#define TG_SYNCHRONIZED_DEFINE(lock) pthread_mutex_t _TG_SYNCHRONIZED_##lock +#define TG_SYNCHRONIZED_INIT(lock) pthread_mutex_init(&_TG_SYNCHRONIZED_##lock, NULL) +#define TG_SYNCHRONIZED_BEGIN(lock) pthread_mutex_lock(&_TG_SYNCHRONIZED_##lock); +#define TG_SYNCHRONIZED_END(lock) pthread_mutex_unlock(&_TG_SYNCHRONIZED_##lock); + +@interface NSObject (TGLock) + +- (void)tgLockObject; +- (void)tgUnlockObject; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/NSObject+TGLock.m b/submodules/LegacyComponents/LegacyComponents/NSObject+TGLock.m new file mode 100644 index 0000000000..fd70fde64a --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/NSObject+TGLock.m @@ -0,0 +1,73 @@ +#import "NSObject+TGLock.h" + +#import + +static const char *lockPropertyKey = "TGObjectLock::lock"; + +@interface TGObjectLockImpl : NSObject +{ + TG_SYNCHRONIZED_DEFINE(objectLock); +} + +- (void)tgTakeLock; +- (void)tgFreeLock; + +@end + +@implementation TGObjectLockImpl + +- (id)init +{ + self = [super init]; + if (self != nil) + { + TG_SYNCHRONIZED_INIT(objectLock); + } + return self; +} + +- (void)tgTakeLock +{ + TG_SYNCHRONIZED_BEGIN(objectLock); +} + +- (void)tgFreeLock +{ + TG_SYNCHRONIZED_END(objectLock); +} + +@end + +@implementation NSObject (TGLock) + +- (void)tgLockObject +{ + TGObjectLockImpl *lock = (TGObjectLockImpl *)objc_getAssociatedObject(self, lockPropertyKey); + if (lock == nil) + { + @synchronized(self) + { + lock = [[TGObjectLockImpl alloc] init]; + objc_setAssociatedObject(self, lockPropertyKey, lock, OBJC_ASSOCIATION_RETAIN); + } + } + + [lock tgTakeLock]; +} + +- (void)tgUnlockObject +{ + TGObjectLockImpl *lock = (TGObjectLockImpl *)objc_getAssociatedObject(self, lockPropertyKey); + if (lock == nil) + { + @synchronized(self) + { + lock = [[TGObjectLockImpl alloc] init]; + objc_setAssociatedObject(self, lockPropertyKey, lock, OBJC_ASSOCIATION_RETAIN); + } + } + + [lock tgFreeLock]; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/NSValue+JNWAdditions.h b/submodules/LegacyComponents/LegacyComponents/NSValue+JNWAdditions.h new file mode 100644 index 0000000000..3596c4570b --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/NSValue+JNWAdditions.h @@ -0,0 +1,28 @@ +#import +#import + +typedef NS_ENUM(NSInteger, JNWValueType) { + JNWValueTypeNumber, + JNWValueTypePoint, + JNWValueTypeSize, + JNWValueTypeRect, + JNWValueTypeAffineTransform, + JNWValueTypeTransform3D, + JNWValueTypeUnknown +}; + +@interface NSValue (JNWAdditions) + +- (CGRect)jnw_rectValue; +- (CGSize)jnw_sizeValue; +- (CGPoint)jnw_pointValue; +- (CGAffineTransform)jnw_affineTransformValue; + ++ (NSValue *)jnw_valueWithRect:(CGRect)rect; ++ (NSValue *)jnw_valueWithSize:(CGSize)size; ++ (NSValue *)jnw_valueWithPoint:(CGPoint)point; ++ (NSValue *)jnw_valueWithAffineTransform:(CGAffineTransform)transform; + +- (JNWValueType)jnw_type; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/NSValue+JNWAdditions.m b/submodules/LegacyComponents/LegacyComponents/NSValue+JNWAdditions.m new file mode 100644 index 0000000000..d44a379ad0 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/NSValue+JNWAdditions.m @@ -0,0 +1,121 @@ +/* + Copyright (c) 2013, Jonathan Willing. All rights reserved. + Licensed under the MIT license + + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + documentation files (the "Software"), to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and + to permit persons to whom the Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED + TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + IN THE SOFTWARE. + */ + +#import "NSValue+JNWAdditions.h" + +@implementation NSValue (JNWAdditions) + +#ifdef __IPHONE_OS_VERSION_MIN_REQUIRED + +- (CGRect)jnw_rectValue { + return [self CGRectValue]; +} + +- (CGSize)jnw_sizeValue { + return [self CGSizeValue]; +} + +- (CGPoint)jnw_pointValue { + return [self CGPointValue]; +} + +- (CGAffineTransform)jnw_affineTransformValue { + return [self CGAffineTransformValue]; +} + ++ (NSValue *)jnw_valueWithRect:(CGRect)rect { + return [self valueWithCGRect:rect]; +} + ++ (NSValue *)jnw_valueWithPoint:(CGPoint)point { + return [self valueWithCGPoint:point]; +} + ++ (NSValue *)jnw_valueWithSize:(CGSize)size { + return [self valueWithCGSize:size]; +} + ++ (NSValue *)jnw_valueWithAffineTransform:(CGAffineTransform)transform { + return [self valueWithCGAffineTransform:transform]; +} + +#elif TARGET_OS_MAC + +- (CGRect)jnw_rectValue { + return [self rectValue]; +} + +- (CGSize)jnw_sizeValue { + return [self sizeValue]; +} + +- (CGPoint)jnw_pointValue { + return [self pointValue]; +} + +- (CGAffineTransform)jnw_affineTransformValue { + CGAffineTransform transform; + [self getValue:&transform]; + return transform; +} + ++ (NSValue *)jnw_valueWithRect:(CGRect)rect { + return [self valueWithRect:rect]; +} + ++ (NSValue *)jnw_valueWithPoint:(CGPoint)point { + return [self valueWithPoint:point]; +} + ++ (NSValue *)jnw_valueWithSize:(CGSize)size { + return [self valueWithSize:size]; +} + ++ (NSValue *)jnw_valueWithAffineTransform:(CGAffineTransform)transform { + return [NSValue valueWithBytes:&transform objCType:@encode(CGAffineTransform)]; +} + +#endif + +- (JNWValueType)jnw_type { + const char *type = self.objCType; + + static const NSInteger numberofNumberTypes = 10; + static const char *numberTypes[numberofNumberTypes] = { "i", "s", "l", "q", "I", "S", "L", "Q", "f", "d" }; + + for (NSInteger i = 0; i < numberofNumberTypes; i++) { + if (strcmp(type, numberTypes[i]) == 0) { + return JNWValueTypeNumber; + } + } + if (strcmp(type, @encode(CGPoint)) == 0) { + return JNWValueTypePoint; + } else if (strcmp(type, @encode(CGSize)) == 0) { + return JNWValueTypeSize; + } else if (strcmp(type, @encode(CGRect)) == 0) { + return JNWValueTypeRect; + } else if (strcmp(type, @encode(CGAffineTransform)) == 0) { + return JNWValueTypeAffineTransform; + } else if (strcmp(type, @encode(CATransform3D)) == 0) { + return JNWValueTypeTransform3D; + } else { + return JNWValueTypeUnknown; + } +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGBlurTool.h b/submodules/LegacyComponents/LegacyComponents/PGBlurTool.h new file mode 100644 index 0000000000..0bf2e80908 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGBlurTool.h @@ -0,0 +1,21 @@ +#import "PGPhotoTool.h" +#import "PGPhotoBlurPass.h" + +@interface PGBlurToolValue : NSObject + +@property (nonatomic, assign) PGBlurToolType type; +@property (nonatomic, assign) CGPoint point; +@property (nonatomic, assign) CGFloat size; +@property (nonatomic, assign) CGFloat falloff; +@property (nonatomic, assign) CGFloat angle; + +@property (nonatomic, assign) CGFloat intensity; +@property (nonatomic, assign) bool editingIntensity; + +@end + +@interface PGBlurTool : PGPhotoTool + +@property (nonatomic, readonly) NSString *intensityEditingTitle; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGBlurTool.m b/submodules/LegacyComponents/LegacyComponents/PGBlurTool.m new file mode 100644 index 0000000000..897ebffb96 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGBlurTool.m @@ -0,0 +1,218 @@ +#import "PGBlurTool.h" + +#import "LegacyComponentsInternal.h" + +#import "PGPhotoBlurPass.h" +#import "TGPhotoEditorBlurToolView.h" +#import "TGPhotoEditorBlurAreaView.h" + +@implementation PGBlurToolValue + +- (instancetype)copyWithZone:(NSZone *)__unused zone +{ + PGBlurToolValue *value = [[PGBlurToolValue alloc] init]; + value.type = self.type; + value.point = self.point; + value.size = self.size; + value.falloff = self.falloff; + value.angle = self.angle; + value.intensity = self.intensity; + value.editingIntensity = self.editingIntensity; + + return value; +} + +- (BOOL)isEqual:(id)object +{ + if (object == self) + return true; + + if (!object || ![object isKindOfClass:[self class]]) + return false; + + PGBlurToolValue *value = (PGBlurToolValue *)object; + + if (value.type != self.type) + return false; + + if (!CGPointEqualToPoint(value.point, self.point)) + return false; + + if (value.size != self.size) + return false; + + if (value.falloff != self.falloff) + return false; + + if (value.angle != self.angle) + return false; + + if (value.intensity != self.intensity) + return false; + + return true; +} + +@end + +@implementation PGBlurTool + +- (instancetype)init +{ + self = [super init]; + if (self != nil) + { + _identifier = @"blur"; + _type = PGPhotoToolTypePass; + _order = 3; + + _pass = [[PGPhotoBlurPass alloc] init]; + + _minimumValue = 0; + _maximumValue = 100; + _defaultValue = 50; + + PGBlurToolValue *value = [[PGBlurToolValue alloc] init]; + value.type = PGBlurToolTypeNone; + value.point = CGPointMake(0.5f, 0.5f); + value.falloff = 0.12f; + value.size = 0.24f; + value.angle = 0; + value.intensity = _defaultValue; + + self.value = value; + } + return self; +} + +- (NSString *)title +{ + return TGLocalized(@"PhotoEditor.BlurTool"); +} + +- (NSString *)intensityEditingTitle +{ + return TGLocalized(@"PhotoEditor.BlurToolRadius"); +} + +- (UIView *)itemControlViewWithChangeBlock:(void (^)(id newValue, bool animated))changeBlock +{ + return [self itemControlViewWithChangeBlock:changeBlock explicit:false nameWidth:0.0f]; +} + +- (UIView *)itemControlViewWithChangeBlock:(void (^)(id, bool))changeBlock explicit:(bool)explicit nameWidth:(CGFloat)__unused nameWidth +{ + __weak PGBlurTool *weakSelf = self; + + UIView *view = [[TGPhotoEditorBlurToolView alloc] initWithEditorItem:self]; + view.valueChanged = ^(id newValue, bool animated) + { + __strong PGBlurTool *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + if (!explicit && [strongSelf.tempValue isEqual:newValue]) + return; + + if (explicit && [strongSelf.value isEqual:newValue]) + return; + + if (!explicit) + strongSelf.tempValue = newValue; + else + strongSelf.value = newValue; + + if (changeBlock != nil) + changeBlock(newValue, animated); + }; + return view; +} + +- (UIView *)itemAreaViewWithChangeBlock:(void (^)(id))changeBlock explicit:(bool)explicit +{ + __weak PGBlurTool *weakSelf = self; + + UIView *view = [[TGPhotoEditorBlurAreaView alloc] initWithEditorItem:self]; + view.valueChanged = ^(id newValue, __unused bool animated) + { + __strong PGPhotoTool *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + if (newValue != nil) + { + if (!explicit && [strongSelf.tempValue isEqual:newValue]) + return; + + if (explicit && [strongSelf.value isEqual:newValue]) + return; + + if (!explicit) + strongSelf.tempValue = newValue; + else + strongSelf.value = newValue; + } + + if (changeBlock != nil) + changeBlock(newValue); + }; + return view; +} + +- (Class)valueClass +{ + return [PGBlurToolValue class]; +} + +- (PGPhotoProcessPass *)pass +{ + PGBlurToolValue *value = (PGBlurToolValue *)self.displayValue; + + if (value.type == PGBlurToolTypeNone) + return nil; + + [self updatePassParameters]; + + return _pass; +} + +- (void)updatePassParameters +{ + PGBlurToolValue *value = (PGBlurToolValue *)self.displayValue; + + PGPhotoBlurPass *blurPass = (PGPhotoBlurPass *)_pass; + blurPass.type = value.type; + blurPass.size = value.size; + blurPass.point = value.point; + blurPass.angle = value.angle; + blurPass.falloff = value.falloff; +} + +- (bool)shouldBeSkipped +{ + if (self.disabled) + return true; + + return (((PGBlurToolValue *)self.displayValue).type == PGBlurToolTypeNone); +} + +- (NSString *)stringValue +{ + if ([self.value isKindOfClass:[PGBlurToolValue class]]) + { + PGBlurToolValue *value = (PGBlurToolValue *)self.value; + if (value.type == PGBlurToolTypeRadial) + return TGLocalized(@"PhotoEditor.BlurToolRadial"); + else if (value.type == PGBlurToolTypeLinear) + return TGLocalized(@"PhotoEditor.BlurToolLinear"); + } + + return nil; +} + +- (bool)isSimple +{ + return false; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGCamera.h b/submodules/LegacyComponents/LegacyComponents/PGCamera.h new file mode 100644 index 0000000000..f71462efdf --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGCamera.h @@ -0,0 +1,134 @@ +#import +#import +#import +#import + +#import + +typedef enum { + PGCameraAuthorizationStatusNotDetermined, + PGCameraAuthorizationStatusRestricted, + PGCameraAuthorizationStatusDenied, + PGCameraAuthorizationStatusAuthorized +} PGCameraAuthorizationStatus; + +typedef enum { + PGMicrophoneAuthorizationStatusNotDetermined, + PGMicrophoneAuthorizationStatusRestricted, + PGMicrophoneAuthorizationStatusDenied, + PGMicrophoneAuthorizationStatusAuthorized +} PGMicrophoneAuthorizationStatus; + +typedef enum +{ + PGCameraModeUndefined, + PGCameraModePhoto, + PGCameraModeVideo, + PGCameraModeSquare, + PGCameraModeClip +} PGCameraMode; + +typedef enum +{ + PGCameraFlashModeOff, + PGCameraFlashModeOn, + PGCameraFlashModeAuto +} PGCameraFlashMode; + +typedef enum +{ + PGCameraPositionUndefined, + PGCameraPositionRear, + PGCameraPositionFront +} PGCameraPosition; + +@class PGCameraCaptureSession; +@class PGCameraDeviceAngleSampler; +@class TGCameraPreviewView; + +@interface PGCamera : NSObject + +@property (readonly, nonatomic) PGCameraCaptureSession *captureSession; +@property (readonly, nonatomic) PGCameraDeviceAngleSampler *deviceAngleSampler; + +@property (nonatomic, copy) void(^captureStarted)(bool resumed); +@property (nonatomic, copy) void(^captureStopped)(bool paused); + +@property (nonatomic, copy) void(^beganModeChange)(PGCameraMode mode, void(^commitBlock)(void)); +@property (nonatomic, copy) void(^finishedModeChange)(void); + +@property (nonatomic, copy) void(^beganPositionChange)(bool targetPositionHasFlash, bool targetPositionHasZoom, void(^commitBlock)(void)); +@property (nonatomic, copy) void(^finishedPositionChange)(void); + +@property (nonatomic, copy) void(^beganAdjustingFocus)(void); +@property (nonatomic, copy) void(^finishedAdjustingFocus)(void); + +@property (nonatomic, copy) void(^flashActivityChanged)(bool flashActive); +@property (nonatomic, copy) void(^flashAvailabilityChanged)(bool flashAvailable); + +@property (nonatomic, copy) void(^beganVideoRecording)(bool moment); +@property (nonatomic, copy) void(^finishedVideoRecording)(bool moment); +@property (nonatomic, copy) void(^reallyBeganVideoRecording)(bool moment); + +@property (nonatomic, copy) void(^captureInterrupted)(AVCaptureSessionInterruptionReason reason); + +@property (nonatomic, copy) void(^onAutoStartVideoRecording)(void); + +@property (nonatomic, copy) UIInterfaceOrientation(^requestedCurrentInterfaceOrientation)(bool *mirrored); + +@property (nonatomic, assign) PGCameraMode cameraMode; +@property (nonatomic, assign) PGCameraFlashMode flashMode; + +@property (nonatomic, readonly) bool isZoomAvailable; +@property (nonatomic, assign) CGFloat zoomLevel; + +@property (nonatomic, assign) bool disableResultMirroring; + +@property (nonatomic, assign) bool disabled; +@property (nonatomic, readonly) bool isCapturing; +@property (nonatomic, readonly) NSTimeInterval videoRecordingDuration; + +@property (nonatomic, assign) bool autoStartVideoRecording; + +- (instancetype)initWithMode:(PGCameraMode)mode position:(PGCameraPosition)position; + +- (void)attachPreviewView:(TGCameraPreviewView *)previewView; + +- (bool)supportsExposurePOI; +- (bool)supportsFocusPOI; +- (void)setFocusPoint:(CGPoint)focusPoint; + +- (bool)supportsExposureTargetBias; +- (void)beginExposureTargetBiasChange; +- (void)setExposureTargetBias:(CGFloat)bias; +- (void)endExposureTargetBiasChange; + +- (void)captureNextFrameCompletion:(void (^)(UIImage * image))completion; + +- (void)takePhotoWithCompletion:(void (^)(UIImage *result, PGCameraShotMetadata *metadata))completion; + +- (void)startVideoRecordingForMoment:(bool)moment completion:(void (^)(NSURL *, CGAffineTransform transform, CGSize dimensions, NSTimeInterval duration, bool success))completion; +- (void)stopVideoRecording; +- (bool)isRecordingVideo; + +- (void)startCaptureForResume:(bool)resume completion:(void (^)(void))completion; +- (void)stopCaptureForPause:(bool)pause completion:(void (^)(void))completion; + +- (bool)isResetNeeded; +- (void)resetSynchronous:(bool)synchronous completion:(void (^)(void))completion; +- (void)resetTerminal:(bool)terminal synchronous:(bool)synchronous completion:(void (^)(void))completion; + +- (bool)hasFlash; +- (bool)flashActive; +- (bool)flashAvailable; + +- (PGCameraPosition)togglePosition; + ++ (bool)cameraAvailable; ++ (bool)hasFrontCamera; ++ (bool)hasRearCamera; + ++ (PGCameraAuthorizationStatus)cameraAuthorizationStatus; ++ (PGMicrophoneAuthorizationStatus)microphoneAuthorizationStatus; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGCamera.m b/submodules/LegacyComponents/LegacyComponents/PGCamera.m new file mode 100644 index 0000000000..1a47ec0667 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGCamera.m @@ -0,0 +1,838 @@ +#import "PGCamera.h" + +#import "LegacyComponentsInternal.h" + +#import "PGCameraCaptureSession.h" +#import "PGCameraMovieWriter.h" +#import "PGCameraDeviceAngleSampler.h" + +#import + +#import "TGCameraPreviewView.h" + +NSString *const PGCameraFlashActiveKey = @"flashActive"; +NSString *const PGCameraFlashAvailableKey = @"flashAvailable"; +NSString *const PGCameraTorchActiveKey = @"torchActive"; +NSString *const PGCameraTorchAvailableKey = @"torchAvailable"; +NSString *const PGCameraAdjustingFocusKey = @"adjustingFocus"; + +@interface PGCamera () +{ + dispatch_queue_t cameraProcessingQueue; + dispatch_queue_t audioProcessingQueue; + + AVCaptureDevice *_microphone; + AVCaptureVideoDataOutput *videoOutput; + AVCaptureAudioDataOutput *audioOutput; + + PGCameraDeviceAngleSampler *_deviceAngleSampler; + + bool _subscribedForCameraChanges; + + bool _invalidated; + bool _wasCapturingOnEnterBackground; + + bool _capturing; + bool _moment; + + TGCameraPreviewView *_previewView; + + NSTimeInterval _captureStartTime; +} +@end + +@implementation PGCamera + +- (instancetype)init +{ + return [self initWithMode:PGCameraModePhoto position:PGCameraPositionUndefined]; +} + +- (instancetype)initWithMode:(PGCameraMode)mode position:(PGCameraPosition)position +{ + self = [super init]; + if (self != nil) + { + _captureSession = [[PGCameraCaptureSession alloc] initWithMode:mode position:position]; + _deviceAngleSampler = [[PGCameraDeviceAngleSampler alloc] init]; + [_deviceAngleSampler startMeasuring]; + + __weak PGCamera *weakSelf = self; + self.captureSession.requestPreviewIsMirrored = ^bool + { + __strong PGCamera *strongSelf = weakSelf; + if (strongSelf == nil || strongSelf->_previewView == nil) + return false; + + TGCameraPreviewView *previewView = strongSelf->_previewView; + return previewView.captureConnection.videoMirrored; + }; + + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleEnteredBackground:) name:UIApplicationDidEnterBackgroundNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleEnteredForeground:) name:UIApplicationWillEnterForegroundNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleRuntimeError:) name:AVCaptureSessionRuntimeErrorNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleInterrupted:) name:AVCaptureSessionWasInterruptedNotification object:nil]; + } + return self; +} + +- (void)dealloc +{ + TGLegacyLog(@"Camera: dealloc"); + [_deviceAngleSampler stopMeasuring]; + [self _unsubscribeFromCameraChanges]; + + self.captureSession.requestPreviewIsMirrored = nil; + + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidEnterBackgroundNotification object:nil]; + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationWillEnterForegroundNotification object:nil]; + [[NSNotificationCenter defaultCenter] removeObserver:self name:AVCaptureSessionRuntimeErrorNotification object:nil]; + [[NSNotificationCenter defaultCenter] removeObserver:self name:AVCaptureSessionWasInterruptedNotification object:nil]; +} + +- (void)handleEnteredBackground:(NSNotification *)__unused notification +{ + if (self.isCapturing) + _wasCapturingOnEnterBackground = true; + + [self stopCaptureForPause:true completion:nil]; +} + +- (void)handleEnteredForeground:(NSNotification *)__unused notification +{ + if (_wasCapturingOnEnterBackground) + { + _wasCapturingOnEnterBackground = false; + [self startCaptureForResume:true completion:nil]; + } +} + +- (void)handleRuntimeError:(NSNotification *)notification +{ + TGLegacyLog(@"ERROR: Camera runtime error: %@", notification.userInfo[AVCaptureSessionErrorKey]); + + __weak PGCamera *weakSelf = self; + TGDispatchAfter(1.5f, [PGCamera cameraQueue]._dispatch_queue, ^ + { + __strong PGCamera *strongSelf = weakSelf; + if (strongSelf == nil || strongSelf->_invalidated) + return; + + [strongSelf _unsubscribeFromCameraChanges]; + + for (AVCaptureInput *input in strongSelf.captureSession.inputs) + [strongSelf.captureSession removeInput:input]; + for (AVCaptureOutput *output in strongSelf.captureSession.outputs) + [strongSelf.captureSession removeOutput:output]; + + [strongSelf.captureSession performInitialConfigurationWithCompletion:^ + { + __strong PGCamera *strongSelf = weakSelf; + if (strongSelf != nil) + [strongSelf _subscribeForCameraChanges]; + }]; + }); +} + +- (void)handleInterrupted:(NSNotification *)notification +{ + if (iosMajorVersion() < 9) + return; + + AVCaptureSessionInterruptionReason reason = [notification.userInfo[AVCaptureSessionInterruptionReasonKey] integerValue]; + TGLegacyLog(@"WARNING: Camera was interrupted with reason %d", reason); + if (self.captureInterrupted != nil) + self.captureInterrupted(reason); + + if (self.isRecordingVideo) + [self stopVideoRecording]; +} + +- (void)_subscribeForCameraChanges +{ + if (_subscribedForCameraChanges) + return; + + _subscribedForCameraChanges = true; + + [self.captureSession.videoDevice addObserver:self forKeyPath:PGCameraFlashActiveKey options:NSKeyValueObservingOptionNew context:NULL]; + [self.captureSession.videoDevice addObserver:self forKeyPath:PGCameraFlashAvailableKey options:NSKeyValueObservingOptionNew context:NULL]; + [self.captureSession.videoDevice addObserver:self forKeyPath:PGCameraTorchActiveKey options:NSKeyValueObservingOptionNew context:NULL]; + [self.captureSession.videoDevice addObserver:self forKeyPath:PGCameraTorchAvailableKey options:NSKeyValueObservingOptionNew context:NULL]; + [self.captureSession.videoDevice addObserver:self forKeyPath:PGCameraAdjustingFocusKey options:NSKeyValueObservingOptionNew context:NULL]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(subjectAreaChanged:) name:AVCaptureDeviceSubjectAreaDidChangeNotification object:self.captureSession.videoDevice]; +} + +- (void)_unsubscribeFromCameraChanges +{ + if (!_subscribedForCameraChanges) + return; + + _subscribedForCameraChanges = false; + + @try { + [self.captureSession.videoDevice removeObserver:self forKeyPath:PGCameraFlashActiveKey]; + [self.captureSession.videoDevice removeObserver:self forKeyPath:PGCameraFlashAvailableKey]; + [self.captureSession.videoDevice removeObserver:self forKeyPath:PGCameraTorchActiveKey]; + [self.captureSession.videoDevice removeObserver:self forKeyPath:PGCameraTorchAvailableKey]; + [self.captureSession.videoDevice removeObserver:self forKeyPath:PGCameraAdjustingFocusKey]; + [[NSNotificationCenter defaultCenter] removeObserver:self name:AVCaptureDeviceSubjectAreaDidChangeNotification object:self.captureSession.videoDevice]; + } @catch(NSException *e) { } +} + +- (void)attachPreviewView:(TGCameraPreviewView *)previewView +{ + TGCameraPreviewView *currentPreviewView = _previewView; + if (currentPreviewView != nil) + [currentPreviewView invalidate]; + + _previewView = previewView; + [previewView setupWithCamera:self]; + + __weak PGCamera *weakSelf = self; + [[PGCamera cameraQueue] dispatch:^ + { + __strong PGCamera *strongSelf = weakSelf; + if (strongSelf == nil || strongSelf->_invalidated) + return; + + [strongSelf.captureSession performInitialConfigurationWithCompletion:^ + { + __strong PGCamera *strongSelf = weakSelf; + if (strongSelf != nil) + [strongSelf _subscribeForCameraChanges]; + }]; + }]; +} + +#pragma mark - + +- (bool)isCapturing +{ + return _capturing; +} + +- (void)startCaptureForResume:(bool)resume completion:(void (^)(void))completion +{ + if (_invalidated) + return; + + [[PGCamera cameraQueue] dispatch:^ + { + if (self.captureSession.isRunning) + return; + + _capturing = true; + + TGLegacyLog(@"Camera: start capture"); +#if !TARGET_IPHONE_SIMULATOR + [self.captureSession startRunning]; +#endif + + if (_captureStartTime < FLT_EPSILON) + _captureStartTime = CFAbsoluteTimeGetCurrent(); + + TGDispatchOnMainThread(^ + { + if (self.captureStarted != nil) + self.captureStarted(resume); + + if (completion != nil) + completion(); + }); + }]; +} + +- (void)stopCaptureForPause:(bool)pause completion:(void (^)(void))completion +{ + if (_invalidated) + return; + + if (!pause) + _invalidated = true; + + TGLegacyLog(@"Camera: stop capture"); + + [[PGCamera cameraQueue] dispatch:^ + { + if (_invalidated) + { + [self.captureSession beginConfiguration]; + + [self.captureSession resetFlashMode]; + + TGLegacyLog(@"Camera: stop capture invalidated"); + TGCameraPreviewView *previewView = _previewView; + if (previewView != nil) + [previewView invalidate]; + + for (AVCaptureInput *input in self.captureSession.inputs) + [self.captureSession removeInput:input]; + for (AVCaptureOutput *output in self.captureSession.outputs) + [self.captureSession removeOutput:output]; + +#if !TARGET_IPHONE_SIMULATOR + [self.captureSession commitConfiguration]; +#endif + } + + TGLegacyLog(@"Camera: stop running"); +#if !TARGET_IPHONE_SIMULATOR + [self.captureSession stopRunning]; +#endif + + _capturing = false; + + TGDispatchOnMainThread(^ + { + if (_invalidated) + _previewView = nil; + + if (self.captureStopped != nil) + self.captureStopped(pause); + }); + + if (completion != nil) + completion(); + }]; +} + +- (bool)isResetNeeded +{ + return self.captureSession.isResetNeeded; +} + +- (void)resetSynchronous:(bool)synchronous completion:(void (^)(void))completion +{ + [self resetTerminal:false synchronous:synchronous completion:completion]; +} + +- (void)resetTerminal:(bool)__unused terminal synchronous:(bool)synchronous completion:(void (^)(void))completion +{ + void (^block)(void) = ^ + { + [self _unsubscribeFromCameraChanges]; + [self.captureSession reset]; + [self _subscribeForCameraChanges]; + + if (completion != nil) + completion(); + }; + + if (synchronous) + [[PGCamera cameraQueue] dispatchSync:block]; + else + [[PGCamera cameraQueue] dispatch:block]; +} + +#pragma mark - + +- (void)captureNextFrameCompletion:(void (^)(UIImage * image))completion +{ + [self.captureSession captureNextFrameCompletion:completion]; +} + +- (UIImage *)normalizeImageOrientation:(UIImage *)image +{ + if (image.imageOrientation == UIImageOrientationUp) { + return image; + } + + CGRect newRect = CGRectMake(0.0f, 0.0f, image.size.width, image.size.height); + + UIGraphicsBeginImageContextWithOptions(newRect.size, true, image.scale); + [image drawInRect:newRect]; + + UIImage *normalized = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + + return normalized; +} + +- (void)takePhotoWithCompletion:(void (^)(UIImage *result, PGCameraShotMetadata *metadata))completion +{ + bool videoMirrored = !self.disableResultMirroring ? _previewView.captureConnection.videoMirrored : false; + + [[PGCamera cameraQueue] dispatch:^ + { + if (!self.captureSession.isRunning || self.captureSession.imageOutput.isCapturingStillImage || _invalidated) + return; + + void (^takePhoto)(void) = ^ + { + AVCaptureConnection *imageConnection = [self.captureSession.imageOutput connectionWithMediaType:AVMediaTypeVideo]; + [imageConnection setVideoMirrored:videoMirrored]; + + UIInterfaceOrientation orientation = UIInterfaceOrientationPortrait; + if (self.requestedCurrentInterfaceOrientation != nil) + orientation = self.requestedCurrentInterfaceOrientation(NULL); + + [imageConnection setVideoOrientation:[PGCamera _videoOrientationForInterfaceOrientation:orientation mirrored:false]]; + + [self.captureSession.imageOutput captureStillImageAsynchronouslyFromConnection:self.captureSession.imageOutput.connections.firstObject completionHandler:^(CMSampleBufferRef imageDataSampleBuffer, NSError *error) + { + if (imageDataSampleBuffer != NULL && error == nil) + { + NSData *imageData = [AVCaptureStillImageOutput jpegStillImageNSDataRepresentation:imageDataSampleBuffer]; + UIImage *image = [[UIImage alloc] initWithData:imageData]; + + if (self.cameraMode == PGCameraModeSquare) + { + CGFloat shorterSide = MIN(image.size.width, image.size.height); + CGFloat longerSide = MAX(image.size.width, image.size.height); + + CGRect cropRect = CGRectMake(CGFloor((longerSide - shorterSide) / 2.0f), 0, shorterSide, shorterSide); + CGImageRef croppedCGImage = CGImageCreateWithImageInRect(image.CGImage, cropRect); + image = [UIImage imageWithCGImage:croppedCGImage scale:image.scale orientation:image.imageOrientation]; + CGImageRelease(croppedCGImage); + } + + PGCameraShotMetadata *metadata = [[PGCameraShotMetadata alloc] init]; + metadata.deviceAngle = [PGCameraShotMetadata relativeDeviceAngleFromAngle:_deviceAngleSampler.currentDeviceAngle orientation:orientation]; + + image = [self normalizeImageOrientation:image]; + + if (completion != nil) + completion(image, metadata); + } + }]; + }; + + NSTimeInterval delta = CFAbsoluteTimeGetCurrent() - _captureStartTime; + if (CFAbsoluteTimeGetCurrent() - _captureStartTime > 0.4) + takePhoto(); + else + TGDispatchAfter(0.4 - delta, [[PGCamera cameraQueue] _dispatch_queue], takePhoto); + }]; +} + +- (void)startVideoRecordingForMoment:(bool)moment completion:(void (^)(NSURL *, CGAffineTransform transform, CGSize dimensions, NSTimeInterval duration, bool success))completion +{ + [[PGCamera cameraQueue] dispatch:^ + { + if (!self.captureSession.isRunning || _invalidated) + return; + + void (^startRecording)(void) = ^ + { + UIInterfaceOrientation orientation = UIInterfaceOrientationPortrait; + bool mirrored = false; + + if (self.requestedCurrentInterfaceOrientation != nil) + orientation = self.requestedCurrentInterfaceOrientation(&mirrored); + + _moment = moment; + + [self.captureSession startVideoRecordingWithOrientation:[PGCamera _videoOrientationForInterfaceOrientation:orientation mirrored:mirrored] mirrored:mirrored completion:completion]; + + TGDispatchOnMainThread(^ + { + if (self.reallyBeganVideoRecording != nil) + self.reallyBeganVideoRecording(moment); + }); + }; + + NSTimeInterval delta = CFAbsoluteTimeGetCurrent() - _captureStartTime; + if (CFAbsoluteTimeGetCurrent() - _captureStartTime > 0.8) + startRecording(); + else + TGDispatchAfter(0.8 - delta, [[PGCamera cameraQueue] _dispatch_queue], startRecording); + + TGDispatchOnMainThread(^ + { + if (self.beganVideoRecording != nil) + self.beganVideoRecording(moment); + }); + }]; +} + +- (void)stopVideoRecording +{ + [[PGCamera cameraQueue] dispatch:^ + { + [self.captureSession stopVideoRecording]; + + TGDispatchOnMainThread(^ + { + if (self.finishedVideoRecording != nil) + self.finishedVideoRecording(_moment); + }); + }]; +} + +- (bool)isRecordingVideo +{ + return self.captureSession.movieWriter.isRecording; +} + +- (NSTimeInterval)videoRecordingDuration +{ + return self.captureSession.movieWriter.currentDuration; +} + +#pragma mark - Mode + +- (PGCameraMode)cameraMode +{ + return self.captureSession.currentMode; +} + +- (void)setCameraMode:(PGCameraMode)cameraMode +{ + if (self.disabled || self.captureSession.currentMode == cameraMode) + return; + + __weak PGCamera *weakSelf = self; + void(^commitBlock)(void) = ^ + { + __strong PGCamera *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [[PGCamera cameraQueue] dispatch:^ + { + strongSelf.captureSession.currentMode = cameraMode; + + if (strongSelf.finishedModeChange != nil) + strongSelf.finishedModeChange(); + + if (strongSelf.autoStartVideoRecording && strongSelf.onAutoStartVideoRecording != nil) + { + TGDispatchAfter(0.5, dispatch_get_main_queue(), ^ + { + strongSelf.onAutoStartVideoRecording(); + }); + } + + strongSelf.autoStartVideoRecording = false; + }]; + }; + + if (self.beganModeChange != nil) + self.beganModeChange(cameraMode, commitBlock); +} + +#pragma mark - Focus and Exposure + +- (void)subjectAreaChanged:(NSNotification *)__unused notification +{ + [self resetFocusPoint]; +} + +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)__unused object change:(NSDictionary *)__unused change context:(void *)__unused context +{ + TGDispatchOnMainThread(^ + { + if ([keyPath isEqualToString:PGCameraAdjustingFocusKey]) + { + bool adjustingFocus = [[change objectForKey:NSKeyValueChangeNewKey] isEqualToNumber:@YES]; + + if (adjustingFocus && self.beganAdjustingFocus != nil) + self.beganAdjustingFocus(); + else if (!adjustingFocus && self.finishedAdjustingFocus != nil) + self.finishedAdjustingFocus(); + } + else if ([keyPath isEqualToString:PGCameraFlashActiveKey] || [keyPath isEqualToString:PGCameraTorchActiveKey]) + { + bool active = [[change objectForKey:NSKeyValueChangeNewKey] isEqualToNumber:@YES]; + + if (self.flashActivityChanged != nil) + self.flashActivityChanged(active); + } + else if ([keyPath isEqualToString:PGCameraFlashAvailableKey] || [keyPath isEqualToString:PGCameraTorchAvailableKey]) + { + bool available = [[change objectForKey:NSKeyValueChangeNewKey] isEqualToNumber:@YES]; + + if (self.flashAvailabilityChanged != nil) + self.flashAvailabilityChanged(available); + } + }); +} + +- (bool)supportsExposurePOI +{ + return [self.captureSession.videoDevice isExposurePointOfInterestSupported]; +} + +- (bool)supportsFocusPOI +{ + return [self.captureSession.videoDevice isFocusPointOfInterestSupported]; +} + +- (void)resetFocusPoint +{ + const CGPoint centerPoint = CGPointMake(0.5f, 0.5f); + [self _setFocusPoint:centerPoint focusMode:AVCaptureFocusModeContinuousAutoFocus exposureMode:AVCaptureExposureModeContinuousAutoExposure monitorSubjectAreaChange:false]; +} + +- (void)setFocusPoint:(CGPoint)point +{ + [self _setFocusPoint:point focusMode:AVCaptureFocusModeAutoFocus exposureMode:AVCaptureExposureModeAutoExpose monitorSubjectAreaChange:true]; +} + +- (void)_setFocusPoint:(CGPoint)point focusMode:(AVCaptureFocusMode)focusMode exposureMode:(AVCaptureExposureMode)exposureMode monitorSubjectAreaChange:(bool)monitorSubjectAreaChange +{ + [[PGCamera cameraQueue] dispatch:^ + { + if (self.disabled) + return; + + [self.captureSession setFocusPoint:point focusMode:focusMode exposureMode:exposureMode monitorSubjectAreaChange:monitorSubjectAreaChange]; + }]; +} + +- (bool)supportsExposureTargetBias +{ + return [self.captureSession.videoDevice respondsToSelector:@selector(setExposureTargetBias:completionHandler:)]; +} + +- (void)beginExposureTargetBiasChange +{ + [[PGCamera cameraQueue] dispatch:^ + { + if (self.disabled) + return; + + [self.captureSession setFocusPoint:self.captureSession.focusPoint focusMode:AVCaptureFocusModeLocked exposureMode:AVCaptureExposureModeLocked monitorSubjectAreaChange:false]; + }]; +} + +- (void)setExposureTargetBias:(CGFloat)bias +{ + [[PGCamera cameraQueue] dispatch:^ + { + if (self.disabled) + return; + + [self.captureSession setExposureTargetBias:bias]; + }]; +} + +- (void)endExposureTargetBiasChange +{ + [[PGCamera cameraQueue] dispatch:^ + { + if (self.disabled) + return; + + [self.captureSession setFocusPoint:self.captureSession.focusPoint focusMode:AVCaptureFocusModeAutoFocus exposureMode:AVCaptureExposureModeAutoExpose monitorSubjectAreaChange:true]; + }]; +} + +#pragma mark - Flash + +- (bool)hasFlash +{ + return self.captureSession.videoDevice.hasFlash; +} + +- (bool)flashActive +{ + if (self.cameraMode == PGCameraModeVideo || self.cameraMode == PGCameraModeClip) + return self.captureSession.videoDevice.torchActive; + + return self.captureSession.videoDevice.flashActive; +} + +- (bool)flashAvailable +{ + if (self.cameraMode == PGCameraModeVideo || self.cameraMode == PGCameraModeClip) + return self.captureSession.videoDevice.torchAvailable; + + return self.captureSession.videoDevice.flashAvailable; +} + +- (PGCameraFlashMode)flashMode +{ + return self.captureSession.currentFlashMode; +} + +- (void)setFlashMode:(PGCameraFlashMode)flashMode +{ + [[PGCamera cameraQueue] dispatch:^ + { + self.captureSession.currentFlashMode = flashMode; + }]; +} + +#pragma mark - Position + +- (PGCameraPosition)togglePosition +{ + if ([AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo].count < 2 || self.disabled) + return self.captureSession.currentCameraPosition; + + [self _unsubscribeFromCameraChanges]; + + PGCameraPosition targetCameraPosition = PGCameraPositionFront; + if (self.captureSession.currentCameraPosition == PGCameraPositionFront) + targetCameraPosition = PGCameraPositionRear; + + AVCaptureDevice *targetDevice = [PGCameraCaptureSession _deviceWithCameraPosition:targetCameraPosition]; + + __weak PGCamera *weakSelf = self; + void(^commitBlock)(void) = ^ + { + __strong PGCamera *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [[PGCamera cameraQueue] dispatch:^ + { + [strongSelf.captureSession setCurrentCameraPosition:targetCameraPosition]; + + if (strongSelf.finishedPositionChange != nil) + strongSelf.finishedPositionChange(); + + [strongSelf setZoomLevel:0.0f]; + [strongSelf _subscribeForCameraChanges]; + }]; + }; + + if (self.beganPositionChange != nil) + self.beganPositionChange(targetDevice.hasFlash, [PGCameraCaptureSession _isZoomAvailableForDevice:targetDevice], commitBlock); + + return targetCameraPosition; +} + +#pragma mark - Zoom + +- (bool)isZoomAvailable +{ + return self.captureSession.isZoomAvailable; +} + +- (CGFloat)zoomLevel +{ + return self.captureSession.zoomLevel; +} + +- (void)setZoomLevel:(CGFloat)zoomLevel +{ + zoomLevel = MAX(0.0f, MIN(1.0f, zoomLevel)); + + [[PGCamera cameraQueue] dispatch:^ + { + if (self.disabled) + return; + + [self.captureSession setZoomLevel:zoomLevel]; + }]; +} + +#pragma mark - Device Angle + +- (void)startDeviceAngleMeasuring +{ + [_deviceAngleSampler startMeasuring]; +} + +- (void)stopDeviceAngleMeasuring +{ + [_deviceAngleSampler stopMeasuring]; +} + +#pragma mark - Availability + ++ (bool)cameraAvailable +{ +#if TARGET_IPHONE_SIMULATOR + return false; +#endif + + return [UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]; +} + ++ (bool)hasRearCamera +{ + return ([PGCameraCaptureSession _deviceWithCameraPosition:PGCameraPositionRear] != nil); +} + ++ (bool)hasFrontCamera +{ + return ([PGCameraCaptureSession _deviceWithCameraPosition:PGCameraPositionFront] != nil); +} + ++ (SQueue *)cameraQueue +{ + static dispatch_once_t onceToken; + static SQueue *queue = nil; + dispatch_once(&onceToken, ^ + { + queue = [[SQueue alloc] init]; + }); + + return queue; +} + ++ (AVCaptureVideoOrientation)_videoOrientationForInterfaceOrientation:(UIInterfaceOrientation)deviceOrientation mirrored:(bool)mirrored +{ + switch (deviceOrientation) + { + case UIInterfaceOrientationPortraitUpsideDown: + return AVCaptureVideoOrientationPortraitUpsideDown; + + case UIInterfaceOrientationLandscapeLeft: + return mirrored ? AVCaptureVideoOrientationLandscapeRight : AVCaptureVideoOrientationLandscapeLeft; + + case UIInterfaceOrientationLandscapeRight: + return mirrored ? AVCaptureVideoOrientationLandscapeLeft : AVCaptureVideoOrientationLandscapeRight; + + default: + return AVCaptureVideoOrientationPortrait; + } +} + ++ (PGCameraAuthorizationStatus)cameraAuthorizationStatus +{ + if ([AVCaptureDevice respondsToSelector:@selector(authorizationStatusForMediaType:)]) + return [PGCamera _cameraAuthorizationStatusForAuthorizationStatus:[AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]]; + + return PGCameraAuthorizationStatusAuthorized; +} + ++ (PGMicrophoneAuthorizationStatus)microphoneAuthorizationStatus +{ + if ([AVCaptureDevice respondsToSelector:@selector(authorizationStatusForMediaType:)]) + return [PGCamera _microphoneAuthorizationStatusForAuthorizationStatus:[AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeAudio]]; + + return PGMicrophoneAuthorizationStatusAuthorized; +} + ++ (PGCameraAuthorizationStatus)_cameraAuthorizationStatusForAuthorizationStatus:(AVAuthorizationStatus)authorizationStatus +{ + switch (authorizationStatus) + { + case AVAuthorizationStatusRestricted: + return PGCameraAuthorizationStatusRestricted; + + case AVAuthorizationStatusDenied: + return PGCameraAuthorizationStatusDenied; + + case AVAuthorizationStatusAuthorized: + return PGCameraAuthorizationStatusAuthorized; + + default: + return PGCameraAuthorizationStatusNotDetermined; + } +} + ++ (PGMicrophoneAuthorizationStatus)_microphoneAuthorizationStatusForAuthorizationStatus:(AVAuthorizationStatus)authorizationStatus +{ + switch (authorizationStatus) + { + case AVAuthorizationStatusRestricted: + return PGMicrophoneAuthorizationStatusRestricted; + + case AVAuthorizationStatusDenied: + return PGMicrophoneAuthorizationStatusDenied; + + case AVAuthorizationStatusAuthorized: + return PGMicrophoneAuthorizationStatusAuthorized; + + default: + return PGMicrophoneAuthorizationStatusNotDetermined; + } +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGCameraCaptureSession.h b/submodules/LegacyComponents/LegacyComponents/PGCameraCaptureSession.h new file mode 100644 index 0000000000..92c90b3592 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGCameraCaptureSession.h @@ -0,0 +1,57 @@ +#import + +#import + +@class PGCameraMovieWriter; + +@interface PGCameraCaptureSession : AVCaptureSession + +@property (nonatomic, readonly) AVCaptureDevice *videoDevice; +@property (nonatomic, readonly) AVCaptureStillImageOutput *imageOutput; +@property (nonatomic, readonly) AVCaptureVideoDataOutput *videoOutput; +@property (nonatomic, readonly) AVCaptureAudioDataOutput *audioOutput; +@property (nonatomic, readonly) AVCaptureMetadataOutput *metadataOutput; +@property (nonatomic, readonly) PGCameraMovieWriter *movieWriter; + +@property (nonatomic, assign) bool alwaysSetFlash; +@property (nonatomic, assign) PGCameraMode currentMode; +@property (nonatomic, assign) PGCameraFlashMode currentFlashMode; + +@property (nonatomic, assign) PGCameraPosition currentCameraPosition; +@property (nonatomic, readonly) PGCameraPosition preferredCameraPosition; + +@property (nonatomic, readonly) bool isZoomAvailable; +@property (nonatomic, assign) CGFloat zoomLevel; + +@property (nonatomic, readonly) CGPoint focusPoint; + +@property (nonatomic, copy) void(^outputSampleBuffer)(CMSampleBufferRef sampleBuffer, AVCaptureConnection *connection); + +@property (nonatomic, copy) void(^changingPosition)(void); +@property (nonatomic, copy) bool(^requestPreviewIsMirrored)(void); + +@property (nonatomic, copy) void(^recognizedQRCode)(NSString *value, AVMetadataMachineReadableCodeObject *object); + +@property (nonatomic, assign) bool compressVideo; + +- (instancetype)initWithMode:(PGCameraMode)mode position:(PGCameraPosition)position; + +- (void)performInitialConfigurationWithCompletion:(void (^)(void))completion; + +- (void)setFocusPoint:(CGPoint)point focusMode:(AVCaptureFocusMode)focusMode exposureMode:(AVCaptureExposureMode)exposureMode monitorSubjectAreaChange:(bool)monitorSubjectAreaChange; +- (void)setExposureTargetBias:(CGFloat)bias; + +- (bool)isResetNeeded; +- (void)reset; +- (void)resetFlashMode; + +- (void)startVideoRecordingWithOrientation:(AVCaptureVideoOrientation)orientation mirrored:(bool)mirrored completion:(void (^)(NSURL *outputURL, CGAffineTransform transform, CGSize dimensions, NSTimeInterval duration, bool success))completion; +- (void)stopVideoRecording; + +- (void)captureNextFrameCompletion:(void (^)(UIImage * image))completion; + ++ (AVCaptureDevice *)_deviceWithCameraPosition:(PGCameraPosition)position; + ++ (bool)_isZoomAvailableForDevice:(AVCaptureDevice *)device; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGCameraCaptureSession.m b/submodules/LegacyComponents/LegacyComponents/PGCameraCaptureSession.m new file mode 100644 index 0000000000..7bcc710026 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGCameraCaptureSession.m @@ -0,0 +1,971 @@ +#import "PGCameraCaptureSession.h" +#import "PGCameraMovieWriter.h" + +#import +#import +#import + +#import "LegacyComponentsInternal.h" + +#import + +#import + +#import +#import + +const NSInteger PGCameraFrameRate = 30; + +@interface PGCameraCaptureSession () +{ + PGCameraMode _currentMode; + + PGCameraPosition _preferredCameraPosition; + + PGCameraFlashMode _photoFlashMode; + PGCameraFlashMode _videoFlashMode; + + AVCaptureDeviceInput *_videoInput; + AVCaptureDeviceInput *_audioInput; + + AVCaptureDevice *_audioDevice; + + dispatch_queue_t _videoQueue; + dispatch_queue_t _audioQueue; + dispatch_queue_t _metadataQueue; + SQueue *_audioSessionQueue; + + bool _captureNextFrame; + bool _capturingForVideoThumbnail; + + NSInteger _frameRate; + + bool _initialized; + + AVCaptureVideoOrientation _captureVideoOrientation; + bool _captureMirrored; + + SMetaDisposable *_currentAudioSession; + bool _hasAudioSession; +} + +@property (nonatomic, copy) void(^capturedFrameCompletion)(UIImage *image); + +@end + +@implementation PGCameraCaptureSession + +- (instancetype)initWithMode:(PGCameraMode)mode position:(PGCameraPosition)position +{ + self = [super init]; + if (self != nil) + { + _currentMode = mode; + _photoFlashMode = PGCameraFlashModeOff; + _videoFlashMode = PGCameraFlashModeOff; + + _videoQueue = dispatch_queue_create(NULL, DISPATCH_QUEUE_SERIAL); + _audioQueue = dispatch_queue_create(NULL, DISPATCH_QUEUE_SERIAL); + _metadataQueue = dispatch_queue_create(NULL, DISPATCH_QUEUE_SERIAL); + _audioSessionQueue = [[SQueue alloc] init]; + + _preferredCameraPosition = position; + + _currentAudioSession = [[SMetaDisposable alloc] init]; + } + return self; +} + +- (void)dealloc +{ + [self endAudioSession]; + [_videoOutput setSampleBufferDelegate:nil queue:NULL]; + [_audioOutput setSampleBufferDelegate:nil queue:NULL]; + [_metadataOutput setMetadataObjectsDelegate:nil queue:NULL]; +} + +- (void)performInitialConfigurationWithCompletion:(void (^)(void))completion +{ + _initialized = true; + + AVCaptureDevice *targetDevice = [PGCameraCaptureSession _deviceWithCameraPosition:_preferredCameraPosition]; + if (targetDevice == nil) + targetDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo]; + _videoDevice = targetDevice; + + NSError *error = nil; + if (_videoDevice != nil) + { + _preferredCameraPosition = [PGCameraCaptureSession _cameraPositionForDevicePosition:_videoDevice.position]; + + _videoInput = [AVCaptureDeviceInput deviceInputWithDevice:_videoDevice error:&error]; + if (_videoInput != nil && [self canAddInput:_videoInput]) + [self addInput:_videoInput]; + else + TGLegacyLog(@"ERROR: camera can't add video input"); + } + else + { + _videoInput = nil; + TGLegacyLog(@"ERROR: camera can't create video device"); + } + + if (_currentMode == PGCameraModePhoto || _currentMode == PGCameraModeSquare) + { +#if !TARGET_IPHONE_SIMULATOR + self.sessionPreset = AVCaptureSessionPresetPhoto; +#endif + } + else + { + [self switchToBestVideoFormatForDevice:_videoDevice]; + [self _addAudioInputRequestAudioSession:true]; + [self setFrameRate:PGCameraFrameRate forDevice:_videoDevice]; + } + + AVCaptureStillImageOutput *imageOutput = [[AVCaptureStillImageOutput alloc] init]; + [imageOutput setOutputSettings:@{AVVideoCodecKey : AVVideoCodecJPEG}]; + if ([self canAddOutput:imageOutput]) + { +#if !TARGET_IPHONE_SIMULATOR + [self addOutput:imageOutput]; +#endif + _imageOutput = imageOutput; + } + else + { + _imageOutput = nil; + TGLegacyLog(@"ERROR: camera can't add still image output"); + } + + AVCaptureVideoDataOutput *videoOutput = [[AVCaptureVideoDataOutput alloc] init]; + videoOutput.alwaysDiscardsLateVideoFrames = true; + videoOutput.videoSettings = @{ (id)kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange) }; + if ([self canAddOutput:videoOutput]) + { + [videoOutput setSampleBufferDelegate:self queue:_videoQueue]; +#if !TARGET_IPHONE_SIMULATOR + [self addOutput:videoOutput]; +#endif + _videoOutput = videoOutput; + } + else + { + _videoOutput = nil; + TGLegacyLog(@"ERROR: camera can't add video output"); + } + + AVCaptureMetadataOutput *metadataOutput = [[AVCaptureMetadataOutput alloc] init]; + if (_videoDevice.position == AVCaptureDevicePositionBack && [self canAddOutput:metadataOutput]) + { +#if !TARGET_IPHONE_SIMULATOR + [self addOutput:metadataOutput]; +#endif + _metadataOutput = metadataOutput; + + if ([metadataOutput.availableMetadataObjectTypes containsObject:AVMetadataObjectTypeQRCode]) { + [metadataOutput setMetadataObjectsDelegate:self queue:_metadataQueue]; + metadataOutput.metadataObjectTypes = @[AVMetadataObjectTypeQRCode]; + } + } + else + { + _metadataOutput = nil; + TGLegacyLog(@"ERROR: camera can't add metadata output"); + } + + self.currentFlashMode = PGCameraFlashModeOff; + + [self _enableLowLightBoost]; + [self _enableVideoStabilization]; + + if (completion != nil) + completion(); +} + +- (bool)isResetNeeded +{ + if (self.currentCameraPosition != _preferredCameraPosition) + return true; + + if (self.currentMode == PGCameraModeVideo || self.currentMode == PGCameraModeClip) + return true; + + if (self.zoomLevel > FLT_EPSILON) + return true; + + return false; +} + +- (void)reset +{ + [self beginConfiguration]; + + [self _removeAudioInputEndAudioSession:true]; + + if (self.currentCameraPosition != _preferredCameraPosition) + { + [self removeInput:_videoInput]; + + AVCaptureDevice *targetDevice = [PGCameraCaptureSession _deviceWithCameraPosition:_preferredCameraPosition]; + if (targetDevice == nil) + targetDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo]; + + _videoDevice = targetDevice; + if (_videoDevice != nil) + { + _videoInput = [AVCaptureDeviceInput deviceInputWithDevice:_videoDevice error:nil]; + if (_videoInput != nil) + [self addInput:_videoInput]; + } + } + + [self resetFlashMode]; + + if (self.currentMode != PGCameraModePhoto) + { + if (self.currentMode == PGCameraModeVideo || self.currentMode == PGCameraModeClip) + self.sessionPreset = AVCaptureSessionPresetPhoto; + + _currentMode = PGCameraModePhoto; + } + + [self commitConfiguration]; + + [self resetFocusPoint]; + + self.zoomLevel = 0.0f; +} + +- (void)resetFlashMode +{ + _photoFlashMode = PGCameraFlashModeOff; + _videoFlashMode = PGCameraFlashModeOff; + self.currentFlashMode = PGCameraFlashModeOff; +} + +- (PGCameraMode)currentMode +{ + return _currentMode; +} + +- (void)setCurrentMode:(PGCameraMode)mode +{ + _currentMode = mode; + + [self beginConfiguration]; + + [self resetFocusPoint]; + + switch (mode) + { + case PGCameraModePhoto: + case PGCameraModeSquare: + { + [self _removeAudioInputEndAudioSession:true]; + self.sessionPreset = AVCaptureSessionPresetPhoto; + [self setFrameRate:0 forDevice:_videoDevice]; + } + break; + + case PGCameraModeVideo: + case PGCameraModeClip: + { + self.sessionPreset = AVCaptureSessionPresetInputPriority; + [self switchToBestVideoFormatForDevice:_videoDevice]; + [self _addAudioInputRequestAudioSession:true]; + [self setFrameRate:PGCameraFrameRate forDevice:_videoDevice]; + } + break; + + default: + break; + } + + [self _enableLowLightBoost]; + [self _enableVideoStabilization]; + + [self commitConfiguration]; +} + +- (void)switchToBestVideoFormatForDevice:(AVCaptureDevice *)device +{ + [self _reconfigureDevice:device withBlock:^(AVCaptureDevice *device) + { + NSArray *availableFormats = device.formats; + AVCaptureDeviceFormat *preferredFormat = nil; + NSMutableArray *maybeFormats = nil; + int32_t maxWidth = 0; + int32_t maxHeight = 0; + for (AVCaptureDeviceFormat *format in availableFormats) + { + if (![format.mediaType isEqualToString:@"vide"] || [[format valueForKey:@"isPhotoFormat"] boolValue]) + continue; + + CMVideoDimensions dimensions = CMVideoFormatDescriptionGetDimensions(format.formatDescription); + if (dimensions.width >= maxWidth && dimensions.width <= 1920 && dimensions.height >= maxHeight && dimensions.height <= 1080) + { + if (dimensions.width > maxWidth) + maybeFormats = [[NSMutableArray alloc] init]; + FourCharCode mediaSubType = CMFormatDescriptionGetMediaSubType(format.formatDescription); + if (mediaSubType == kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange) + { + maxWidth = dimensions.width; + + NSArray *rateRanges = format.videoSupportedFrameRateRanges; + bool supportedRate = true; + for (AVFrameRateRange *range in rateRanges) + { + if (range.maxFrameRate > 60) + { + supportedRate = false; + break; + } + } + + if (supportedRate) + [maybeFormats addObject:format]; + } + } + } + + preferredFormat = maybeFormats.lastObject; + + [device setActiveFormat:preferredFormat]; + }]; +} + +- (void)requestAudioSession +{ + if (_hasAudioSession) + return; + + _hasAudioSession = true; + [_audioSessionQueue dispatchSync:^ + { + [_currentAudioSession setDisposable:[[LegacyComponentsGlobals provider] requestAudioSession:TGAudioSessionTypePlayAndRecord interrupted:nil]]; + }]; +} + +- (void)endAudioSession +{ + _hasAudioSession = false; + SMetaDisposable *currentAudioSession = _currentAudioSession; + [_audioSessionQueue dispatch:^ + { + [currentAudioSession setDisposable:nil]; + }]; +} + +- (void)_enableLowLightBoost +{ + [self _reconfigureDevice:_videoDevice withBlock:^(AVCaptureDevice *device) + { + if (device.isLowLightBoostSupported) + device.automaticallyEnablesLowLightBoostWhenAvailable = true; + }]; +} + +- (void)_enableVideoStabilization +{ + AVCaptureConnection *videoConnection = [_videoOutput connectionWithMediaType:AVMediaTypeVideo]; + if (videoConnection.supportsVideoStabilization) + { + if ([videoConnection respondsToSelector:@selector(setPreferredVideoStabilizationMode:)]) + videoConnection.preferredVideoStabilizationMode = AVCaptureVideoStabilizationModeStandard; + else + videoConnection.enablesVideoStabilizationWhenAvailable = true; + } +} + +- (void)_addAudioInputRequestAudioSession:(bool)requestAudioSession +{ + if (_audioDevice != nil) + return; + + AVCaptureDevice *audioDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio]; + + NSError *error = nil; + if (audioDevice != nil) + { + _audioDevice = audioDevice; + AVCaptureDeviceInput *audioInput = [AVCaptureDeviceInput deviceInputWithDevice:_audioDevice error:&error]; + if ([self canAddInput:audioInput]) + { + [self addInput:audioInput]; + _audioInput = audioInput; + } + } + + AVCaptureAudioDataOutput *audioOutput = [[AVCaptureAudioDataOutput alloc] init]; + if ([self canAddOutput:audioOutput]) + { + [audioOutput setSampleBufferDelegate:self queue:_audioQueue]; + [self addOutput:audioOutput]; + _audioOutput = audioOutput; + } + + if (requestAudioSession) + [self requestAudioSession]; +} + +- (void)_removeAudioInputEndAudioSession:(bool)endAudioSession +{ + if (_audioDevice == nil) + return; + + [self removeInput:_audioInput]; + _audioInput = nil; + + [_audioOutput setSampleBufferDelegate:nil queue:[SQueue mainQueue]._dispatch_queue]; + [self removeOutput:_audioOutput]; + _audioOutput = nil; + + _audioDevice = nil; + + if (endAudioSession) + [self endAudioSession]; +} + +#pragma mark - Zoom + +- (CGFloat)_maximumZoomFactor +{ + return MIN(5.0f, self.videoDevice.activeFormat.videoMaxZoomFactor); +} + +- (CGFloat)zoomLevel +{ + if (![self.videoDevice respondsToSelector:@selector(videoZoomFactor)]) + return 1.0f; + + return (self.videoDevice.videoZoomFactor - 1.0f) / ([self _maximumZoomFactor] - 1.0f); +} + +- (void)setZoomLevel:(CGFloat)zoomLevel +{ + if (![self.videoDevice respondsToSelector:@selector(setVideoZoomFactor:)]) + return; + + __weak PGCameraCaptureSession *weakSelf = self; + [self _reconfigureDevice:self.videoDevice withBlock:^(AVCaptureDevice *device) + { + __strong PGCameraCaptureSession *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + device.videoZoomFactor = MAX(1.0f, MIN([strongSelf _maximumZoomFactor], 1.0f + ([strongSelf _maximumZoomFactor] - 1.0f) * zoomLevel)); + }]; +} + +- (bool)isZoomAvailable +{ + return [PGCameraCaptureSession _isZoomAvailableForDevice:self.videoDevice]; +} + ++ (bool)_isZoomAvailableForDevice:(AVCaptureDevice *)device +{ + if (![device respondsToSelector:@selector(setVideoZoomFactor:)]) + return false; + + if (device.position == AVCaptureDevicePositionFront) + return false; + + return true; +} + +#pragma mark - Focus and Exposure + +- (void)resetFocusPoint +{ + const CGPoint centerPoint = CGPointMake(0.5f, 0.5f); + [self setFocusPoint:centerPoint focusMode:AVCaptureFocusModeContinuousAutoFocus exposureMode:AVCaptureExposureModeContinuousAutoExposure monitorSubjectAreaChange:false]; +} + +- (void)setFocusPoint:(CGPoint)point focusMode:(AVCaptureFocusMode)focusMode exposureMode:(AVCaptureExposureMode)exposureMode monitorSubjectAreaChange:(bool)monitorSubjectAreaChange +{ + [self _reconfigureDevice:self.videoDevice withBlock:^(AVCaptureDevice *device) + { + _focusPoint = point; + + if ([device isExposurePointOfInterestSupported] && [device isExposureModeSupported:exposureMode]) + { + [device setExposurePointOfInterest:point]; + [device setExposureMode:exposureMode]; + } + if ([device isFocusPointOfInterestSupported] && [device isFocusModeSupported:focusMode]) + { + [device setFocusPointOfInterest:point]; + [device setFocusMode:focusMode]; + } + + [device setSubjectAreaChangeMonitoringEnabled:monitorSubjectAreaChange]; + + if ([device respondsToSelector:@selector(exposureTargetBias)]) + { + if (fabsf(device.exposureTargetBias) > FLT_EPSILON) + [device setExposureTargetBias:0.0f completionHandler:nil]; + } + }]; +} + +- (void)setExposureTargetBias:(CGFloat)bias +{ + [self _reconfigureDevice:self.videoDevice withBlock:^(AVCaptureDevice *device) + { + CGFloat value = 0.0f; + CGFloat extremum = (bias >= 0) ? device.maxExposureTargetBias : device.minExposureTargetBias; + value = fabs(bias) * extremum * 0.85f; + + [device setExposureTargetBias:(float)value completionHandler:nil]; + }]; +} + +#pragma mark - Flash + +- (PGCameraFlashMode)currentFlashMode +{ + switch (self.currentMode) + { + case PGCameraModeVideo: + case PGCameraModeClip: + return _videoFlashMode; + + default: + return _photoFlashMode; + } +} + +- (void)setCurrentFlashMode:(PGCameraFlashMode)mode +{ + [self _reconfigureDevice:self.videoDevice withBlock:^(AVCaptureDevice *device) + { + switch (self.currentMode) + { + case PGCameraModeVideo: + case PGCameraModeClip: + { + AVCaptureTorchMode torchMode = [PGCameraCaptureSession _deviceTorchModeForCameraFlashMode:mode]; + if (device.hasTorch && [device isTorchModeSupported:torchMode]) + { + _videoFlashMode = mode; + if (mode != PGCameraFlashModeAuto) + { + device.torchMode = torchMode; + } + else + { + device.torchMode = AVCaptureTorchModeOff; + + AVCaptureFlashMode flashMode = [PGCameraCaptureSession _deviceFlashModeForCameraFlashMode:mode]; + if (device.hasFlash && [device isFlashModeSupported:flashMode]) + device.flashMode = flashMode; + } + } + else if (mode == PGCameraFlashModeAuto && self.alwaysSetFlash) + { + AVCaptureFlashMode flashMode = [PGCameraCaptureSession _deviceFlashModeForCameraFlashMode:mode]; + if (device.hasFlash && [device isFlashModeSupported:flashMode]) + device.flashMode = flashMode; + } + } + break; + + default: + { + AVCaptureFlashMode flashMode = [PGCameraCaptureSession _deviceFlashModeForCameraFlashMode:mode]; + if (device.hasFlash && [device isFlashModeSupported:flashMode]) + { + _photoFlashMode = mode; + device.flashMode = flashMode; + } + } + break; + } + }]; +} + ++ (AVCaptureFlashMode)_deviceFlashModeForCameraFlashMode:(PGCameraFlashMode)mode +{ + switch (mode) + { + case PGCameraFlashModeAuto: + return AVCaptureFlashModeAuto; + + case PGCameraFlashModeOn: + return AVCaptureFlashModeOn; + + default: + return AVCaptureFlashModeOff; + } +} + ++ (AVCaptureTorchMode)_deviceTorchModeForCameraFlashMode:(PGCameraFlashMode)mode +{ + switch (mode) + { + case PGCameraFlashModeAuto: + return AVCaptureTorchModeAuto; + + case PGCameraFlashModeOn: + return AVCaptureTorchModeOn; + + default: + return AVCaptureTorchModeOff; + } +} + +#pragma mark - Position + +- (PGCameraPosition)currentCameraPosition +{ + if (_videoDevice != nil) + return [PGCameraCaptureSession _cameraPositionForDevicePosition:_videoDevice.position]; + + return PGCameraPositionUndefined; +} + +- (void)setCurrentCameraPosition:(PGCameraPosition)position +{ + NSError *error; + + AVCaptureDevice *deviceForTargetPosition = [PGCameraCaptureSession _deviceWithCameraPosition:position]; + AVCaptureDeviceInput *newVideoInput = [[AVCaptureDeviceInput alloc] initWithDevice:deviceForTargetPosition error:&error]; + + if (newVideoInput != nil) + { + [self resetFocusPoint]; + + [self beginConfiguration]; + + [self removeInput:_videoInput]; + if ([self canAddInput:newVideoInput]) + { + [self addInput:newVideoInput]; + _videoInput = newVideoInput; + } + else + { + [self addInput:_videoInput]; + } + + if (self.currentMode == PGCameraModeVideo) { + [self switchToBestVideoFormatForDevice:deviceForTargetPosition]; + [self _removeAudioInputEndAudioSession:false]; + [self _addAudioInputRequestAudioSession:false]; + } + + if (self.changingPosition != nil) + self.changingPosition(); + + [self commitConfiguration]; + + if (self.currentMode == PGCameraModeVideo || self.currentMode == PGCameraModeClip) + [self setFrameRate:PGCameraFrameRate forDevice:deviceForTargetPosition]; + else + [self setFrameRate:0 forDevice:deviceForTargetPosition]; + } + + _videoDevice = deviceForTargetPosition; + + [self setCurrentFlashMode:self.currentFlashMode]; + + [self _enableLowLightBoost]; + [self _enableVideoStabilization]; +} + ++ (AVCaptureDevice *)_deviceWithCameraPosition:(PGCameraPosition)position +{ + return [self _deviceWithPosition:[self _devicePositionForCameraPosition:position]]; +} + ++ (AVCaptureDevice *)_deviceWithPosition:(AVCaptureDevicePosition)position +{ + NSArray *devices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo]; + + for (AVCaptureDevice *device in devices) + { + if (device.position == position) + return device; + } + + return nil; +} + ++ (PGCameraPosition)_cameraPositionForDevicePosition:(AVCaptureDevicePosition)position +{ + switch (position) + { + case AVCaptureDevicePositionBack: + return PGCameraPositionRear; + + case AVCaptureDevicePositionFront: + return PGCameraPositionFront; + + default: + return PGCameraPositionUndefined; + } +} + ++ (AVCaptureDevicePosition)_devicePositionForCameraPosition:(PGCameraPosition)position +{ + switch (position) + { + case PGCameraPositionRear: + return AVCaptureDevicePositionBack; + + case PGCameraPositionFront: + return AVCaptureDevicePositionFront; + + default: + return AVCaptureDevicePositionUnspecified; + } +} + +#pragma mark - Configuration + +- (void)_reconfigureDevice:(AVCaptureDevice *)device withBlock:(void (^)(AVCaptureDevice *device))block +{ + if (block == nil) + return; + + NSError *error = nil; + [device lockForConfiguration:&error]; + block(device); + [device unlockForConfiguration]; + + if (error != nil) + TGLegacyLog(@"ERROR: failed to reconfigure camera: %@", error); +} + +- (void)setFrameRate:(NSInteger)frameRate forDevice:(AVCaptureDevice *)videoDevice +{ + _frameRate = frameRate; + + if ([videoDevice respondsToSelector:@selector(setActiveVideoMinFrameDuration:)] && + [videoDevice respondsToSelector:@selector(setActiveVideoMaxFrameDuration:)]) + { + if (_frameRate > 0) + { + NSInteger maxFrameRate = PGCameraFrameRate; + if (videoDevice.activeFormat.videoSupportedFrameRateRanges.count > 0) + { + AVFrameRateRange *range = self.videoDevice.activeFormat.videoSupportedFrameRateRanges.firstObject; + if (range.maxFrameRate < maxFrameRate) + maxFrameRate = (NSInteger)range.maxFrameRate; + } + + [self _reconfigureDevice:videoDevice withBlock:^(AVCaptureDevice *device) + { + [device setActiveVideoMinFrameDuration:CMTimeMake(1, (int32_t)maxFrameRate)]; + [device setActiveVideoMaxFrameDuration:CMTimeMake(1, (int32_t)maxFrameRate)]; + }]; + } + else + { + [self _reconfigureDevice:videoDevice withBlock:^(AVCaptureDevice *device) + { + [device setActiveVideoMinFrameDuration:kCMTimeInvalid]; + [device setActiveVideoMaxFrameDuration:kCMTimeInvalid]; + }]; + } + } +} + +- (NSInteger)frameRate +{ + return _frameRate; +} + +#pragma mark - + +- (void)startVideoRecordingWithOrientation:(AVCaptureVideoOrientation)orientation mirrored:(bool)mirrored completion:(void (^)(NSURL *outputURL, CGAffineTransform transform, CGSize dimensions, NSTimeInterval duration, bool success))completion +{ + if (_movieWriter.isRecording) + return; + + if (_videoFlashMode == PGCameraFlashModeAuto) + { + [self _reconfigureDevice:self.videoDevice withBlock:^(AVCaptureDevice *device) + { + AVCaptureTorchMode torchMode = [PGCameraCaptureSession _deviceTorchModeForCameraFlashMode:PGCameraFlashModeAuto]; + if (device.hasTorch && [device isTorchModeSupported:torchMode]) + device.torchMode = torchMode; + }]; + } + + _captureVideoOrientation = orientation; + _captureMirrored = mirrored; + + NSDictionary *videoSettings = [_videoOutput recommendedVideoSettingsForAssetWriterWithOutputFileType:AVFileTypeMPEG4]; + NSDictionary *audioSettings = [_audioOutput recommendedAudioSettingsForAssetWriterWithOutputFileType:AVFileTypeMPEG4]; + + if (self.compressVideo) + { + videoSettings = [TGMediaVideoConversionPresetSettings videoSettingsForPreset:TGMediaVideoConversionPresetCompressedMedium dimensions:CGSizeMake(848, 480)]; + audioSettings = [TGMediaVideoConversionPresetSettings audioSettingsForPreset:TGMediaVideoConversionPresetCompressedMedium]; + } + + _movieWriter = [[PGCameraMovieWriter alloc] initWithVideoTransform:TGTransformForVideoOrientation(orientation, mirrored) videoOutputSettings:videoSettings audioOutputSettings:audioSettings]; + _movieWriter.finishedWithMovieAtURL = completion; + [_movieWriter startRecording]; +} + +- (void)stopVideoRecording +{ + if (!_movieWriter.isRecording) + return; + + __weak PGCameraCaptureSession *weakSelf = self; + [_movieWriter stopRecordingWithCompletion:^ + { + __strong PGCameraCaptureSession *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + strongSelf->_movieWriter = nil; + + strongSelf->_videoFlashMode = PGCameraFlashModeOff; + }]; +} + +- (void)captureNextFrameCompletion:(void (^)(UIImage * image))completion +{ + dispatch_async(_videoQueue, ^ + { + _captureNextFrame = true; + self.capturedFrameCompletion = completion; + }); +} + +#define clamp(a) (uint8_t)(a > 255 ? 255 : (a < 0 ? 0 : a)) + +- (UIImage *)imageFromSampleBuffer:(CMSampleBufferRef)sampleBuffer orientation:(UIImageOrientation)orientation +{ + CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer); + CVPixelBufferLockBaseAddress(imageBuffer,0); + + size_t width = CVPixelBufferGetWidth(imageBuffer); + size_t height = CVPixelBufferGetHeight(imageBuffer); + uint8_t *yBuffer = CVPixelBufferGetBaseAddressOfPlane(imageBuffer, 0); + size_t yPitch = CVPixelBufferGetBytesPerRowOfPlane(imageBuffer, 0); + uint8_t *cbCrBuffer = CVPixelBufferGetBaseAddressOfPlane(imageBuffer, 1); + size_t cbCrPitch = CVPixelBufferGetBytesPerRowOfPlane(imageBuffer, 1); + + int bytesPerPixel = 4; + uint8_t *rgbBuffer = malloc(width * height * bytesPerPixel); + + for (size_t y = 0; y < height; y++) + { + uint8_t *rgbBufferLine = &rgbBuffer[y * width * bytesPerPixel]; + uint8_t *yBufferLine = &yBuffer[y * yPitch]; + uint8_t *cbCrBufferLine = &cbCrBuffer[(y >> 1) * cbCrPitch]; + + for (size_t x = 0; x < width; x++) + { + int16_t y = yBufferLine[x]; + int16_t cb = cbCrBufferLine[x & ~1] - 128; + int16_t cr = cbCrBufferLine[x | 1] - 128; + + uint8_t *rgbOutput = &rgbBufferLine[x * bytesPerPixel]; + + int16_t r = (int16_t)round( y + cr * 1.4 ); + int16_t g = (int16_t)round( y + cb * -0.343 + cr * -0.711 ); + int16_t b = (int16_t)round( y + cb * 1.765); + + rgbOutput[0] = 0xff; + rgbOutput[1] = clamp(b); + rgbOutput[2] = clamp(g); + rgbOutput[3] = clamp(r); + } + } + + CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); + CGContextRef context = CGBitmapContextCreate(rgbBuffer, width, height, 8, width * bytesPerPixel, colorSpace, kCGBitmapByteOrder32Little | kCGImageAlphaNoneSkipLast); + CGImageRef quartzImage = CGBitmapContextCreateImage(context); + UIImage *image = [UIImage imageWithCGImage:quartzImage scale:1.0 orientation:orientation]; + + CGContextRelease(context); + CGColorSpaceRelease(colorSpace); + CGImageRelease(quartzImage); + free(rgbBuffer); + + CVPixelBufferUnlockBaseAddress(imageBuffer, 0); + + return image; +} + +static UIImageOrientation TGSnapshotOrientationForVideoOrientation(bool mirrored) +{ + return mirrored ? UIImageOrientationLeftMirrored : UIImageOrientationRight; +} + +- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection +{ + if (!self.isRunning) + return; + + if (!CMSampleBufferDataIsReady(sampleBuffer)) + { + TGLegacyLog(@"WARNING: camera sample buffer data is not ready, skipping"); + return; + } + + if (self.outputSampleBuffer != nil) + { + CFRetain(sampleBuffer); + self.outputSampleBuffer(sampleBuffer, connection); + CFRelease(sampleBuffer); + } + + if (_movieWriter.isRecording) + [_movieWriter _processSampleBuffer:sampleBuffer]; + + if (!_captureNextFrame || captureOutput != _videoOutput) + return; + + _captureNextFrame = false; + + if (self.capturedFrameCompletion != nil) + { + CFRetain(sampleBuffer); + void(^capturedFrameCompletion)(UIImage *image) = self.capturedFrameCompletion; + self.capturedFrameCompletion = nil; + + [[SQueue concurrentDefaultQueue] dispatch:^ + { + TGDispatchOnMainThread(^{ + bool mirrored = self.requestPreviewIsMirrored(); + UIImageOrientation orientation = TGSnapshotOrientationForVideoOrientation(mirrored); + [[SQueue concurrentDefaultQueue] dispatch:^ + { + UIImage *image = [self imageFromSampleBuffer:sampleBuffer orientation:orientation]; + CFRelease(sampleBuffer); + capturedFrameCompletion(image); + }]; + }); + }]; + } +} + +#pragma mark - Metadata + +- (void)captureOutput:(AVCaptureOutput *)output didOutputMetadataObjects:(NSArray<__kindof AVMetadataObject *> *)metadataObjects fromConnection:(AVCaptureConnection *)connection +{ + if (!self.isRunning) + return; + + if ([metadataObjects.firstObject isKindOfClass:[AVMetadataMachineReadableCodeObject class]]) + { + AVMetadataMachineReadableCodeObject *object = (AVMetadataMachineReadableCodeObject *)metadataObjects.firstObject; + if (object.type == AVMetadataObjectTypeQRCode && object.stringValue.length > 0) + { + TGDispatchOnMainThread(^{ + if (self.recognizedQRCode != nil) + self.recognizedQRCode(object.stringValue, object); + }); + } + } +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGCameraDeviceAngleSampler.h b/submodules/LegacyComponents/LegacyComponents/PGCameraDeviceAngleSampler.h new file mode 100644 index 0000000000..e887d79d49 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGCameraDeviceAngleSampler.h @@ -0,0 +1,15 @@ +#import +#import + +@interface PGCameraDeviceAngleSampler : NSObject + +@property (nonatomic, copy) void(^deviceOrientationChanged)(UIDeviceOrientation orientation); + +@property (nonatomic, readonly) UIDeviceOrientation deviceOrientation; +@property (nonatomic, readonly) bool isMeasuring; +@property (nonatomic, readonly) CGFloat currentDeviceAngle; + +- (void)startMeasuring; +- (void)stopMeasuring; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGCameraDeviceAngleSampler.m b/submodules/LegacyComponents/LegacyComponents/PGCameraDeviceAngleSampler.m new file mode 100644 index 0000000000..1c742bed83 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGCameraDeviceAngleSampler.m @@ -0,0 +1,154 @@ +#import "PGCameraDeviceAngleSampler.h" + +#import + +#import + +#import "LegacyComponentsInternal.h" + +@interface PGCameraDeviceAngleSampler () +{ + CMMotionManager *_motionManager; + NSOperationQueue *_motionQueue; +} +@end + +@implementation PGCameraDeviceAngleSampler + +- (instancetype)init +{ + self = [super init]; + if (self != nil) + { + _deviceOrientation = UIDeviceOrientationUnknown; + + _motionManager = [[CMMotionManager alloc] init]; + _motionManager.accelerometerUpdateInterval = 1.0f; + _motionManager.deviceMotionUpdateInterval = 1.0f; + _motionManager.gyroUpdateInterval = 1.0f; + _motionManager.magnetometerUpdateInterval = 1.0f; + _motionQueue = [[NSOperationQueue alloc] init]; + } + return self; +} + +- (void)dealloc +{ + [self stopMeasuring]; +} + +- (bool)isMeasuring +{ + return [_motionManager isDeviceMotionActive]; +} + +- (void)stopMeasuring +{ + [_motionManager stopDeviceMotionUpdates]; +} + +- (void)startMeasuring +{ + if (![_motionManager isDeviceMotionAvailable]) + return; + + __weak PGCameraDeviceAngleSampler *weakSelf = self; + [_motionManager startAccelerometerUpdatesToQueue:_motionQueue withHandler:^(CMAccelerometerData *accelerometerData, __unused NSError *error) + { + __strong PGCameraDeviceAngleSampler *strongSelf = weakSelf; + if (strongSelf == nil || accelerometerData == nil || error != nil) + return; + + CMAcceleration acceleration = accelerometerData.acceleration; + CGFloat xx = -acceleration.x; + CGFloat yy = acceleration.y; + CGFloat z = acceleration.z; + CGFloat angle = atan2(yy, xx); + + UIDeviceOrientation deviceOrientation = strongSelf.deviceOrientation; + CGFloat absoluteZ = fabs(z); + + if (deviceOrientation == UIDeviceOrientationFaceUp || deviceOrientation == UIDeviceOrientationFaceDown) + { + if (absoluteZ < 0.845f) + { + if (angle < -2.6f) + deviceOrientation = UIDeviceOrientationLandscapeRight; + else if (angle > -2.05f && angle < -1.1f) + deviceOrientation = UIDeviceOrientationPortrait; + else if (angle > -0.48f && angle < 0.48f) + deviceOrientation = UIDeviceOrientationLandscapeLeft; + else if (angle > 1.08f && angle < 2.08f) + deviceOrientation = UIDeviceOrientationPortraitUpsideDown; + } + else if (z < 0.f) + { + deviceOrientation = UIDeviceOrientationFaceUp; + } + else if (z > 0.f) + { + deviceOrientation = UIDeviceOrientationFaceDown; + } + } + else + { + if (z > 0.875f) + { + deviceOrientation = UIDeviceOrientationFaceDown; + } + else if (z < -0.875f) + { + deviceOrientation = UIDeviceOrientationFaceUp; + } + else + { + switch (deviceOrientation) + { + case UIDeviceOrientationLandscapeLeft: + if (angle < -1.07f) deviceOrientation = UIDeviceOrientationPortrait; + if (angle > 1.08f) deviceOrientation = UIDeviceOrientationPortraitUpsideDown; + break; + + case UIDeviceOrientationLandscapeRight: + if (angle < 0.f && angle > -2.05f) deviceOrientation = UIDeviceOrientationPortrait; + if (angle > 0.f && angle < 2.05f) deviceOrientation = UIDeviceOrientationPortraitUpsideDown; + break; + + case UIDeviceOrientationPortraitUpsideDown: + if (angle > 2.66f) deviceOrientation = UIDeviceOrientationLandscapeRight; + if (angle < 0.48f) deviceOrientation = UIDeviceOrientationLandscapeLeft; + break; + + case UIDeviceOrientationPortrait: + default: + if (angle > -0.47f) deviceOrientation = UIDeviceOrientationLandscapeLeft; + if (angle < -2.64f) deviceOrientation = UIDeviceOrientationLandscapeRight; + break; + } + } + } + + if (deviceOrientation != strongSelf.deviceOrientation) + { + strongSelf->_deviceOrientation = deviceOrientation; + + TGDispatchOnMainThread(^ + { + if (strongSelf.deviceOrientationChanged != nil) + strongSelf.deviceOrientationChanged(deviceOrientation); + }); + } + + }]; + + [_motionManager startDeviceMotionUpdatesUsingReferenceFrame:CMAttitudeReferenceFrameXArbitraryZVertical toQueue:_motionQueue withHandler:^(CMDeviceMotion *motion, __unused NSError *error) + { + __strong PGCameraDeviceAngleSampler *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + strongSelf->_currentDeviceAngle = TGRadiansToDegrees((CGFloat)(atan2(motion.gravity.x, motion.gravity.y) - M_PI)) * -1; + }]; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGCameraMomentSegment.h b/submodules/LegacyComponents/LegacyComponents/PGCameraMomentSegment.h new file mode 100644 index 0000000000..ab0699b79b --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGCameraMomentSegment.h @@ -0,0 +1,13 @@ +#import +#import +#import + +@interface PGCameraMomentSegment : NSObject + +@property (nonatomic, readonly) NSURL *fileURL; +@property (nonatomic, readonly) AVAsset *asset; +@property (nonatomic, readonly) NSTimeInterval duration; + +- (instancetype)initWithURL:(NSURL *)url duration:(NSTimeInterval)duration; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGCameraMomentSegment.m b/submodules/LegacyComponents/LegacyComponents/PGCameraMomentSegment.m new file mode 100644 index 0000000000..50c9ed87a8 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGCameraMomentSegment.m @@ -0,0 +1,22 @@ +#import "PGCameraMomentSegment.h" + +@interface PGCameraMomentSegment () +{ + +} +@end + +@implementation PGCameraMomentSegment + +- (instancetype)initWithURL:(NSURL *)url duration:(NSTimeInterval)duration +{ + self = [super init]; + if (self != nil) + { + _fileURL = url; + _duration = duration; + } + return self; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGCameraMomentSession.h b/submodules/LegacyComponents/LegacyComponents/PGCameraMomentSession.h new file mode 100644 index 0000000000..a7f8269308 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGCameraMomentSession.h @@ -0,0 +1,28 @@ +#import + +@class PGCamera; + +@interface PGCameraMomentSession : NSObject + +@property (nonatomic, copy) void (^beganCapture)(void); +@property (nonatomic, copy) void (^finishedCapture)(void); +@property (nonatomic, copy) bool (^captureIsAvailable)(void); +@property (nonatomic, copy) void (^durationChanged)(NSTimeInterval); + +@property (nonatomic, readonly) bool isCapturing; +@property (nonatomic, readonly) UIImage *previewImage; +@property (nonatomic, readonly) bool hasSegments; + +@property (nonatomic, readonly) PGCameraMomentSegment *lastSegment; + +- (instancetype)initWithCamera:(PGCamera *)camera; + +- (void)captureSegment; +- (void)commitSegment; + +- (void)addSegment:(PGCameraMomentSegment *)segment; +- (void)removeSegment:(PGCameraMomentSegment *)segment; +- (void)removeLastSegment; +- (void)removeAllSegments; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGCameraMomentSession.m b/submodules/LegacyComponents/LegacyComponents/PGCameraMomentSession.m new file mode 100644 index 0000000000..d83cbfc354 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGCameraMomentSession.m @@ -0,0 +1,89 @@ +#import "PGCameraMomentSession.h" +#import "PGCamera.h" + +@interface PGCameraMomentSession () +{ + NSString *_uniqueIdentifier; + NSURL *_segmentsDirectory; + + PGCamera *_camera; + NSMutableArray *_segments; +} +@end + +@implementation PGCameraMomentSession + +- (instancetype)initWithCamera:(PGCamera *)camera +{ + self = [super init]; + if (self != nil) + { + _camera = camera; + _segments = [[NSMutableArray alloc] init]; + + int64_t uniqueId = 0; + arc4random_buf(&uniqueId, 8); + _uniqueIdentifier = [NSString stringWithFormat:@"%x", (int)arc4random()]; + } + return self; +} + +- (NSURL *)segmentsDirectory +{ + if (_segmentsDirectory == nil) + _segmentsDirectory = [NSURL fileURLWithPath:[NSTemporaryDirectory() stringByAppendingPathComponent:_uniqueIdentifier]]; + + return _segmentsDirectory; +} + +- (void)captureSegment +{ + if (self.isCapturing) + return; + + _isCapturing = true; + + if (self.beganCapture != nil) + self.beganCapture(); + + [_camera startVideoRecordingForMoment:true completion:^(NSURL *resultUrl, __unused CGAffineTransform transform, __unused CGSize dimensions, NSTimeInterval duration, bool success) + { + if (!success) + return; + + _isCapturing = false; + + if (self.finishedCapture != nil) + self.finishedCapture(); + + PGCameraMomentSegment *segment = [[PGCameraMomentSegment alloc] initWithURL:resultUrl duration:duration]; + [self addSegment:segment]; + }]; +} + +- (void)commitSegment +{ + [_camera stopVideoRecording]; +} + +- (void)addSegment:(PGCameraMomentSegment *)segment +{ + [_segments addObject:segment]; +} + +- (void)removeSegment:(PGCameraMomentSegment *)segment +{ + [_segments removeObject:segment]; +} + +- (void)removeLastSegment +{ + [_segments removeLastObject]; +} + +- (void)removeAllSegments +{ + [_segments removeAllObjects]; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGCameraMovieWriter.h b/submodules/LegacyComponents/LegacyComponents/PGCameraMovieWriter.h new file mode 100644 index 0000000000..5e187052f3 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGCameraMovieWriter.h @@ -0,0 +1,20 @@ +#import + +@interface PGCameraMovieWriter : NSObject + +@property (nonatomic, copy) void(^finishedWithMovieAtURL)(NSURL *url, CGAffineTransform transform, CGSize dimensions, NSTimeInterval duration, bool success); + +@property (nonatomic, readonly) NSTimeInterval currentDuration; +@property (nonatomic, readonly) bool isRecording; +@property (nonatomic, assign) bool liveUpload; + +- (instancetype)initWithVideoTransform:(CGAffineTransform)videoTransform videoOutputSettings:(NSDictionary *)videoSettings audioOutputSettings:(NSDictionary *)audioSettings; + +- (void)startRecording; +- (void)stopRecordingWithCompletion:(void (^)(void))completion; + +- (void)_processSampleBuffer:(CMSampleBufferRef)sampleBuffer; + ++ (NSString *)outputFileType; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGCameraMovieWriter.m b/submodules/LegacyComponents/LegacyComponents/PGCameraMovieWriter.m new file mode 100644 index 0000000000..264404d478 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGCameraMovieWriter.m @@ -0,0 +1,288 @@ +#import "PGCameraMovieWriter.h" + +#import "LegacyComponentsInternal.h" + +#import +#import + +#import + +@interface PGCameraMovieWriter () +{ + AVAssetWriter *_assetWriter; + AVAssetWriterInput *_videoInput; + AVAssetWriterInput *_audioInput; + + bool _startedWriting; + bool _finishedWriting; + + CGAffineTransform _videoTransform; + NSDictionary *_videoOutputSettings; + NSDictionary *_audioOutputSettings; + + CMTime _startTimeStamp; + CMTime _lastVideoTimeStamp; + CMTime _lastAudioTimeStamp; + + NSMutableArray *_delayedAudioSamples; + + NSTimeInterval _captureStartTime; + SQueue *_queue; + + bool _stopIminent; + void (^_finishCompletion)(void); +} +@end + +@implementation PGCameraMovieWriter + +- (instancetype)initWithVideoTransform:(CGAffineTransform)videoTransform videoOutputSettings:(NSDictionary *)videoSettings audioOutputSettings:(NSDictionary *)audioSettings +{ + self = [super init]; + if (self != nil) + { + _videoTransform = videoTransform; + _videoOutputSettings = videoSettings; + _audioOutputSettings = audioSettings; + + _queue = [[SQueue alloc] init]; + + _delayedAudioSamples = [[NSMutableArray alloc] init]; + } + return self; +} + +- (void)startRecording +{ + [_queue dispatch:^ + { + if (_isRecording || _finishedWriting) + return; + + _captureStartTime = CFAbsoluteTimeGetCurrent(); + + NSError *error = nil; + + NSString *path = [PGCameraMovieWriter tempOutputPath]; + + if ([[NSFileManager defaultManager] fileExistsAtPath:path]) + [[NSFileManager defaultManager] removeItemAtPath:path error:NULL]; + + _assetWriter = [[AVAssetWriter alloc] initWithURL:[NSURL fileURLWithPath:path] fileType:[PGCameraMovieWriter outputFileType] error:&error]; + + if (_assetWriter == nil && error != nil) + { + TGLegacyLog(@"ERROR: camera movie writer failed to initialize: %@", error); + return; + } + + _videoInput = [[AVAssetWriterInput alloc] initWithMediaType:AVMediaTypeVideo outputSettings:_videoOutputSettings]; + _videoInput.expectsMediaDataInRealTime = true; + _videoInput.transform = _videoTransform; + + if ([_assetWriter canAddInput:_videoInput]) + { + [_assetWriter addInput:_videoInput]; + } + else + { + TGLegacyLog(@"ERROR: camera movie writer failed to add video input"); + return; + } + + if (_audioOutputSettings != nil) + { + _audioInput = [[AVAssetWriterInput alloc] initWithMediaType:AVMediaTypeAudio outputSettings:_audioOutputSettings]; + _audioInput.expectsMediaDataInRealTime = true; + if ([_assetWriter canAddInput:_audioInput]) + { + [_assetWriter addInput:_audioInput]; + } + else + { + TGLegacyLog(@"ERROR: camera movie writer failed to add audio input"); + return; + } + } + + [_assetWriter startWriting]; + _isRecording = true; + }]; +} + +- (void)stopRecordingWithCompletion:(void (^)(void))completion +{ + [_queue dispatch:^ + { + if (fabs(CFAbsoluteTimeGetCurrent() - _captureStartTime) < 0.5) + return; + + _stopIminent = true; + _finishCompletion = completion; + + if (_assetWriter.status == AVAssetWriterStatusUnknown || _assetWriter.status > AVAssetWriterStatusCompleted) + { + TGDispatchOnMainThread(^ + { + if (self.finishedWithMovieAtURL != nil) + self.finishedWithMovieAtURL(nil, CGAffineTransformIdentity, CGSizeZero, 0.0, false); + TGLegacyLog(@"ERROR: camera movie writer failed to write movie: %@", _assetWriter.error); + + _assetWriter = nil; + }); + + return; + } + + if (_audioOutputSettings == nil) + [self _finishWithCompletion]; + }]; +} + +- (void)_finishWithCompletion +{ + _isRecording = false; + + __weak PGCameraMovieWriter *weakSelf = self; + [_assetWriter finishWritingWithCompletionHandler:^ + { + __strong PGCameraMovieWriter *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + strongSelf->_finishedWriting = true; + + TGDispatchOnMainThread(^ + { + if (strongSelf->_assetWriter.status == AVAssetWriterStatusCompleted) + { + if (strongSelf.finishedWithMovieAtURL != nil) + { + AVURLAsset *asset = [[AVURLAsset alloc] initWithURL:strongSelf->_assetWriter.outputURL options:nil]; + AVAssetTrack *track = [[asset tracksWithMediaType:AVMediaTypeVideo] firstObject]; + CGSize dimensions = TGTransformDimensionsWithTransform(track.naturalSize, strongSelf->_videoTransform); + strongSelf.finishedWithMovieAtURL(strongSelf->_assetWriter.outputURL, strongSelf->_videoTransform, dimensions, strongSelf.currentDuration, true); + } + } + else + { + if (strongSelf.finishedWithMovieAtURL != nil) + strongSelf.finishedWithMovieAtURL(strongSelf->_assetWriter.outputURL, CGAffineTransformIdentity, CGSizeZero, 0.0, false); + TGLegacyLog(@"ERROR: camera movie writer failed to write movie: %@", strongSelf->_assetWriter.error); + } + + strongSelf->_assetWriter = nil; + + if (_finishCompletion != nil) + _finishCompletion(); + }); + }]; + +} + +- (void)_processSampleBuffer:(CMSampleBufferRef)sampleBuffer +{ + CFRetain(sampleBuffer); + [_queue dispatch:^ + { + CMFormatDescriptionRef formatDescription = CMSampleBufferGetFormatDescription(sampleBuffer); + CMMediaType mediaType = CMFormatDescriptionGetMediaType(formatDescription); + CMTime timestamp = CMSampleBufferGetPresentationTimeStamp(sampleBuffer); + + if (_assetWriter.status > AVAssetWriterStatusCompleted) + { + TGLegacyLog(@"WARNING: camera movie writer status is %d", _assetWriter.status); + if (_assetWriter.status == AVAssetWriterStatusFailed) + { + TGLegacyLog(@"ERROR: camera movie writer error: %@", _assetWriter.error); + _isRecording = false; + + if (self.finishedWithMovieAtURL != nil) + self.finishedWithMovieAtURL(_assetWriter.outputURL, CGAffineTransformIdentity, CGSizeZero, 0.0, false); + } + return; + } + + bool keepSample = false; + if (mediaType == kCMMediaType_Video) + { + if (!_startedWriting) + { + [_assetWriter startSessionAtSourceTime:timestamp]; + _startTimeStamp = timestamp; + + _startedWriting = true; + } + + while (!_videoInput.readyForMoreMediaData) + { + NSDate *maxDate = [NSDate dateWithTimeIntervalSinceNow:0.1]; + [[NSRunLoop currentRunLoop] runUntilDate:maxDate]; + } + + bool success = [_videoInput appendSampleBuffer:sampleBuffer]; + if (success) + _lastVideoTimeStamp = timestamp; + else + TGLegacyLog(@"ERROR: camera movie writer failed to append pixel buffer"); + + if (_audioOutputSettings != nil && _stopIminent && CMTimeCompare(_lastVideoTimeStamp, _lastAudioTimeStamp) != -1) { + [self _finishWithCompletion]; + } + } + else if (mediaType == kCMMediaType_Audio && !_stopIminent) + { + if (!_startedWriting) + { + [_delayedAudioSamples addObject:(__bridge id _Nonnull)(sampleBuffer)]; + keepSample = true; + } + else + { + if (_delayedAudioSamples.count > 0) + { + for (id sample in _delayedAudioSamples) + { + CMSampleBufferRef buffer = (__bridge CMSampleBufferRef)(sample); + [_audioInput appendSampleBuffer:buffer]; + CFRelease(buffer); + } + + _delayedAudioSamples = nil; + } + + while (!_audioInput.isReadyForMoreMediaData) + { + NSDate *maxDate = [NSDate dateWithTimeIntervalSinceNow:0.1]; + [[NSRunLoop currentRunLoop] runUntilDate:maxDate]; + } + + bool success = [_audioInput appendSampleBuffer:sampleBuffer]; + if (success) + _lastAudioTimeStamp = timestamp; + else + TGLegacyLog(@"ERROR: camera movie writer failed to append audio buffer"); + } + } + + if (!keepSample) + CFRelease(sampleBuffer); + }]; +} + +- (NSTimeInterval)currentDuration +{ + return CMTimeGetSeconds(CMTimeSubtract(_lastVideoTimeStamp, _startTimeStamp)); +} + ++ (NSString *)outputFileType +{ + return AVFileTypeMPEG4; +} + ++ (NSString *)tempOutputPath +{ + return [NSTemporaryDirectory() stringByAppendingPathComponent:[[NSString alloc] initWithFormat:@"camvideo_%x.mp4", (int)arc4random()]]; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGCameraShotMetadata.h b/submodules/LegacyComponents/LegacyComponents/PGCameraShotMetadata.h new file mode 100644 index 0000000000..59a07a7051 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGCameraShotMetadata.h @@ -0,0 +1,10 @@ +#import +#import + +@interface PGCameraShotMetadata : NSObject + +@property (nonatomic, assign) CGFloat deviceAngle; + ++ (CGFloat)relativeDeviceAngleFromAngle:(CGFloat)angle orientation:(UIInterfaceOrientation)orientation; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGCameraShotMetadata.m b/submodules/LegacyComponents/LegacyComponents/PGCameraShotMetadata.m new file mode 100644 index 0000000000..7189cd749b --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGCameraShotMetadata.m @@ -0,0 +1,33 @@ +#import "PGCameraShotMetadata.h" + +@implementation PGCameraShotMetadata + ++ (CGFloat)relativeDeviceAngleFromAngle:(CGFloat)angle orientation:(UIInterfaceOrientation)orientation +{ + switch (orientation) + { + case UIInterfaceOrientationPortraitUpsideDown: + angle -= 180.0f; + break; + + case UIInterfaceOrientationLandscapeLeft: + angle -= 90.0f; + break; + + case UIInterfaceOrientationLandscapeRight: + angle -= 270.0f; + break; + + default: + if (angle > 180.0f) + angle = angle - 360.0f; + break; + } + + if (ABS(angle) < 45.0f) + return angle; + + return 0.0f; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGCameraVolumeButtonHandler.h b/submodules/LegacyComponents/LegacyComponents/PGCameraVolumeButtonHandler.h new file mode 100644 index 0000000000..a958f1a9f4 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGCameraVolumeButtonHandler.h @@ -0,0 +1,14 @@ +#import + +@interface PGCameraVolumeButtonHandler : NSObject + +@property (nonatomic, assign) bool enabled; +@property (nonatomic, assign) bool ignoring; + +- (instancetype)initWithUpButtonPressedBlock:(void (^)(void))upButtonPressedBlock upButtonReleasedBlock:(void (^)(void))upButtonReleasedBlock downButtonPressedBlock:(void (^)(void))downButtonPressedBlock downButtonReleasedBlock:(void (^)(void))downButtonReleasedBlock; + +- (void)enableIn:(NSTimeInterval)timeInterval; +- (void)disableFor:(NSTimeInterval)timeInterval; +- (void)ignoreEventsFor:(NSTimeInterval)timeInterval andDisable:(bool)disable; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGCameraVolumeButtonHandler.m b/submodules/LegacyComponents/LegacyComponents/PGCameraVolumeButtonHandler.m new file mode 100644 index 0000000000..05e0f2431c --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGCameraVolumeButtonHandler.m @@ -0,0 +1,153 @@ +#import "PGCameraVolumeButtonHandler.h" + +#import "LegacyComponentsInternal.h" + +#import "TGStringUtils.h" +#import "Freedom.h" + +@interface PGCameraVolumeButtonHandler () + +@property (nonatomic, copy) void(^upButtonPressedBlock)(void); +@property (nonatomic, copy) void(^upButtonReleasedBlock)(void); +@property (nonatomic, copy) void(^downButtonPressedBlock)(void); +@property (nonatomic, copy) void(^downButtonReleasedBlock)(void); + +@end + +@implementation PGCameraVolumeButtonHandler + +- (instancetype)initWithUpButtonPressedBlock:(void (^)(void))upButtonPressedBlock upButtonReleasedBlock:(void (^)(void))upButtonReleasedBlock downButtonPressedBlock:(void (^)(void))downButtonPressedBlock downButtonReleasedBlock:(void (^)(void))downButtonReleasedBlock +{ + self = [super init]; + if (self != nil) + { + self.upButtonPressedBlock = upButtonPressedBlock; + self.upButtonReleasedBlock = upButtonReleasedBlock; + self.downButtonPressedBlock = downButtonPressedBlock; + self.downButtonReleasedBlock = downButtonReleasedBlock; + + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNotification:) name:nil object:nil]; + + self.enabled = true; + } + return self; +} + +- (void)dealloc +{ + self.enabled = false; + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +#pragma mark - + +static void PGButtonHandlerEnableMonitoring(bool enable) +{ + static void (*methodImpl)(id, SEL, BOOL) = NULL; + static dispatch_once_t onceToken; + static SEL methodSelector = NULL; + dispatch_once(&onceToken, ^ + { + methodImpl = (void (*)(id, SEL, BOOL))freedomImplInstancesOfClass([UIApplication class], 0xf8de0049, NULL); + }); + + if (methodImpl != NULL) { + methodImpl([[LegacyComponentsGlobals provider] applicationInstance], methodSelector, enable); + } +} + +#pragma mark - + +- (void)handleNotification:(NSNotification *)notification +{ + NSUInteger nameLength = notification.name.length; + if (nameLength == 46 || nameLength == 44 || nameLength == 42) + { + uint32_t hash = legacy_murMurHash32(notification.name); + switch (hash) + { + case 0xaeae3258: //_UIApplicationVolumeDownButtonDownNotification + { + if (self.downButtonPressedBlock != nil) + self.downButtonPressedBlock(); + } + break; + + case 0x784c165e: //_UIApplicationVolumeDownButtonUpNotification + { + if (self.downButtonReleasedBlock != nil) + self.downButtonReleasedBlock(); + } + break; + + case 0xba416d8e: //_UIApplicationVolumeUpButtonDownNotification + { + if (self.upButtonPressedBlock != nil) + self.upButtonPressedBlock(); + } + break; + + case 0x4074ecfb: //_UIApplicationVolumeUpButtonUpNotification + { + if (self.upButtonReleasedBlock != nil) + self.upButtonReleasedBlock(); + } + break; + + default: + break; + } + } +} + +#pragma mark - + +- (void)setEnabled:(bool)enabled +{ + _enabled = enabled; + TGDispatchOnMainThread(^{ + PGButtonHandlerEnableMonitoring(enabled); + }); +} + +- (void)enableIn:(NSTimeInterval)timeInterval +{ + if (_enabled) + return; + + TGDispatchAfter(timeInterval, dispatch_get_main_queue(), ^ + { + [self setEnabled:true]; + }); +} + +- (void)disableFor:(NSTimeInterval)timeInterval +{ + if (!_enabled) + return; + + TGDispatchAfter(timeInterval, dispatch_get_main_queue(), ^ + { + _enabled = true; + }); +} + +- (void)ignoreEventsFor:(NSTimeInterval)timeInterval andDisable:(bool)disable +{ + if (!self.enabled) + return; + + _ignoring = true; + + [self performSelector:@selector(_ignoreFinished:) withObject:@(disable) afterDelay:timeInterval]; +} + +- (void)_ignoreFinished:(NSNumber *)disable +{ + _ignoring = false; + + if (disable.boolValue) + self.enabled = false; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGContrastTool.h b/submodules/LegacyComponents/LegacyComponents/PGContrastTool.h new file mode 100644 index 0000000000..5be618be1e --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGContrastTool.h @@ -0,0 +1,5 @@ +#import "PGPhotoTool.h" + +@interface PGContrastTool : PGPhotoTool + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGContrastTool.m b/submodules/LegacyComponents/LegacyComponents/PGContrastTool.m new file mode 100644 index 0000000000..f72969da23 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGContrastTool.m @@ -0,0 +1,68 @@ +#import "PGContrastTool.h" + +#import "LegacyComponentsInternal.h" + +@interface PGContrastTool () +{ + PGPhotoProcessPassParameter *_parameter; +} +@end + +@implementation PGContrastTool + +- (instancetype)init +{ + self = [super init]; + if (self != nil) + { + _identifier = @"contrast"; + _type = PGPhotoToolTypeShader; + _order = 6; + + _minimumValue = -100; + _maximumValue = 100; + _defaultValue = 0; + + self.value = @(_defaultValue); + } + return self; +} + +- (NSString *)title +{ + return TGLocalized(@"PhotoEditor.ContrastTool"); +} + +- (bool)shouldBeSkipped +{ + return (ABS(((NSNumber *)self.displayValue).floatValue - (float)self.defaultValue) < FLT_EPSILON); +} + +- (NSArray *)parameters +{ + if (!_parameters) + { + _parameter = [PGPhotoProcessPassParameter parameterWithName:@"contrast" type:@"lowp float"]; + _parameters = @[ _parameter ]; + } + + return _parameters; +} + +- (void)updateParameters +{ + NSNumber *value = (NSNumber *)self.displayValue; + + CGFloat parameterValue = (value.floatValue / 100.0f) * 0.3f + 1; + [_parameter setFloatValue:parameterValue]; +} + +- (NSString *)shaderString +{ + return PGShaderString + ( + result = vec4(clamp(((result.rgb - vec3(0.5)) * contrast + vec3(0.5)), 0.0, 1.0), result.a); + ); +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGCurvesTool.h b/submodules/LegacyComponents/LegacyComponents/PGCurvesTool.h new file mode 100644 index 0000000000..9dec83abd4 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGCurvesTool.h @@ -0,0 +1,38 @@ +#import "PGPhotoTool.h" + +typedef enum +{ + PGCurvesTypeLuminance, + PGCurvesTypeRed, + PGCurvesTypeGreen, + PGCurvesTypeBlue +} PGCurvesType; + +@interface PGCurvesValue : NSObject + +@property (nonatomic, assign) CGFloat blacksLevel; +@property (nonatomic, assign) CGFloat shadowsLevel; +@property (nonatomic, assign) CGFloat midtonesLevel; +@property (nonatomic, assign) CGFloat highlightsLevel; +@property (nonatomic, assign) CGFloat whitesLevel; + +- (NSArray *)interpolateCurve; + ++ (instancetype)defaultValue; + +@end + +@interface PGCurvesToolValue : NSObject + +@property (nonatomic, strong) PGCurvesValue *luminanceCurve; +@property (nonatomic, strong) PGCurvesValue *redCurve; +@property (nonatomic, strong) PGCurvesValue *greenCurve; +@property (nonatomic, strong) PGCurvesValue *blueCurve; + +@property (nonatomic, assign) PGCurvesType activeType; + +@end + +@interface PGCurvesTool : PGPhotoTool + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGCurvesTool.m b/submodules/LegacyComponents/LegacyComponents/PGCurvesTool.m new file mode 100644 index 0000000000..26cd42c9e8 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGCurvesTool.m @@ -0,0 +1,359 @@ +#import "PGCurvesTool.h" + +#import "LegacyComponentsInternal.h" + +#import "TGPhotoEditorCurvesToolView.h" +#import "TGPhotoEditorCurvesHistogramView.h" + +const NSUInteger PGCurveGranularity = 100; +const NSUInteger PGCurveDataStep = 2; + +@interface PGCurvesValue () +{ + NSArray *_cachedDataPoints; +} +@end + +@implementation PGCurvesValue + +- (instancetype)copyWithZone:(NSZone *)__unused zone +{ + PGCurvesValue *value = [[PGCurvesValue alloc] init]; + value.blacksLevel = self.blacksLevel; + value.shadowsLevel = self.shadowsLevel; + value.midtonesLevel = self.midtonesLevel; + value.highlightsLevel = self.highlightsLevel; + value.whitesLevel = self.whitesLevel; + + return value; +} + ++ (instancetype)defaultValue +{ + PGCurvesValue *value = [[PGCurvesValue alloc] init]; + value.blacksLevel = 0; + value.shadowsLevel = 25; + value.midtonesLevel = 50; + value.highlightsLevel = 75; + value.whitesLevel = 100; + + return value; +} + +- (NSArray *)dataPoints +{ + if (_cachedDataPoints == nil) + [self interpolateCurve]; + + return _cachedDataPoints; +} + +- (bool)isDefault +{ + if (fabs(self.blacksLevel - 0) < FLT_EPSILON + && fabs(self.shadowsLevel - 25) < FLT_EPSILON + && fabs(self.midtonesLevel - 50) < FLT_EPSILON + && fabs(self.highlightsLevel - 75) < FLT_EPSILON + && fabs(self.whitesLevel - 100) < FLT_EPSILON) + { + return true; + } + + return false; +} + +- (NSArray *)interpolateCurve +{ + NSMutableArray *points = [[NSMutableArray alloc] init]; + [points addObject:[NSValue valueWithCGPoint:CGPointMake(-0.001, self.blacksLevel / 100.0)]]; + [points addObject:[NSValue valueWithCGPoint:CGPointMake(0.0, self.blacksLevel / 100.0)]]; + [points addObject:[NSValue valueWithCGPoint:CGPointMake(0.25, self.shadowsLevel / 100.0)]]; + [points addObject:[NSValue valueWithCGPoint:CGPointMake(0.5, self.midtonesLevel / 100.0)]]; + [points addObject:[NSValue valueWithCGPoint:CGPointMake(0.75, self.highlightsLevel / 100.0)]]; + [points addObject:[NSValue valueWithCGPoint:CGPointMake(1, self.whitesLevel / 100.0)]]; + [points addObject:[NSValue valueWithCGPoint:CGPointMake(1.001, self.whitesLevel / 100.0)]]; + + NSMutableArray *dataPoints = [[NSMutableArray alloc] init]; + + NSMutableArray *interpolatedPoints = [[NSMutableArray alloc] init]; + [interpolatedPoints addObject:points.firstObject]; + + for (NSUInteger index = 1; index < points.count - 2; index++) + { + CGPoint point0 = [points[index - 1] CGPointValue]; + CGPoint point1 = [points[index] CGPointValue]; + CGPoint point2 = [points[index + 1] CGPointValue]; + CGPoint point3 = [points[index + 2] CGPointValue]; + + for (NSUInteger i = 1; i < PGCurveGranularity; i++) + { + CGFloat t = (CGFloat)i * (1.0f / (CGFloat)PGCurveGranularity); + CGFloat tt = t * t; + CGFloat ttt = tt * t; + + CGPoint pi = + { + 0.5 * (2 * point1.x + (point2.x - point0.x) * t + (2 * point0.x - 5 * point1.x + 4 * point2.x - point3.x) * tt + (3 * point1.x - point0.x - 3 * point2.x + point3.x) * ttt), + 0.5 * (2 * point1.y + (point2.y - point0.y) * t + (2 * point0.y - 5 * point1.y + 4 * point2.y - point3.y) * tt + (3 * point1.y - point0.y - 3 * point2.y + point3.y) * ttt) + }; + + pi.y = MAX(0, MIN(1, pi.y)); + + if (pi.x > point0.x) + [interpolatedPoints addObject:[NSValue valueWithCGPoint:pi]]; + + if ((i - 1) % PGCurveDataStep == 0) + [dataPoints addObject:@(pi.y)]; + } + + [interpolatedPoints addObject:[NSValue valueWithCGPoint:point2]]; + } + + [interpolatedPoints addObject:points.lastObject]; + + _cachedDataPoints = dataPoints; + + return interpolatedPoints; +} + +@end + +@implementation PGCurvesToolValue + +- (instancetype)copyWithZone:(NSZone *)__unused zone +{ + PGCurvesToolValue *value = [[PGCurvesToolValue alloc] init]; + value.luminanceCurve = [self.luminanceCurve copy]; + value.redCurve = [self.redCurve copy]; + value.greenCurve = [self.greenCurve copy]; + value.blueCurve = [self.blueCurve copy]; + value.activeType = self.activeType; + + return value; +} + ++ (instancetype)defaultValue +{ + PGCurvesToolValue *value = [[PGCurvesToolValue alloc] init]; + value.luminanceCurve = [PGCurvesValue defaultValue]; + value.redCurve = [PGCurvesValue defaultValue]; + value.greenCurve = [PGCurvesValue defaultValue]; + value.blueCurve = [PGCurvesValue defaultValue]; + value.activeType = PGCurvesTypeLuminance; + + return value; +} + +- (id)cleanValue +{ + PGCurvesToolValue *value = [[PGCurvesToolValue alloc] init]; + value.luminanceCurve = [self.luminanceCurve copy]; + value.redCurve = [self.redCurve copy]; + value.greenCurve = [self.greenCurve copy]; + value.blueCurve = [self.blueCurve copy]; + value.activeType = PGCurvesTypeLuminance; + + return value; +} + +@end + + +@interface PGCurvesTool () +{ + PGPhotoProcessPassParameter *_rgbCurveParameter; + PGPhotoProcessPassParameter *_redCurveParameter; + PGPhotoProcessPassParameter *_greenCurveParameter; + PGPhotoProcessPassParameter *_blueCurveParameter; + + PGPhotoProcessPassParameter *_skipToneParameter; +} +@end + +@implementation PGCurvesTool + +- (instancetype)init +{ + self = [super init]; + if (self != nil) + { + _identifier = @"curves"; + _type = PGPhotoToolTypeShader; + _order = 1; + + _minimumValue = 0; + _maximumValue = 100; + _defaultValue = 0; + + self.value = [PGCurvesToolValue defaultValue]; + } + return self; +} + +- (NSString *)title +{ + return TGLocalized(@"PhotoEditor.CurvesTool"); +} + +- (UIView *)itemAreaViewWithChangeBlock:(void (^)(id))changeBlock explicit:(bool)explicit +{ + __weak PGCurvesTool *weakSelf = self; + + UIView *view = [[TGPhotoEditorCurvesToolView alloc] initWithEditorItem:self]; + view.valueChanged = ^(id newValue, __unused bool animated) + { + __strong PGPhotoTool *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + if (newValue != nil) + { + if (!explicit && [strongSelf.tempValue isEqual:newValue]) + return; + + if (explicit && [strongSelf.value isEqual:newValue]) + return; + + if (!explicit) + strongSelf.tempValue = newValue; + else + strongSelf.value = newValue; + } + + if (changeBlock != nil) + changeBlock(newValue); + }; + return view; +} + +- (UIView *)itemControlViewWithChangeBlock:(void (^)(id, bool))__unused changeBlock explicit:(bool)explicit nameWidth:(CGFloat)__unused nameWidth +{ + __weak PGCurvesTool *weakSelf = self; + + UIView *view = [[TGPhotoEditorCurvesHistogramView alloc] initWithEditorItem:self]; + view.valueChanged = ^(id newValue, __unused bool animated) + { + __strong PGPhotoTool *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + if (newValue != nil) + { + if (!explicit && [strongSelf.tempValue isEqual:newValue]) + return; + + if (explicit && [strongSelf.value isEqual:newValue]) + return; + + if (!explicit) + strongSelf.tempValue = newValue; + else + strongSelf.value = newValue; + } + + if (changeBlock != nil) + changeBlock(newValue, false); + }; + + return view; +} + +- (Class)valueClass +{ + return [PGCurvesToolValue class]; +} + +- (bool)shouldBeSkipped +{ + PGCurvesToolValue *value = (PGCurvesToolValue *)self.displayValue; + return [value.luminanceCurve isDefault] && [value.redCurve isDefault] && [value.greenCurve isDefault] && [value.blueCurve isDefault]; +} + +- (NSArray *)parameters +{ + if (!_parameters) + { + NSInteger count = PGCurveGranularity * PGCurveDataStep; + _rgbCurveParameter = [PGPhotoProcessPassParameter parameterWithName:@"rgbCurveValues" type:@"lowp float" count:count]; + _redCurveParameter = [PGPhotoProcessPassParameter parameterWithName:@"redCurveValues" type:@"lowp float" count:count]; + _greenCurveParameter = [PGPhotoProcessPassParameter parameterWithName:@"greenCurveValues" type:@"lowp float" count:count]; + _blueCurveParameter = [PGPhotoProcessPassParameter parameterWithName:@"blueCurveValues" type:@"lowp float" count:count]; + _skipToneParameter = [PGPhotoProcessPassParameter parameterWithName:@"skipTone" type:@"lowp float"]; + + _parameters = @[ _rgbCurveParameter, _redCurveParameter, _greenCurveParameter, _blueCurveParameter, _skipToneParameter ]; + } + + return _parameters; +} + +- (void)updateParameters +{ + PGCurvesToolValue *value = (PGCurvesToolValue *)self.displayValue; + + [_rgbCurveParameter setFloatArray:[value.luminanceCurve dataPoints]]; + [_redCurveParameter setFloatArray:[value.redCurve dataPoints]]; + [_greenCurveParameter setFloatArray:[value.greenCurve dataPoints]]; + [_blueCurveParameter setFloatArray:[value.blueCurve dataPoints]]; + + [_skipToneParameter setFloatValue:self.shouldBeSkipped ? 1.0 : 0.0]; +} + +- (NSString *)ancillaryShaderString +{ + return PGShaderString + ( + lowp vec3 applyLuminanceCurve(lowp vec3 pixel) { + int index = int(clamp(pixel.z / (1.0 / 200.0), 0.0, 199.0)); + highp float value = rgbCurveValues[index]; + + highp float grayscale = (smoothstep(0.0, 0.1, pixel.z) * (1.0 - smoothstep(0.8, 1.0, pixel.z))); + highp float saturation = mix(0.0, pixel.y, grayscale); + pixel.y = saturation; + pixel.z = value; + return pixel; + } + + lowp vec3 applyRGBCurve(lowp vec3 pixel) { + int index = int(clamp(pixel.r / (1.0 / 200.0), 0.0, 199.0)); + highp float value = redCurveValues[index]; + pixel.r = value; + + index = int(clamp(pixel.g / (1.0 / 200.0), 0.0, 199.0)); + value = greenCurveValues[index]; + pixel.g = clamp(value, 0.0, 1.0); + + index = int(clamp(pixel.b / (1.0 / 200.0), 0.0, 199.0)); + value = blueCurveValues[index]; + pixel.b = clamp(value, 0.0, 1.0); + + return pixel; + } + ); +} + +- (NSString *)stringValue +{ + if (![self shouldBeSkipped]) + { + return @"◆"; + } + + return nil; +} + +- (NSString *)shaderString +{ + return PGShaderString + ( + if (skipTone < toolEpsilon) { + result = vec4(applyRGBCurve(hslToRgb(applyLuminanceCurve(rgbToHsl(result.rgb)))), result.a); + } + ); +} + +- (bool)isSimple +{ + return false; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGEnhanceTool.h b/submodules/LegacyComponents/LegacyComponents/PGEnhanceTool.h new file mode 100644 index 0000000000..6429846998 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGEnhanceTool.h @@ -0,0 +1,5 @@ +#import "PGPhotoTool.h" + +@interface PGEnhanceTool : PGPhotoTool + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGEnhanceTool.m b/submodules/LegacyComponents/LegacyComponents/PGEnhanceTool.m new file mode 100644 index 0000000000..4bb20d3755 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGEnhanceTool.m @@ -0,0 +1,52 @@ +#import "PGEnhanceTool.h" + +#import "LegacyComponentsInternal.h" + +#import "PGPhotoEnhancePass.h" + +@implementation PGEnhanceTool + +- (instancetype)init +{ + self = [super init]; + if (self != nil) + { + _identifier = @"enhance"; + _type = PGPhotoToolTypePass; + _order = 0; + + _pass = [[PGPhotoEnhancePass alloc] init]; + + _minimumValue = 0; + _maximumValue = 100; + _defaultValue = 0; + + self.value = @(_defaultValue); + } + return self; +} + +- (NSString *)title +{ + return TGLocalized(@"PhotoEditor.EnhanceTool"); +} + +- (PGPhotoProcessPass *)pass +{ + [self updatePassParameters]; + + return _pass; +} + +- (bool)shouldBeSkipped +{ + return (ABS(((NSNumber *)self.displayValue).floatValue - self.defaultValue) < FLT_EPSILON); +} + +- (void)updatePassParameters +{ + NSNumber *value = (NSNumber *)self.displayValue; + [(PGPhotoEnhancePass *)_pass setIntensity:value.floatValue / 100]; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGExposureTool.h b/submodules/LegacyComponents/LegacyComponents/PGExposureTool.h new file mode 100644 index 0000000000..0868856f15 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGExposureTool.h @@ -0,0 +1,5 @@ +#import "PGPhotoTool.h" + +@interface PGExposureTool : PGPhotoTool + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGExposureTool.m b/submodules/LegacyComponents/LegacyComponents/PGExposureTool.m new file mode 100644 index 0000000000..638d38c75e --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGExposureTool.m @@ -0,0 +1,79 @@ +#import "PGExposureTool.h" + +#import "LegacyComponentsInternal.h" + +@interface PGExposureTool () +{ + PGPhotoProcessPassParameter *_parameter; +} +@end + +@implementation PGExposureTool + +- (instancetype)init +{ + self = [super init]; + if (self != nil) + { + _identifier = @"exposure"; + _type = PGPhotoToolTypeShader; + _order = 10; + + _minimumValue = -100; + _maximumValue = 100; + _defaultValue = 0; + + self.value = @(_defaultValue); + } + return self; +} + +- (NSString *)title +{ + return TGLocalized(@"PhotoEditor.ExposureTool"); +} + +- (bool)shouldBeSkipped +{ + return (ABS(((NSNumber *)self.displayValue).floatValue - (float)self.defaultValue) < FLT_EPSILON); +} + +- (NSArray *)parameters +{ + if (!_parameters) + { + _parameter = [PGPhotoProcessPassParameter parameterWithName:@"exposure" type:@"lowp float"]; + _parameters = @[ _parameter ]; + } + + return _parameters; +} + +- (void)updateParameters +{ + NSNumber *value = (NSNumber *)self.displayValue; + + CGFloat parameterValue = (value.floatValue / 100.0f); + [_parameter setFloatValue:parameterValue]; +} + +- (NSString *)shaderString +{ + return PGShaderString + ( + if (abs(exposure) > toolEpsilon) { + mediump float mag = exposure * 1.045; + mediump float exppower = 1.0 + abs(mag); + + if (mag < 0.0) { + exppower = 1.0 / exppower; + } + + result.r = 1.0 - pow((1.0 - result.r), exppower); + result.g = 1.0 - pow((1.0 - result.g), exppower); + result.b = 1.0 - pow((1.0 - result.b), exppower); + } + ); +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGFadeTool.h b/submodules/LegacyComponents/LegacyComponents/PGFadeTool.h new file mode 100644 index 0000000000..b3db968425 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGFadeTool.h @@ -0,0 +1,5 @@ +#import "PGPhotoTool.h" + +@interface PGFadeTool : PGPhotoTool + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGFadeTool.m b/submodules/LegacyComponents/LegacyComponents/PGFadeTool.m new file mode 100644 index 0000000000..ad07744ea5 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGFadeTool.m @@ -0,0 +1,96 @@ +#import "PGFadeTool.h" + +#import "LegacyComponentsInternal.h" + +@interface PGFadeTool () +{ + PGPhotoProcessPassParameter *_parameter; +} +@end + +@implementation PGFadeTool + +- (instancetype)init +{ + self = [super init]; + if (self != nil) + { + _identifier = @"fade"; + _type = PGPhotoToolTypeShader; + _order = 7; + + _minimumValue = 0; + _maximumValue = 100; + _defaultValue = 0; + + self.value = @(_defaultValue); + } + return self; +} + +- (NSString *)title +{ + return TGLocalized(@"PhotoEditor.FadeTool"); +} + +- (bool)shouldBeSkipped +{ + return (ABS(((NSNumber *)self.displayValue).floatValue - self.defaultValue) < FLT_EPSILON); +} + +- (NSArray *)parameters +{ + if (!_parameters) + { + _parameter = [PGPhotoProcessPassParameter parameterWithName:@"fadeAmount" type:@"lowp float"]; + _parameters = @[ _parameter ]; + } + + return _parameters; +} + +- (void)updateParameters +{ + NSNumber *value = (NSNumber *)self.displayValue; + + CGFloat parameterValue = value.floatValue / 100.0f; + [_parameter setFloatValue:parameterValue]; +} + +- (NSString *)ancillaryShaderString +{ + return PGShaderString + ( + highp vec3 fadeAdjust(highp vec3 color, highp float fadeVal) { + highp vec3 co1 = vec3(-0.9772); + highp vec3 co2 = vec3(1.708); + highp vec3 co3 = vec3(-0.1603); + highp vec3 co4 = vec3(0.2878); + + highp vec3 comp1 = co1 * pow(vec3(color), vec3(3.0)); + highp vec3 comp2 = co2 * pow(vec3(color), vec3(2.0)); + highp vec3 comp3 = co3 * vec3(color); + highp vec3 comp4 = co4; + + highp vec3 finalComponent = comp1 + comp2 + comp3 + comp4; + highp vec3 difference = finalComponent - color; + highp vec3 scalingValue = vec3(0.9); + + highp vec3 faded = color + (difference * scalingValue); + + return (color * (1.0 - fadeVal)) + (faded * fadeVal); + } + ); +} + +- (NSString *)shaderString +{ + return PGShaderString + ( + if (abs(fadeAmount) > toolEpsilon) { + result.rgb = fadeAdjust(result.rgb, fadeAmount); + } + ); +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGGrainTool.h b/submodules/LegacyComponents/LegacyComponents/PGGrainTool.h new file mode 100644 index 0000000000..36e3139db9 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGGrainTool.h @@ -0,0 +1,5 @@ +#import "PGPhotoTool.h" + +@interface PGGrainTool : PGPhotoTool + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGGrainTool.m b/submodules/LegacyComponents/LegacyComponents/PGGrainTool.m new file mode 100644 index 0000000000..4ea1ada749 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGGrainTool.m @@ -0,0 +1,158 @@ +#import "PGGrainTool.h" + +#import "LegacyComponentsInternal.h" + +@interface PGGrainTool () +{ + PGPhotoProcessPassParameter *_parameter; +} +@end + +@implementation PGGrainTool + +- (instancetype)init +{ + self = [super init]; + if (self != nil) + { + _identifier = @"grain"; + _type = PGPhotoToolTypeShader; + _order = 12; + + _minimumValue = 0; + _maximumValue = 100; + _defaultValue = 0; + + self.value = @(_defaultValue); + } + return self; +} + +- (NSString *)title +{ + return TGLocalized(@"PhotoEditor.GrainTool"); +} + +- (bool)shouldBeSkipped +{ + return (ABS(((NSNumber *)self.displayValue).floatValue - (float)self.defaultValue) < FLT_EPSILON); +} + +- (NSArray *)parameters +{ + if (!_parameters) + { + _parameter = [PGPhotoProcessPassParameter parameterWithName:@"grain" type:@"lowp float"]; + _parameters = @[ _parameter, + [PGPhotoProcessPassParameter constWithName:@"permTexUnit" type:@"lowp float" value:@"1.0 / 256.0"], + [PGPhotoProcessPassParameter constWithName:@"permTexUnitHalf" type:@"lowp float" value:@"0.5 / 256.0"], + [PGPhotoProcessPassParameter constWithName:@"grainsize" type:@"lowp float" value:@"2.3"] ]; + } + + return _parameters; +} + +- (void)updateParameters +{ + NSNumber *value = (NSNumber *)self.displayValue; + + CGFloat parameterValue = value.floatValue / 100.0f * 0.04f; + [_parameter setFloatValue:parameterValue]; +} + +- (NSString *)ancillaryShaderString +{ + return PGShaderString + ( + highp vec4 rnm(in highp vec2 tc) { + highp float noise = sin(dot(tc,vec2(12.9898,78.233))) * 43758.5453; + + highp float noiseR = fract(noise)*2.0-1.0; + highp float noiseG = fract(noise*1.2154)*2.0-1.0; + highp float noiseB = fract(noise*1.3453)*2.0-1.0; + highp float noiseA = fract(noise*1.3647)*2.0-1.0; + + return vec4(noiseR,noiseG,noiseB,noiseA); + } + + highp float fade(in highp float t) { + return t*t*t*(t*(t*6.0-15.0)+10.0); + } + + highp float pnoise3D(in highp vec3 p) + { + highp vec3 pi = permTexUnit*floor(p)+permTexUnitHalf; + highp vec3 pf = fract(p); + + // Noise contributions from (x=0, y=0), z=0 and z=1 + highp float perm00 = rnm(pi.xy).a ; + highp vec3 grad000 = rnm(vec2(perm00, pi.z)).rgb * 4.0 - 1.0; + highp float n000 = dot(grad000, pf); + highp vec3 grad001 = rnm(vec2(perm00, pi.z + permTexUnit)).rgb * 4.0 - 1.0; + highp float n001 = dot(grad001, pf - vec3(0.0, 0.0, 1.0)); + + // Noise contributions from (x=0, y=1), z=0 and z=1 + highp float perm01 = rnm(pi.xy + vec2(0.0, permTexUnit)).a ; + highp vec3 grad010 = rnm(vec2(perm01, pi.z)).rgb * 4.0 - 1.0; + highp float n010 = dot(grad010, pf - vec3(0.0, 1.0, 0.0)); + highp vec3 grad011 = rnm(vec2(perm01, pi.z + permTexUnit)).rgb * 4.0 - 1.0; + highp float n011 = dot(grad011, pf - vec3(0.0, 1.0, 1.0)); + + // Noise contributions from (x=1, y=0), z=0 and z=1 + highp float perm10 = rnm(pi.xy + vec2(permTexUnit, 0.0)).a ; + highp vec3 grad100 = rnm(vec2(perm10, pi.z)).rgb * 4.0 - 1.0; + highp float n100 = dot(grad100, pf - vec3(1.0, 0.0, 0.0)); + highp vec3 grad101 = rnm(vec2(perm10, pi.z + permTexUnit)).rgb * 4.0 - 1.0; + highp float n101 = dot(grad101, pf - vec3(1.0, 0.0, 1.0)); + + // Noise contributions from (x=1, y=1), z=0 and z=1 + highp float perm11 = rnm(pi.xy + vec2(permTexUnit, permTexUnit)).a ; + highp vec3 grad110 = rnm(vec2(perm11, pi.z)).rgb * 4.0 - 1.0; + highp float n110 = dot(grad110, pf - vec3(1.0, 1.0, 0.0)); + highp vec3 grad111 = rnm(vec2(perm11, pi.z + permTexUnit)).rgb * 4.0 - 1.0; + highp float n111 = dot(grad111, pf - vec3(1.0, 1.0, 1.0)); + + // Blend contributions along x + highp vec4 n_x = mix(vec4(n000, n001, n010, n011), vec4(n100, n101, n110, n111), fade(pf.x)); + + // Blend contributions along y + highp vec2 n_xy = mix(n_x.xy, n_x.zw, fade(pf.y)); + + // Blend contributions along z + highp float n_xyz = mix(n_xy.x, n_xy.y, fade(pf.z)); + + return n_xyz; + } + + lowp vec2 coordRot(in lowp vec2 tc, in lowp float angle) + { + lowp float rotX = ((tc.x * 2.0 - 1.0) * cos(angle)) - ((tc.y * 2.0 - 1.0) * sin(angle)); + lowp float rotY = ((tc.y * 2.0 - 1.0) * cos(angle)) + ((tc.x * 2.0 - 1.0) * sin(angle)); + rotX = rotX * 0.5 + 0.5; + rotY = rotY * 0.5 + 0.5; + return vec2(rotX,rotY); + } + ); +} + +- (NSString *)shaderString +{ + return PGShaderString + ( + if (abs(grain) > toolEpsilon) { + highp vec3 rotOffset = vec3(1.425, 3.892, 5.835); + highp vec2 rotCoordsR = coordRot(texCoord, rotOffset.x); + highp vec3 noise = vec3(pnoise3D(vec3(rotCoordsR * vec2(width / grainsize, height / grainsize),0.0))); + + lowp vec3 lumcoeff = vec3(0.299,0.587,0.114); + lowp float luminance = dot(result.rgb, lumcoeff); + lowp float lum = smoothstep(0.2, 0.0, luminance); + lum += luminance; + + noise = mix(noise,vec3(0.0),pow(lum,4.0)); + result.rgb = result.rgb + noise * grain; + } + ); +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGHighlightsTool.h b/submodules/LegacyComponents/LegacyComponents/PGHighlightsTool.h new file mode 100644 index 0000000000..47f361beee --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGHighlightsTool.h @@ -0,0 +1,5 @@ +#import "PGPhotoTool.h" + +@interface PGHighlightsTool : PGPhotoTool + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGHighlightsTool.m b/submodules/LegacyComponents/LegacyComponents/PGHighlightsTool.m new file mode 100644 index 0000000000..488f827c85 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGHighlightsTool.m @@ -0,0 +1,85 @@ +#import "PGHighlightsTool.h" + +#import "LegacyComponentsInternal.h" + +@interface PGHighlightsTool () +{ + PGPhotoProcessPassParameter *_parameter; +} +@end + +@implementation PGHighlightsTool + +- (instancetype)init +{ + self = [super init]; + if (self != nil) + { + _identifier = @"highlights"; + _type = PGPhotoToolTypeShader; + _order = 5; + + _minimumValue = -100; + _maximumValue = 100; + _defaultValue = 0; + + self.value = @(_defaultValue); + } + return self; +} + +- (NSString *)title +{ + return TGLocalized(@"PhotoEditor.HighlightsTool"); +} + +- (bool)shouldBeSkipped +{ + return (ABS(((NSNumber *)self.displayValue).floatValue - self.defaultValue) < FLT_EPSILON); +} + +- (NSArray *)parameters +{ + if (!_parameters) + { + _parameter = [PGPhotoProcessPassParameter parameterWithName:@"highlights" type:@"lowp float"]; + _parameters = @[ [PGPhotoProcessPassParameter constWithName:@"hsLuminanceWeighting" type:@"mediump vec3" value:@"vec3(0.3, 0.3, 0.3)"], + _parameter ]; + } + + return _parameters; +} + +- (void)updateParameters +{ + NSNumber *value = (NSNumber *)self.displayValue; + + CGFloat parameterValue = (value.floatValue * 0.75f + 100.0f) / 100.0f; + [_parameter setFloatValue:parameterValue]; +} + +- (NSString *)shaderString +{ + return PGShaderString + ( + mediump float hsLuminance = dot(result.rgb, hsLuminanceWeighting); + + mediump float shadow = clamp((pow(hsLuminance, 1.0 / shadows) + (-0.76) * pow(hsLuminance, 2.0 / shadows)) - hsLuminance, 0.0, 1.0); + mediump float highlight = clamp((1.0 - (pow(1.0 - hsLuminance, 1.0 / (2.0 - highlights)) + (-0.8) * pow(1.0 - hsLuminance, 2.0 / (2.0 - highlights)))) - hsLuminance, -1.0, 0.0); + lowp vec3 hsresult = vec3(0.0, 0.0, 0.0) + ((hsLuminance + shadow + highlight) - 0.0) * ((result.rgb - vec3(0.0, 0.0, 0.0)) / (hsLuminance - 0.0)); + + mediump float contrastedLuminance = ((hsLuminance - 0.5) * 1.5) + 0.5; + mediump float whiteInterp = contrastedLuminance * contrastedLuminance * contrastedLuminance; + mediump float whiteTarget = clamp(highlights, 1.0, 2.0) - 1.0; + hsresult = mix(hsresult, vec3(1.0), whiteInterp * whiteTarget); + + mediump float invContrastedLuminance = 1.0 - contrastedLuminance; + mediump float blackInterp = invContrastedLuminance * invContrastedLuminance * invContrastedLuminance; + mediump float blackTarget = 1.0 - clamp(shadows, 0.0, 1.0); + hsresult = mix(hsresult, vec3(0.0), blackInterp * blackTarget); + + result = vec4(hsresult.rgb, result.a); + ); +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGPhotoBlurPass.h b/submodules/LegacyComponents/LegacyComponents/PGPhotoBlurPass.h new file mode 100644 index 0000000000..44f7b13a2a --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGPhotoBlurPass.h @@ -0,0 +1,18 @@ +#import "PGPhotoProcessPass.h" + +typedef enum +{ + PGBlurToolTypeNone, + PGBlurToolTypeRadial, + PGBlurToolTypeLinear +} PGBlurToolType; + +@interface PGPhotoBlurPass : PGPhotoProcessPass + +@property (nonatomic, assign) PGBlurToolType type; +@property (nonatomic, assign) CGFloat size; +@property (nonatomic, assign) CGFloat falloff; +@property (nonatomic, assign) CGPoint point; +@property (nonatomic, assign) CGFloat angle; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGPhotoBlurPass.m b/submodules/LegacyComponents/LegacyComponents/PGPhotoBlurPass.m new file mode 100644 index 0000000000..50e5ef9b7e --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGPhotoBlurPass.m @@ -0,0 +1,277 @@ +#import "PGPhotoBlurPass.h" + +#import "GPUImageTwoInputFilter.h" +#import "PGPhotoGaussianBlurFilter.h" + +NSString *const PGPhotoRadialBlurShaderString = PGShaderString +( + varying highp vec2 texCoord; + varying highp vec2 texCoord2; + + uniform sampler2D sourceImage; + uniform sampler2D inputImageTexture2; + + uniform lowp float excludeSize; + uniform lowp vec2 excludePoint; + uniform lowp float excludeFalloff; + uniform highp float aspectRatio; + + void main() + { + lowp vec4 sharpImageColor = texture2D(sourceImage, texCoord); + lowp vec4 blurredImageColor = texture2D(inputImageTexture2, texCoord2); + + highp vec2 texCoordToUse = vec2(texCoord2.x, (texCoord2.y * aspectRatio + 0.5 - 0.5 * aspectRatio)); + highp float distanceFromCenter = distance(excludePoint, texCoordToUse); + + gl_FragColor = mix(blurredImageColor, sharpImageColor, smoothstep(1.0, excludeFalloff, clamp(distanceFromCenter / excludeSize, 0.0, 1.0))); + } +); + +NSString *const PGPhotoLinearBlurShaderString = PGShaderString +( + varying highp vec2 texCoord; + varying highp vec2 texCoord2; + + uniform sampler2D sourceImage; + uniform sampler2D inputImageTexture2; + + uniform lowp float excludeSize; + uniform lowp vec2 excludePoint; + uniform lowp float excludeFalloff; + uniform highp float angle; + uniform highp float aspectRatio; + + void main() + { + lowp vec4 sharpImageColor = texture2D(sourceImage, texCoord); + lowp vec4 blurredImageColor = texture2D(inputImageTexture2, texCoord2); + + highp vec2 texCoordToUse = vec2(texCoord2.x, (texCoord2.y * aspectRatio + 0.5 - 0.5 * aspectRatio)); + highp float distanceFromCenter = abs((texCoordToUse.x - excludePoint.x) * cos(angle) + (texCoordToUse.y - excludePoint.y) * sin(angle)); + + gl_FragColor = mix(blurredImageColor, sharpImageColor, smoothstep(1.0, excludeFalloff, clamp(distanceFromCenter / excludeSize, 0.0, 1.0))); + } +); + +@interface PGPhotoBlurFilter : GPUImageOutput +{ + PGPhotoGaussianBlurFilter *_blurFilter; + + GPUImageTwoInputFilter *_radialFocusFilter; + GPUImageTwoInputFilter *_linearFocusFilter; + + GPUImageOutput *_currentFocusFilter; + + bool _endProcessing; +} +@end + +@implementation PGPhotoBlurFilter + +- (instancetype)init +{ + self = [super init]; + if (self != nil) + { + _blurFilter = [[PGPhotoGaussianBlurFilter alloc] init]; + + _radialFocusFilter = [[GPUImageTwoInputFilter alloc] initWithFragmentShaderFromString:PGPhotoRadialBlurShaderString]; + _linearFocusFilter = [[GPUImageTwoInputFilter alloc] initWithFragmentShaderFromString:PGPhotoLinearBlurShaderString]; + } + return self; +} + +- (void)setType:(PGBlurToolType)type +{ + id target = nil; + if (_currentFocusFilter.targets.count > 0) + target = _currentFocusFilter.targets[0]; + + [_currentFocusFilter removeAllTargets]; + + switch (type) + { + case PGBlurToolTypeRadial: + { + _currentFocusFilter = _radialFocusFilter; + } + break; + + case PGBlurToolTypeLinear: + { + _currentFocusFilter = _linearFocusFilter; + } + break; + + default: + break; + } + + if (target != nil) + [_currentFocusFilter addTarget:target atTextureLocation:0]; + + [_blurFilter removeAllTargets]; + [_blurFilter addTarget:_currentFocusFilter atTextureLocation:1]; +} + +- (void)setExcludeSize:(CGFloat)excludeSize +{ + [_radialFocusFilter setFloat:(float)excludeSize forUniformName:@"excludeSize"]; + [_linearFocusFilter setFloat:(float)excludeSize forUniformName:@"excludeSize"]; +} + +- (void)setExcludeFalloff:(CGFloat)excludeFalloff +{ + [_radialFocusFilter setFloat:(float)excludeFalloff forUniformName:@"excludeFalloff"]; + [_linearFocusFilter setFloat:(float)excludeFalloff forUniformName:@"excludeFalloff"]; +} + +- (void)setExcludePoint:(CGPoint)excludePoint +{ + [_radialFocusFilter setPoint:excludePoint forUniformName:@"excludePoint"]; + [_linearFocusFilter setPoint:excludePoint forUniformName:@"excludePoint"]; +} + +- (void)setAngle:(CGFloat)angle +{ + [_linearFocusFilter setFloat:(float)angle forUniformName:@"angle"]; +} + +#pragma mark GPUImageOutput + +- (void)setTargetToIgnoreForUpdates:(id)targetToIgnoreForUpdates +{ + [_currentFocusFilter setTargetToIgnoreForUpdates:targetToIgnoreForUpdates]; +} + +- (void)addTarget:(id)newTarget atTextureLocation:(NSInteger)textureLocation +{ + [_currentFocusFilter addTarget:newTarget atTextureLocation:textureLocation]; +} + +- (void)removeTarget:(id)targetToRemove +{ + [_currentFocusFilter removeTarget:targetToRemove]; +} + +- (void)removeAllTargets +{ + [_currentFocusFilter removeAllTargets]; +} + +- (void)setFrameProcessingCompletionBlock:(void (^)(GPUImageOutput *, CMTime))frameProcessingCompletionBlock +{ + [_currentFocusFilter setFrameProcessingCompletionBlock:frameProcessingCompletionBlock]; +} + +- (void (^)(GPUImageOutput *, CMTime))frameProcessingCompletionBlock +{ + return [_currentFocusFilter frameProcessingCompletionBlock]; +} + +- (void)newFrameReadyAtTime:(CMTime)frameTime atIndex:(NSInteger)__unused textureIndex +{ + [_blurFilter newFrameReadyAtTime:frameTime atIndex:0]; + [_currentFocusFilter newFrameReadyAtTime:frameTime atIndex:0]; +} + +- (void)setInputFramebuffer:(GPUImageFramebuffer *)newInputFramebuffer atIndex:(NSInteger)__unused textureIndex +{ + [_blurFilter setInputFramebuffer:newInputFramebuffer atIndex:0]; + [_currentFocusFilter setInputFramebuffer:newInputFramebuffer atIndex:0]; +} + +- (NSInteger)nextAvailableTextureIndex +{ + return 0; +} + +- (void)setInputSize:(CGSize)newSize atIndex:(NSInteger)textureIndex +{ + [_blurFilter setInputSize:newSize atIndex:textureIndex]; + [_radialFocusFilter setInputSize:newSize atIndex:textureIndex]; + [_linearFocusFilter setInputSize:newSize atIndex:textureIndex]; + + CGFloat aspectRatio = newSize.height / newSize.width; + [_radialFocusFilter setFloat:(float)aspectRatio forUniformName:@"aspectRatio"]; + [_linearFocusFilter setFloat:(float)aspectRatio forUniformName:@"aspectRatio"]; +} + +- (void)setInputRotation:(GPUImageRotationMode)newInputRotation atIndex:(NSInteger)textureIndex +{ + [_blurFilter setInputRotation:newInputRotation atIndex:textureIndex]; + [_radialFocusFilter setInputRotation:newInputRotation atIndex:textureIndex]; + [_linearFocusFilter setInputRotation:newInputRotation atIndex:textureIndex]; +} + +- (CGSize)maximumOutputSize +{ + return CGSizeZero; +} + +- (void)endProcessing +{ + if (!_endProcessing) + { + _endProcessing = true; + [_currentFocusFilter endProcessing]; + } +} + +- (BOOL)wantsMonochromeInput +{ + return false; +} + +- (void)setCurrentlyReceivingMonochromeInput:(BOOL)__unused newValue +{ + +} + +@end + +@implementation PGPhotoBlurPass + +- (instancetype)init +{ + self = [super init]; + if (self != nil) + { + PGPhotoBlurFilter *filter = [[PGPhotoBlurFilter alloc] init]; + _filter = filter; + } + return self; +} + +- (void)setType:(PGBlurToolType)type +{ + _type = type; + [(PGPhotoBlurFilter *)_filter setType:type]; +} + +- (void)setSize:(CGFloat)size +{ + _size = size; + [(PGPhotoBlurFilter *)_filter setExcludeSize:size]; +} + +- (void)setFalloff:(CGFloat)falloff +{ + _falloff = falloff; + [(PGPhotoBlurFilter *)_filter setExcludeFalloff:falloff]; +} + +- (void)setPoint:(CGPoint)point +{ + _point = point; + [(PGPhotoBlurFilter *)_filter setExcludePoint:point]; +} + +- (void)setAngle:(CGFloat)angle +{ + _angle = angle; + [(PGPhotoBlurFilter *)_filter setAngle:angle + (CGFloat)M_PI_2]; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGPhotoCustomFilterPass.h b/submodules/LegacyComponents/LegacyComponents/PGPhotoCustomFilterPass.h new file mode 100644 index 0000000000..8a493a4b0b --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGPhotoCustomFilterPass.h @@ -0,0 +1,13 @@ +#import "PGPhotoProcessPass.h" + +@class TGPhotoEditingContext; + +@interface PGPhotoCustomFilterPass : PGPhotoProcessPass + +@property (nonatomic, assign) CGFloat intensity; + +- (instancetype)initWithShaderFile:(NSString *)shaderFile textureFiles:(NSArray *)textureFiles; +- (instancetype)initWithShaderFile:(NSString *)shaderFile textureFiles:(NSArray *)textureFiles optimized:(bool)optimized; +- (instancetype)initWithShaderString:(NSString *)shaderString textureImages:(NSArray *)textureImages; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGPhotoCustomFilterPass.m b/submodules/LegacyComponents/LegacyComponents/PGPhotoCustomFilterPass.m new file mode 100644 index 0000000000..bd47f4a682 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGPhotoCustomFilterPass.m @@ -0,0 +1,429 @@ +#import "PGPhotoCustomFilterPass.h" + +#import "PGPhotoEditorPicture.h" + +#import "LegacyComponentsInternal.h" + +NSString *const PGPhotoFilterDefinitionsShaderString = PGShaderString +( + precision highp float; + + varying vec2 texCoord; + uniform sampler2D sourceImage; + uniform float intensity; +); + +NSString *const PGPhotoFilterMainShaderString = PGShaderString +( + void main() { + vec4 texel = texture2D(sourceImage, texCoord); + vec4 result = filter(texel); + + gl_FragColor = vec4(mix(texel.rgb, result.rgb, intensity), texel.a); + } +); + +@interface PGPhotoCustomFilter : GPUImageFilter +{ + GLint _intensityUniform; + + GLuint _filterSourceTexture2; + GLuint _filterSourceTexture3; + GLuint _filterSourceTexture4; + GLuint _filterSourceTexture5; + GLuint _filterSourceTexture6; + + GLint _filterInputTextureUniform2; + GLint _filterInputTextureUniform3; + GLint _filterInputTextureUniform4; + GLint _filterInputTextureUniform5; + GLint _filterInputTextureUniform6; +} + +@property (nonatomic, assign) CGFloat intensity; + +@end + +@implementation PGPhotoCustomFilter + +- (instancetype)initWithFragmentShaderFromString:(NSString *)fragmentShaderString +{ + self = [super initWithFragmentShaderFromString:fragmentShaderString]; + if (self != nil) + { + _intensityUniform = [filterProgram uniformIndex:@"intensity"]; + self.intensity = 1.0f; + + _filterInputTextureUniform2 = [filterProgram uniformIndex:@"inputImageTexture2"]; + _filterInputTextureUniform3 = [filterProgram uniformIndex:@"inputImageTexture3"]; + _filterInputTextureUniform4 = [filterProgram uniformIndex:@"inputImageTexture4"]; + _filterInputTextureUniform5 = [filterProgram uniformIndex:@"inputImageTexture5"]; + _filterInputTextureUniform6 = [filterProgram uniformIndex:@"inputImageTexture6"]; + } + return self; +} + +- (void)dealloc +{ + runAsynchronouslyOnVideoProcessingQueue(^ + { + if (_filterSourceTexture2) + glDeleteTextures(1, &_filterSourceTexture2); + + if (_filterSourceTexture3) + glDeleteTextures(1, &_filterSourceTexture3); + + if (_filterSourceTexture4) + glDeleteTextures(1, &_filterSourceTexture4); + + if (_filterSourceTexture5) + glDeleteTextures(1, &_filterSourceTexture5); + + if (_filterSourceTexture6) + glDeleteTextures(1, &_filterSourceTexture6); + }); +} + +- (void)addTextureWithImage:(UIImage *)image textureIndex:(NSInteger)textureIndex +{ + bool redrawNeeded = false; + CGImageRef cgImage = image.CGImage; + + if (image.imageOrientation != UIImageOrientationUp) + redrawNeeded = true; + + CGSize imageSize = CGSizeMake(CGImageGetWidth(cgImage), CGImageGetHeight(cgImage)); + if (image.imageOrientation == UIImageOrientationLeft || image.imageOrientation == UIImageOrientationRight) + imageSize = CGSizeMake(imageSize.height, imageSize.width); + + GLubyte *imageData = NULL; + CFDataRef dataFromImageDataProvider = NULL; + GLenum format = GL_BGRA; + + if (!redrawNeeded) + { + if (CGImageGetBytesPerRow(cgImage) != CGImageGetWidth(cgImage) * 4 || + CGImageGetBitsPerPixel(cgImage) != 32 || + CGImageGetBitsPerComponent(cgImage) != 8) + { + redrawNeeded = true; + } + else + { + CGBitmapInfo bitmapInfo = CGImageGetBitmapInfo(cgImage); + if ((bitmapInfo & kCGBitmapFloatComponents) != 0) + { + redrawNeeded = true; + } + else + { + CGBitmapInfo byteOrderInfo = bitmapInfo & kCGBitmapByteOrderMask; + if (byteOrderInfo == kCGBitmapByteOrder32Little) + { + /* Little endian, for alpha-first we can use this bitmap directly in GL */ + CGImageAlphaInfo alphaInfo = bitmapInfo & kCGBitmapAlphaInfoMask; + if (alphaInfo != kCGImageAlphaPremultipliedFirst && alphaInfo != kCGImageAlphaFirst && + alphaInfo != kCGImageAlphaNoneSkipFirst) + { + redrawNeeded = true; + } + } + else if (byteOrderInfo == kCGBitmapByteOrderDefault || byteOrderInfo == kCGBitmapByteOrder32Big) + { + /* Big endian, for alpha-last we can use this bitmap directly in GL */ + CGImageAlphaInfo alphaInfo = bitmapInfo & kCGBitmapAlphaInfoMask; + if (alphaInfo != kCGImageAlphaPremultipliedLast && alphaInfo != kCGImageAlphaLast && + alphaInfo != kCGImageAlphaNoneSkipLast) + { + redrawNeeded = true; + } else + { + /* Can access directly using GL_RGBA pixel format */ + format = GL_RGBA; + } + } + } + } + } + + if (redrawNeeded) + { + imageData = (GLubyte *) calloc(1, (int)imageSize.width * (int)imageSize.height * 4); + + CGColorSpaceRef genericRGBColorspace = CGColorSpaceCreateDeviceRGB(); + + CGContextRef imageContext = CGBitmapContextCreate(imageData, (size_t)imageSize.width, (size_t)imageSize.height, 8, (size_t)imageSize.width * 4, genericRGBColorspace, kCGBitmapByteOrder32Little | kCGImageAlphaPremultipliedFirst); + + CGSize imageDrawSize = CGSizeMake(imageSize.width, imageSize.height); + if (image.imageOrientation == UIImageOrientationLeft || image.imageOrientation == UIImageOrientationRight) + imageDrawSize = CGSizeMake(imageDrawSize.height, imageDrawSize.width); + + CGAffineTransform transform = CGAffineTransformIdentity; + switch (image.imageOrientation) + { + case UIImageOrientationDown: + case UIImageOrientationDownMirrored: + transform = CGAffineTransformTranslate(transform, imageSize.width, imageSize.height); + transform = CGAffineTransformRotate(transform, (CGFloat)M_PI); + break; + + case UIImageOrientationLeft: + case UIImageOrientationLeftMirrored: + transform = CGAffineTransformTranslate(transform, imageSize.width, 0); + transform = CGAffineTransformRotate(transform, (CGFloat)M_PI_2); + break; + + case UIImageOrientationRight: + case UIImageOrientationRightMirrored: + transform = CGAffineTransformTranslate(transform, 0, imageSize.height); + transform = CGAffineTransformRotate(transform, (CGFloat)-M_PI_2); + break; + + default: + break; + } + + switch (image.imageOrientation) + { + case UIImageOrientationUpMirrored: + case UIImageOrientationDownMirrored: + transform = CGAffineTransformTranslate(transform, imageSize.width,0); + transform = CGAffineTransformScale(transform, -1, 1); + break; + + case UIImageOrientationLeftMirrored: + case UIImageOrientationRightMirrored: + transform = CGAffineTransformTranslate(transform, imageSize.height, 0); + transform = CGAffineTransformScale(transform, -1, 1); + break; + + default: + break; + } + + CGContextConcatCTM(imageContext, transform); + + CGContextDrawImage(imageContext, CGRectMake(0.0f, 0.0f, imageDrawSize.width, imageDrawSize.height), cgImage); + CGContextRelease(imageContext); + CGColorSpaceRelease(genericRGBColorspace); + } + else + { + dataFromImageDataProvider = CGDataProviderCopyData(CGImageGetDataProvider(cgImage)); + imageData = (GLubyte *)CFDataGetBytePtr(dataFromImageDataProvider); + } + + runSynchronouslyOnVideoProcessingQueue(^ + { + [GPUImageContext useImageProcessingContext]; + + switch (textureIndex) + { + case 0: + { + glActiveTexture(GL_TEXTURE3); + glGenTextures(1, &_filterSourceTexture2); + glBindTexture(GL_TEXTURE_2D, _filterSourceTexture2); + } + break; + + case 1: + { + glActiveTexture(GL_TEXTURE4); + glGenTextures(1, &_filterSourceTexture3); + glBindTexture(GL_TEXTURE_2D, _filterSourceTexture3); + } + break; + + case 2: + { + glActiveTexture(GL_TEXTURE5); + glGenTextures(1, &_filterSourceTexture4); + glBindTexture(GL_TEXTURE_2D, _filterSourceTexture4); + } + break; + + case 3: + { + glActiveTexture(GL_TEXTURE6); + glGenTextures(1, &_filterSourceTexture5); + glBindTexture(GL_TEXTURE_2D, _filterSourceTexture5); + break; + } + + case 4: + { + glActiveTexture(GL_TEXTURE7); + glGenTextures(1, &_filterSourceTexture6); + glBindTexture(GL_TEXTURE_2D, _filterSourceTexture6); + break; + } + + default: + break; + } + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, (GLsizei)imageSize.width, (GLsizei)imageSize.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, imageData); + + if (redrawNeeded) + free(imageData); + else if (dataFromImageDataProvider) + CFRelease(dataFromImageDataProvider); + }); +} + +- (void)renderToTextureWithVertices:(const GLfloat *)vertices textureCoordinates:(const GLfloat *)textureCoordinates +{ + if (self.preventRendering) + { + [firstInputFramebuffer unlock]; + return; + } + + [GPUImageContext setActiveShaderProgram:filterProgram]; + + outputFramebuffer = [[GPUImageContext sharedFramebufferCache] fetchFramebufferForSize:[self sizeOfFBO] textureOptions:self.outputTextureOptions onlyTexture:false]; + [outputFramebuffer activateFramebuffer]; + if (usingNextFrameForImageCapture) + { + [outputFramebuffer lock]; + } + + [self setUniformsForProgramAtIndex:0]; + + glClearColor(backgroundColorRed, backgroundColorGreen, backgroundColorBlue, backgroundColorAlpha); + glClear(GL_COLOR_BUFFER_BIT); + + glActiveTexture(GL_TEXTURE2); + glBindTexture(GL_TEXTURE_2D, [firstInputFramebuffer texture]); + glUniform1i(filterInputTextureUniform, 2); + + if (_filterSourceTexture2) + { + glActiveTexture(GL_TEXTURE3); + glBindTexture(GL_TEXTURE_2D, _filterSourceTexture2); + glUniform1i(_filterInputTextureUniform2, 3); + } + + if (_filterSourceTexture3) + { + glActiveTexture(GL_TEXTURE4); + glBindTexture(GL_TEXTURE_2D, _filterSourceTexture3); + glUniform1i(_filterInputTextureUniform3, 4); + } + + if (_filterSourceTexture4) + { + glActiveTexture(GL_TEXTURE5); + glBindTexture(GL_TEXTURE_2D, _filterSourceTexture4); + glUniform1i(_filterInputTextureUniform4, 5); + } + + if (_filterSourceTexture5) + { + glActiveTexture(GL_TEXTURE6); + glBindTexture(GL_TEXTURE_2D, _filterSourceTexture5); + glUniform1i(_filterInputTextureUniform5, 6); + } + + if (_filterSourceTexture6) + { + glActiveTexture(GL_TEXTURE7); + glBindTexture(GL_TEXTURE_2D, _filterSourceTexture6); + glUniform1i(_filterInputTextureUniform6, 7); + } + + glVertexAttribPointer(filterPositionAttribute, 2, GL_FLOAT, 0, 0, vertices); + glVertexAttribPointer(filterTextureCoordinateAttribute, 2, GL_FLOAT, 0, 0, textureCoordinates); + + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); + + [firstInputFramebuffer unlock]; + + if (usingNextFrameForImageCapture) + dispatch_semaphore_signal(imageCaptureSemaphore); +} + +- (void)setIntensity:(CGFloat)intensity +{ + _intensity = intensity; + + [self setFloat:(float)_intensity forUniform:_intensityUniform program:filterProgram]; +} + +@end + +@implementation PGPhotoCustomFilterPass + +@dynamic intensity; + +- (instancetype)initWithShaderFile:(NSString *)shaderFile textureFiles:(NSArray *)textureFiles +{ + return [self initWithShaderFile:shaderFile textureFiles:textureFiles optimized:false]; +} + +- (instancetype)initWithShaderFile:(NSString *)shaderFile textureFiles:(NSArray *)textureFiles optimized:(bool)optimized +{ + NSString *fragmentShaderPathname = TGComponentsPathForResource(shaderFile, @"fsh"); + NSString *fragmentShaderString = [NSString stringWithContentsOfFile:fragmentShaderPathname encoding:NSUTF8StringEncoding error:nil]; + + NSMutableArray *textureImages = [[NSMutableArray alloc] init]; + for (id textureDefinition in textureFiles) + { + NSString *textureFile = nil; + if ([textureDefinition isKindOfClass:[NSString class]]) + { + textureFile = (NSString *)textureDefinition; + } + else if ([textureDefinition isKindOfClass:[NSArray class]]) + { + NSArray *textureDefinitions = (NSArray *)textureDefinition; + textureFile = optimized ? textureDefinitions.lastObject : textureDefinitions.firstObject; + } + + NSString *name = [[textureFile lastPathComponent] stringByDeletingPathExtension]; + NSString *extension = [textureFile pathExtension]; + + NSString *texturePathname = TGComponentsPathForResource(name, extension); + UIImage *textureImage = [UIImage imageWithContentsOfFile:texturePathname]; + + [textureImages addObject:textureImage]; + } + + return [self initWithShaderString:fragmentShaderString textureImages:textureImages]; +} + +- (instancetype)initWithShaderString:(NSString *)shaderString textureImages:(NSArray *)textureImages +{ + self = [super init]; + if (self != nil) + { + NSMutableString *fullShaderString = [[NSMutableString alloc] initWithString:PGPhotoFilterDefinitionsShaderString]; + [fullShaderString appendString:shaderString]; + [fullShaderString appendString:PGPhotoFilterMainShaderString]; + + PGPhotoCustomFilter *filter = [[PGPhotoCustomFilter alloc] initWithFragmentShaderFromString:fullShaderString]; + + NSInteger index = 0; + for (UIImage *image in textureImages) + { + [filter addTextureWithImage:image textureIndex:index]; + index++; + } + + _filter = filter; + } + return self; +} + +- (void)setIntensity:(CGFloat)intensity +{ + [(PGPhotoCustomFilter *)_filter setIntensity:intensity]; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGPhotoEditor.h b/submodules/LegacyComponents/LegacyComponents/PGPhotoEditor.h new file mode 100644 index 0000000000..ee9f09daf1 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGPhotoEditor.h @@ -0,0 +1,50 @@ +#import +#import + +#import + +@class TGPhotoEditorPreviewView; +@class TGPaintingData; + +@interface PGPhotoEditor : NSObject + +@property (nonatomic, assign) CGSize originalSize; +@property (nonatomic, assign) CGRect cropRect; +@property (nonatomic, readonly) CGSize rotatedCropSize; +@property (nonatomic, assign) CGFloat cropRotation; +@property (nonatomic, assign) UIImageOrientation cropOrientation; +@property (nonatomic, assign) CGFloat cropLockedAspectRatio; +@property (nonatomic, assign) bool cropMirrored; +@property (nonatomic, strong) TGPaintingData *paintingData; +@property (nonatomic, assign) NSTimeInterval trimStartValue; +@property (nonatomic, assign) NSTimeInterval trimEndValue; +@property (nonatomic, assign) bool sendAsGif; +@property (nonatomic, assign) TGMediaVideoConversionPreset preset; + +@property (nonatomic, weak) TGPhotoEditorPreviewView *previewOutput; +@property (nonatomic, readonly) NSArray *tools; + +@property (nonatomic, readonly) bool processing; +@property (nonatomic, readonly) bool readyForProcessing; + +@property (nonatomic, readonly) bool enableStickers; + +- (instancetype)initWithOriginalSize:(CGSize)originalSize adjustments:(id)adjustments forVideo:(bool)forVideo enableStickers:(bool)enableStickers; + +- (void)cleanup; + +- (void)setImage:(UIImage *)image forCropRect:(CGRect)cropRect cropRotation:(CGFloat)cropRotation cropOrientation:(UIImageOrientation)cropOrientation cropMirrored:(bool)cropMirrored fullSize:(bool)fullSize; + +- (void)processAnimated:(bool)animated completion:(void (^)(void))completion; + +- (void)createResultImageWithCompletion:(void (^)(UIImage *image))completion; +- (UIImage *)currentResultImage; + +- (bool)hasDefaultCropping; + +- (SSignal *)histogramSignal; + +- (id)exportAdjustments; +- (id)exportAdjustmentsWithPaintingData:(TGPaintingData *)paintingData; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGPhotoEditor.m b/submodules/LegacyComponents/LegacyComponents/PGPhotoEditor.m new file mode 100644 index 0000000000..20bd9f2466 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGPhotoEditor.m @@ -0,0 +1,398 @@ +#import "PGPhotoEditor.h" + +#import +#import + +#import "LegacyComponentsInternal.h" + +#import +#import "TGPhotoEditorPreviewView.h" +#import "PGPhotoEditorView.h" +#import "PGPhotoEditorPicture.h" + +#import +#import +#import + +#import "PGPhotoToolComposer.h" +#import "PGEnhanceTool.h" +#import "PGExposureTool.h" +#import "PGContrastTool.h" +#import "PGWarmthTool.h" +#import "PGSaturationTool.h" +#import "PGHighlightsTool.h" +#import "PGShadowsTool.h" +#import "PGVignetteTool.h" +#import "PGGrainTool.h" +#import "PGBlurTool.h" +#import "PGSharpenTool.h" +#import "PGFadeTool.h" +#import "PGTintTool.h" +#import "PGCurvesTool.h" + +#import "PGPhotoHistogramGenerator.h" + +@interface PGPhotoEditor () +{ + PGPhotoToolComposer *_toolComposer; + + id _initialAdjustments; + + PGPhotoEditorPicture *_currentInput; + NSArray *_currentProcessChain; + GPUImageOutput *_finalFilter; + + PGPhotoHistogram *_currentHistogram; + PGPhotoHistogramGenerator *_histogramGenerator; + + UIImageOrientation _imageCropOrientation; + CGRect _imageCropRect; + CGFloat _imageCropRotation; + bool _imageCropMirrored; + + SPipe *_histogramPipe; + + SQueue *_queue; + + bool _forVideo; + + bool _processing; + bool _needsReprocessing; + + bool _fullSize; +} +@end + +@implementation PGPhotoEditor + +- (instancetype)initWithOriginalSize:(CGSize)originalSize adjustments:(id)adjustments forVideo:(bool)forVideo enableStickers:(bool)enableStickers +{ + self = [super init]; + if (self != nil) + { + _queue = [[SQueue alloc] init]; + + _forVideo = forVideo; + _enableStickers = enableStickers; + + _originalSize = originalSize; + _cropRect = CGRectMake(0.0f, 0.0f, _originalSize.width, _originalSize.height); + _paintingData = adjustments.paintingData; + + _tools = [self toolsInit]; + _toolComposer = [[PGPhotoToolComposer alloc] init]; + [_toolComposer addPhotoTools:_tools]; + [_toolComposer compose]; + + _histogramPipe = [[SPipe alloc] init]; + + __weak PGPhotoEditor *weakSelf = self; + _histogramGenerator = [[PGPhotoHistogramGenerator alloc] init]; + _histogramGenerator.histogramReady = ^(PGPhotoHistogram *histogram) + { + __strong PGPhotoEditor *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + strongSelf->_currentHistogram = histogram; + strongSelf->_histogramPipe.sink(histogram); + }; + + [self _importAdjustments:adjustments]; + } + return self; +} + +- (void)dealloc +{ + TGDispatchAfter(1.5f, dispatch_get_main_queue(), ^ + { + [[GPUImageContext sharedFramebufferCache] purgeAllUnassignedFramebuffers]; + }); +} + +- (void)cleanup +{ + [[GPUImageContext sharedFramebufferCache] purgeAllUnassignedFramebuffers]; +} + +- (NSArray *)toolsInit +{ + NSMutableArray *tools = [NSMutableArray array]; + for (Class toolClass in [PGPhotoEditor availableTools]) + { + PGPhotoTool *toolInstance = [[toolClass alloc] init]; + [tools addObject:toolInstance]; + } + + return tools; +} + +- (void)setImage:(UIImage *)image forCropRect:(CGRect)cropRect cropRotation:(CGFloat)cropRotation cropOrientation:(UIImageOrientation)cropOrientation cropMirrored:(bool)cropMirrored fullSize:(bool)fullSize +{ + [_toolComposer invalidate]; + _currentProcessChain = nil; + + _imageCropRect = cropRect; + _imageCropRotation = cropRotation; + _imageCropOrientation = cropOrientation; + _imageCropMirrored = cropMirrored; + + [_currentInput removeAllTargets]; + _currentInput = [[PGPhotoEditorPicture alloc] initWithImage:image]; + + _histogramGenerator.imageSize = image.size; + + _fullSize = fullSize; +} + +#pragma mark - Properties + +- (CGSize)rotatedCropSize +{ + if (_cropOrientation == UIImageOrientationLeft || _cropOrientation == UIImageOrientationRight) + return CGSizeMake(_cropRect.size.height, _cropRect.size.width); + + return _cropRect.size; +} + +- (bool)hasDefaultCropping +{ + if (!_CGRectEqualToRectWithEpsilon(self.cropRect, CGRectMake(0, 0, _originalSize.width, _originalSize.height), 1.0f) || self.cropOrientation != UIImageOrientationUp || ABS(self.cropRotation) > FLT_EPSILON || self.cropMirrored) + { + return false; + } + + return true; +} + +#pragma mark - Processing + +- (bool)readyForProcessing +{ + return (_currentInput != nil); +} + +- (void)processAnimated:(bool)animated completion:(void (^)(void))completion +{ + [self processAnimated:animated capture:false synchronous:false completion:completion]; +} + +- (void)processAnimated:(bool)animated capture:(bool)capture synchronous:(bool)synchronous completion:(void (^)(void))completion +{ + if (self.previewOutput == nil) + return; + + if (iosMajorVersion() < 7) + animated = false; + + if (_processing && completion == nil) + { + _needsReprocessing = true; + return; + } + + _processing = true; + + [_queue dispatch:^ + { + NSMutableArray *processChain = [NSMutableArray array]; + + for (PGPhotoTool *tool in _toolComposer.advancedTools) + { + if (!tool.shouldBeSkipped && tool.pass != nil) + [processChain addObject:tool.pass]; + } + + _toolComposer.imageSize = _cropRect.size; + [processChain addObject:_toolComposer]; + + TGPhotoEditorPreviewView *previewOutput = self.previewOutput; + + if (![_currentProcessChain isEqualToArray:processChain]) + { + [_currentInput removeAllTargets]; + + for (PGPhotoProcessPass *pass in _currentProcessChain) + [pass.filter removeAllTargets]; + + _currentProcessChain = processChain; + + GPUImageOutput *lastFilter = ((PGPhotoProcessPass *)_currentProcessChain.firstObject).filter; + [_currentInput addTarget:lastFilter]; + + NSInteger chainLength = _currentProcessChain.count; + if (chainLength > 1) + { + for (NSInteger i = 1; i < chainLength; i++) + { + PGPhotoProcessPass *pass = ((PGPhotoProcessPass *)_currentProcessChain[i]); + GPUImageOutput *filter = pass.filter; + [lastFilter addTarget:filter]; + lastFilter = filter; + } + } + _finalFilter = lastFilter; + + [_finalFilter addTarget:previewOutput.imageView]; + [_finalFilter addTarget:_histogramGenerator]; + } + + if (capture) + [_finalFilter useNextFrameForImageCapture]; + + for (PGPhotoProcessPass *pass in _currentProcessChain) + [pass process]; + + if (animated) + { + TGDispatchOnMainThread(^ + { + [previewOutput prepareTransitionFadeView]; + }); + } + + [_currentInput processSynchronous:true completion:^ + { + if (completion != nil) + completion(); + + _processing = false; + + if (animated) + { + TGDispatchOnMainThread(^ + { + [previewOutput performTransitionFade]; + }); + } + + if (_needsReprocessing && !synchronous) + { + _needsReprocessing = false; + [self processAnimated:false completion:nil]; + } + }]; + } synchronous:synchronous]; +} + +#pragma mark - Result + +- (void)createResultImageWithCompletion:(void (^)(UIImage *image))completion +{ + [self processAnimated:false capture:true synchronous:false completion:^ + { + UIImage *image = [_finalFilter imageFromCurrentFramebufferWithOrientation:UIImageOrientationUp]; + + if (completion != nil) + completion(image); + }]; +} + +- (UIImage *)currentResultImage +{ + __block UIImage *image = nil; + [self processAnimated:false capture:true synchronous:true completion:^ + { + image = [_finalFilter imageFromCurrentFramebufferWithOrientation:UIImageOrientationUp]; + }]; + return image; +} + +#pragma mark - Editor Values + +- (void)_importAdjustments:(id)adjustments +{ + _initialAdjustments = adjustments; + + if (adjustments != nil) + self.cropRect = adjustments.cropRect; + + self.cropOrientation = adjustments.cropOrientation; + self.cropLockedAspectRatio = adjustments.cropLockedAspectRatio; + self.cropMirrored = adjustments.cropMirrored; + self.paintingData = adjustments.paintingData; + + if ([adjustments isKindOfClass:[PGPhotoEditorValues class]]) + { + PGPhotoEditorValues *editorValues = (PGPhotoEditorValues *)adjustments; + + self.cropRotation = editorValues.cropRotation; + + for (PGPhotoTool *tool in self.tools) + { + id value = editorValues.toolValues[tool.identifier]; + if (value != nil && [value isKindOfClass:[tool valueClass]]) + tool.value = [value copy]; + } + } + else if ([adjustments isKindOfClass:[TGVideoEditAdjustments class]]) + { + TGVideoEditAdjustments *videoAdjustments = (TGVideoEditAdjustments *)adjustments; + self.trimStartValue = videoAdjustments.trimStartValue; + self.trimEndValue = videoAdjustments.trimEndValue; + self.sendAsGif = videoAdjustments.sendAsGif; + self.preset = videoAdjustments.preset; + } +} + +- (id)exportAdjustments +{ + return [self exportAdjustmentsWithPaintingData:_paintingData]; +} + +- (id)exportAdjustmentsWithPaintingData:(TGPaintingData *)paintingData +{ + if (!_forVideo) + { + NSMutableDictionary *toolValues = [[NSMutableDictionary alloc] init]; + for (PGPhotoTool *tool in self.tools) + { + if (!tool.shouldBeSkipped) + { + if (!([tool.value isKindOfClass:[NSNumber class]] && ABS([tool.value floatValue] - (float)tool.defaultValue) < FLT_EPSILON)) + toolValues[tool.identifier] = [tool.value copy]; + } + } + + return [PGPhotoEditorValues editorValuesWithOriginalSize:self.originalSize cropRect:self.cropRect cropRotation:self.cropRotation cropOrientation:self.cropOrientation cropLockedAspectRatio:self.cropLockedAspectRatio cropMirrored:self.cropMirrored toolValues:toolValues paintingData:paintingData sendAsGif:self.sendAsGif]; + } + else + { + TGVideoEditAdjustments *initialAdjustments = (TGVideoEditAdjustments *)_initialAdjustments; + + return [TGVideoEditAdjustments editAdjustmentsWithOriginalSize:self.originalSize cropRect:self.cropRect cropOrientation:self.cropOrientation cropLockedAspectRatio:self.cropLockedAspectRatio cropMirrored:self.cropMirrored trimStartValue:initialAdjustments.trimStartValue trimEndValue:initialAdjustments.trimEndValue paintingData:paintingData sendAsGif:self.sendAsGif preset:self.preset]; + } +} + +- (SSignal *)histogramSignal +{ + return [[SSignal single:_currentHistogram] then:_histogramPipe.signalProducer()]; +} + ++ (NSArray *)availableTools +{ + static NSArray *tools; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^ + { + tools = @[ [PGEnhanceTool class], + [PGExposureTool class], + [PGContrastTool class], + [PGSaturationTool class], + [PGWarmthTool class], + [PGFadeTool class], + [PGTintTool class], + [PGHighlightsTool class], + [PGShadowsTool class], + [PGVignetteTool class], + [PGGrainTool class], + [PGBlurTool class], + [PGSharpenTool class], + [PGCurvesTool class] ]; + }); + + return tools; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGPhotoEditorItem.h b/submodules/LegacyComponents/LegacyComponents/PGPhotoEditorItem.h new file mode 100644 index 0000000000..777fe85af8 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGPhotoEditorItem.h @@ -0,0 +1,33 @@ +#import + +@protocol PGPhotoEditorItem + +@property (nonatomic, readonly) NSString *identifier; +@property (nonatomic, readonly) NSString *title; + +@property (nonatomic, readonly) NSArray *parameters; + +@property (nonatomic, readonly) CGFloat defaultValue; +@property (nonatomic, readonly) CGFloat minimumValue; +@property (nonatomic, readonly) CGFloat maximumValue; +@property (nonatomic, readonly) bool segmented; + +@property (nonatomic, strong) id value; +@property (nonatomic, strong) id tempValue; +@property (nonatomic, readonly) id displayValue; +@property (nonatomic, readonly) NSString *stringValue; + +@property (nonatomic, readonly) bool shouldBeSkipped; +@property (nonatomic, assign) bool beingEdited; +@property (nonatomic, assign) bool disabled; + +@property (copy, nonatomic) void(^parametersChanged)(void); + +- (UIView *)itemControlViewWithChangeBlock:(void (^)(id newValue, bool animated))changeBlock; +- (UIView *)itemAreaViewWithChangeBlock:(void (^)(id newValue))changeBlock; + +- (Class)valueClass; + +- (void)updateParameters; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGPhotoEditorPicture.h b/submodules/LegacyComponents/LegacyComponents/PGPhotoEditorPicture.h new file mode 100644 index 0000000000..6231fcc3ad --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGPhotoEditorPicture.h @@ -0,0 +1,10 @@ +#import "GPUImageOutput.h" + +@interface PGPhotoEditorPicture : GPUImageOutput + +- (instancetype)initWithImage:(UIImage *)image; +- (instancetype)initWithImage:(UIImage *)image textureOptions:(GPUTextureOptions)textureOptions; + +- (bool)processSynchronous:(bool)synchronous completion:(void (^)(void))completion; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGPhotoEditorPicture.m b/submodules/LegacyComponents/LegacyComponents/PGPhotoEditorPicture.m new file mode 100644 index 0000000000..399acd45c3 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGPhotoEditorPicture.m @@ -0,0 +1,254 @@ +#import "PGPhotoEditorPicture.h" + +@interface PGPhotoEditorPicture () +{ + CGSize _imageSize; + bool _processed; + + dispatch_semaphore_t _updateSemaphore; +} +@end + +@implementation PGPhotoEditorPicture + +- (instancetype)initWithImage:(UIImage *)image +{ + GPUTextureOptions defaultOptions; + defaultOptions.minFilter = GL_LINEAR; + defaultOptions.magFilter = GL_LINEAR; + defaultOptions.wrapS = GL_CLAMP_TO_EDGE; + defaultOptions.wrapT = GL_CLAMP_TO_EDGE; + defaultOptions.internalFormat = GL_RGBA; + defaultOptions.format = GL_BGRA; + defaultOptions.type = GL_UNSIGNED_BYTE; + + return [self initWithImage:image textureOptions:defaultOptions]; +} + +- (instancetype)initWithImage:(UIImage *)image textureOptions:(GPUTextureOptions)textureOptions +{ + self = [super init]; + if (self != nil) + { + _updateSemaphore = dispatch_semaphore_create(0); + dispatch_semaphore_signal(_updateSemaphore); + + [self setupWithCGImage:image.CGImage orientation:image.imageOrientation textureOptions:textureOptions]; + } + return self; +} + +- (void)setupWithCGImage:(CGImageRef)image orientation:(UIImageOrientation)orientation textureOptions:(GPUTextureOptions)textureOptions +{ + bool redrawNeeded = false; + + if (orientation != UIImageOrientationUp) + redrawNeeded = true; + + CGSize imageSize = CGSizeMake(CGImageGetWidth(image), CGImageGetHeight(image)); + if (orientation == UIImageOrientationLeft || orientation == UIImageOrientationRight) + imageSize = CGSizeMake(imageSize.height, imageSize.width); + + CGSize fittedImageSize = [GPUImageContext sizeThatFitsWithinATextureForSize:imageSize]; + if (!CGSizeEqualToSize(fittedImageSize, imageSize)) + { + imageSize = fittedImageSize; + redrawNeeded = true; + } + + GLubyte *imageData = NULL; + CFDataRef dataFromImageDataProvider = NULL; + GLenum format = GL_BGRA; + + if (!redrawNeeded) + { + if (CGImageGetBytesPerRow(image) != CGImageGetWidth(image) * 4 || + CGImageGetBitsPerPixel(image) != 32 || + CGImageGetBitsPerComponent(image) != 8) + { + redrawNeeded = true; + } + else + { + CGBitmapInfo bitmapInfo = CGImageGetBitmapInfo(image); + if ((bitmapInfo & kCGBitmapFloatComponents) != 0) + { + redrawNeeded = true; + } + else + { + CGBitmapInfo byteOrderInfo = bitmapInfo & kCGBitmapByteOrderMask; + if (byteOrderInfo == kCGBitmapByteOrder32Little) + { + /* Little endian, for alpha-first we can use this bitmap directly in GL */ + CGImageAlphaInfo alphaInfo = bitmapInfo & kCGBitmapAlphaInfoMask; + if (alphaInfo != kCGImageAlphaPremultipliedFirst && alphaInfo != kCGImageAlphaFirst && + alphaInfo != kCGImageAlphaNoneSkipFirst) + { + redrawNeeded = true; + } + } + else if (byteOrderInfo == kCGBitmapByteOrderDefault || byteOrderInfo == kCGBitmapByteOrder32Big) + { + /* Big endian, for alpha-last we can use this bitmap directly in GL */ + CGImageAlphaInfo alphaInfo = bitmapInfo & kCGBitmapAlphaInfoMask; + if (alphaInfo != kCGImageAlphaPremultipliedLast && alphaInfo != kCGImageAlphaLast && + alphaInfo != kCGImageAlphaNoneSkipLast) + { + redrawNeeded = true; + } else + { + /* Can access directly using GL_RGBA pixel format */ + format = GL_RGBA; + } + } + } + } + } + + _imageSize = imageSize; + + if (redrawNeeded) + { + imageData = (GLubyte *) calloc(1, (int)imageSize.width * (int)imageSize.height * 4); + + CGColorSpaceRef genericRGBColorspace = CGColorSpaceCreateDeviceRGB(); + + CGContextRef imageContext = CGBitmapContextCreate(imageData, (size_t)imageSize.width, (size_t)imageSize.height, 8, (size_t)imageSize.width * 4, genericRGBColorspace, kCGBitmapByteOrder32Little | kCGImageAlphaPremultipliedFirst); + + CGSize imageDrawSize = CGSizeMake(imageSize.width, imageSize.height); + if (orientation == UIImageOrientationLeft || orientation == UIImageOrientationRight) + imageDrawSize = CGSizeMake(imageDrawSize.height, imageDrawSize.width); + + CGAffineTransform transform = CGAffineTransformIdentity; + switch (orientation) + { + case UIImageOrientationDown: + case UIImageOrientationDownMirrored: + transform = CGAffineTransformTranslate(transform, imageSize.width, imageSize.height); + transform = CGAffineTransformRotate(transform, (CGFloat)M_PI); + break; + + case UIImageOrientationLeft: + case UIImageOrientationLeftMirrored: + transform = CGAffineTransformTranslate(transform, imageSize.width, 0); + transform = CGAffineTransformRotate(transform, (CGFloat)M_PI_2); + break; + + case UIImageOrientationRight: + case UIImageOrientationRightMirrored: + transform = CGAffineTransformTranslate(transform, 0, imageSize.height); + transform = CGAffineTransformRotate(transform, (CGFloat)-M_PI_2); + break; + + default: + break; + } + + switch (orientation) + { + case UIImageOrientationUpMirrored: + case UIImageOrientationDownMirrored: + transform = CGAffineTransformTranslate(transform, imageSize.width,0); + transform = CGAffineTransformScale(transform, -1, 1); + break; + + case UIImageOrientationLeftMirrored: + case UIImageOrientationRightMirrored: + transform = CGAffineTransformTranslate(transform, imageSize.height, 0); + transform = CGAffineTransformScale(transform, -1, 1); + break; + + default: + break; + } + + CGContextConcatCTM(imageContext, transform); + CGContextSetInterpolationQuality(imageContext, kCGInterpolationHigh); + CGContextDrawImage(imageContext, CGRectMake(0.0f, 0.0f, imageDrawSize.width, imageDrawSize.height), image); + CGContextRelease(imageContext); + CGColorSpaceRelease(genericRGBColorspace); + } + else + { + dataFromImageDataProvider = CGDataProviderCopyData(CGImageGetDataProvider(image)); + imageData = (GLubyte *)CFDataGetBytePtr(dataFromImageDataProvider); + } + + runSynchronouslyOnVideoProcessingQueue(^ + { + [GPUImageContext useImageProcessingContext]; + + outputFramebuffer = [[GPUImageContext sharedFramebufferCache] fetchFramebufferForSize:imageSize textureOptions:textureOptions onlyTexture:true]; + [outputFramebuffer disableReferenceCounting]; + + glBindTexture(GL_TEXTURE_2D, [outputFramebuffer texture]); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, (int)imageSize.width, (int)imageSize.height, 0, format, GL_UNSIGNED_BYTE, imageData); + + glBindTexture(GL_TEXTURE_2D, 0); + }); + + if (redrawNeeded) + free(imageData); + else if (dataFromImageDataProvider) + CFRelease(dataFromImageDataProvider); +} + +- (void)dealloc +{ + [outputFramebuffer enableReferenceCounting]; + [outputFramebuffer unlock]; +} + +- (void)addTarget:(id)newTarget atTextureLocation:(NSInteger)textureLocation +{ + [super addTarget:newTarget atTextureLocation:textureLocation]; + + if (_processed) + { + [newTarget setInputSize:_imageSize atIndex:textureLocation]; + [newTarget newFrameReadyAtTime:kCMTimeIndefinite atIndex:textureLocation]; + } +} + +- (void)removeAllTargets +{ + [super removeAllTargets]; + _processed = false; +} + +- (bool)processSynchronous:(bool)synchronous completion:(void (^)(void))completion +{ + _processed = true; + + if (dispatch_semaphore_wait(_updateSemaphore, DISPATCH_TIME_NOW) != 0) + return false; + + void (^block)(void) = ^ + { + for (id currentTarget in targets) + { + NSInteger indexOfObject = [targets indexOfObject:currentTarget]; + NSInteger textureIndexOfTarget = [[targetTextureIndices objectAtIndex:indexOfObject] integerValue]; + + [currentTarget setCurrentlyReceivingMonochromeInput:false]; + [currentTarget setInputSize:_imageSize atIndex:textureIndexOfTarget]; + [currentTarget setInputFramebuffer:outputFramebuffer atIndex:textureIndexOfTarget]; + [currentTarget newFrameReadyAtTime:kCMTimeIndefinite atIndex:textureIndexOfTarget]; + } + + dispatch_semaphore_signal(_updateSemaphore); + + if (completion != nil) + completion(); + }; + + if (synchronous) + runSynchronouslyOnVideoProcessingQueue(block); + else + runAsynchronouslyOnVideoProcessingQueue(block); + + return true; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGPhotoEditorRawDataInput.h b/submodules/LegacyComponents/LegacyComponents/PGPhotoEditorRawDataInput.h new file mode 100644 index 0000000000..6c83a9d8e7 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGPhotoEditorRawDataInput.h @@ -0,0 +1,29 @@ +#import "GPUImageOutput.h" + +typedef enum { + GPUPixelFormatBGRA = GL_BGRA, + GPUPixelFormatRGBA = GL_RGBA, + GPUPixelFormatRGB = GL_RGB +} GPUPixelFormat; + +typedef enum { + GPUPixelTypeUByte = GL_UNSIGNED_BYTE, + GPUPixelTypeFloat = GL_FLOAT +} GPUPixelType; + +@interface PGPhotoEditorRawDataInput : GPUImageOutput + +- (instancetype)initWithBytes:(GLubyte *)bytesToUpload size:(CGSize)imageSize; +- (instancetype)initWithBytes:(GLubyte *)bytesToUpload size:(CGSize)imageSize pixelFormat:(GPUPixelFormat)pixelFormat; +- (instancetype)initWithBytes:(GLubyte *)bytesToUpload size:(CGSize)imageSize pixelFormat:(GPUPixelFormat)pixelFormat type:(GPUPixelType)pixelType; + +@property (nonatomic, assign) GPUPixelFormat pixelFormat; +@property (nonatomic, assign) GPUPixelType pixelType; + +- (void)updateDataWithBytes:(GLubyte *)bytesToUpload size:(CGSize)imageSize; +- (void)processData; +- (void)processDataForTimestamp:(CMTime)frameTime; +- (CGSize)outputImageSize; +- (void)invalidate; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGPhotoEditorRawDataInput.m b/submodules/LegacyComponents/LegacyComponents/PGPhotoEditorRawDataInput.m new file mode 100644 index 0000000000..c21806465f --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGPhotoEditorRawDataInput.m @@ -0,0 +1,155 @@ +#import "PGPhotoEditorRawDataInput.h" + +@interface PGPhotoEditorRawDataInput () +{ + CGSize _imageSize; + bool _processed; + + dispatch_semaphore_t _updateSemaphore; +} +@end + +@implementation PGPhotoEditorRawDataInput + +- (instancetype)init +{ + self = [super init]; + if (self != nil) + { + _updateSemaphore = dispatch_semaphore_create(1); + _processed = false; + } + return self; +} + +- (void)invalidate +{ +} + +- (instancetype)initWithBytes:(GLubyte *)bytes size:(CGSize)size +{ + return [self initWithBytes:bytes size:size pixelFormat:GPUPixelFormatBGRA type:GPUPixelTypeUByte]; +} + +- (instancetype)initWithBytes:(GLubyte *)bytes size:(CGSize)size pixelFormat:(GPUPixelFormat)pixelFormat +{ + return [self initWithBytes:bytes size:size pixelFormat:pixelFormat type:GPUPixelTypeUByte]; +} + +- (instancetype)initWithBytes:(GLubyte *)bytes size:(CGSize)size pixelFormat:(GPUPixelFormat)pixelFormat type:(GPUPixelType)pixelType +{ + self = [self init]; + if (self != nil) + { + _imageSize = size; + self.pixelFormat = pixelFormat; + self.pixelType = pixelType; + + if (bytes != NULL) + [self uploadBytes:bytes]; + } + return self; +} + +- (void)dealloc +{ + [outputFramebuffer enableReferenceCounting]; + [outputFramebuffer unlock]; +} + +- (void)addTarget:(id)newTarget atTextureLocation:(NSInteger)textureLocation +{ + [super addTarget:newTarget atTextureLocation:textureLocation]; + + if (_processed) + { + [newTarget setInputSize:_imageSize atIndex:textureLocation]; + [newTarget setInputFramebuffer:outputFramebuffer atIndex:textureLocation]; + [newTarget newFrameReadyAtTime:kCMTimeIndefinite atIndex:textureLocation]; + } +} + +- (void)removeAllTargets +{ + [super removeAllTargets]; + _processed = false; +} + +- (void)uploadBytes:(GLubyte *)bytes +{ + [GPUImageContext useImageProcessingContext]; + + if (outputFramebuffer != nil) + { + [outputFramebuffer enableReferenceCounting]; + [outputFramebuffer unlock]; + } + outputFramebuffer = [[GPUImageContext sharedFramebufferCache] fetchFramebufferForSize:_imageSize textureOptions:self.outputTextureOptions onlyTexture:true]; + [outputFramebuffer disableReferenceCounting]; + + glBindTexture(GL_TEXTURE_2D, [outputFramebuffer texture]); + glTexImage2D(GL_TEXTURE_2D, 0, _pixelFormat==GPUPixelFormatRGB ? GL_RGB : GL_RGBA, (int)_imageSize.width, (int)_imageSize.height, 0, (GLint)_pixelFormat, (GLenum)_pixelType, bytes); +} + +- (void)updateDataWithBytes:(GLubyte *)bytes size:(CGSize)size +{ + _imageSize = size; + + [self uploadBytes:bytes]; +} + +- (void)processData +{ + _processed = true; + + if (dispatch_semaphore_wait(_updateSemaphore, DISPATCH_TIME_NOW) != 0) + return; + + runAsynchronouslyOnVideoProcessingQueue(^ + { + CGSize pixelSizeOfImage = [self outputImageSize]; + + for (id currentTarget in targets) + { + NSInteger indexOfObject = [targets indexOfObject:currentTarget]; + NSInteger textureIndexOfTarget = [[targetTextureIndices objectAtIndex:indexOfObject] integerValue]; + + [currentTarget setInputSize:pixelSizeOfImage atIndex:textureIndexOfTarget]; + [currentTarget setInputFramebuffer:outputFramebuffer atIndex:textureIndexOfTarget]; + [currentTarget newFrameReadyAtTime:kCMTimeInvalid atIndex:textureIndexOfTarget]; + } + + dispatch_semaphore_signal(_updateSemaphore); + }); +} + +- (void)processDataForTimestamp:(CMTime)frameTime +{ + _processed = true; + + if (dispatch_semaphore_wait(_updateSemaphore, DISPATCH_TIME_NOW) != 0) + return; + + runAsynchronouslyOnVideoProcessingQueue(^ + { + CGSize pixelSizeOfImage = [self outputImageSize]; + + for (id currentTarget in targets) + { + NSInteger indexOfObject = [targets indexOfObject:currentTarget]; + NSInteger textureIndexOfTarget = [[targetTextureIndices objectAtIndex:indexOfObject] integerValue]; + + [currentTarget setInputSize:pixelSizeOfImage atIndex:textureIndexOfTarget]; + [currentTarget newFrameReadyAtTime:frameTime atIndex:textureIndexOfTarget]; + } + + dispatch_semaphore_signal(_updateSemaphore); + }); +} + +- (CGSize)outputImageSize +{ + return _imageSize; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGPhotoEditorRawDataOutput.h b/submodules/LegacyComponents/LegacyComponents/PGPhotoEditorRawDataOutput.h new file mode 100644 index 0000000000..5ebe76b771 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGPhotoEditorRawDataOutput.h @@ -0,0 +1,33 @@ +#import +#import "GPUImageContext.h" + +typedef struct +{ + GLubyte red; + GLubyte green; + GLubyte blue; + GLubyte alpha; +} PGByteColorVector; + +@protocol GPURawDataProcessor; + +@interface PGPhotoEditorRawDataOutput : NSObject +{ + GPUImageRotationMode inputRotation; + bool outputBGRA; +} + +@property (nonatomic, readonly) GLubyte *rawBytesForImage; +@property (nonatomic, copy) void(^newFrameAvailableBlock)(void); +@property (nonatomic, assign) bool enabled; +@property (nonatomic, assign) CGSize imageSize; + +- (instancetype)initWithImageSize:(CGSize)newImageSize resultsInBGRAFormat:(bool)resultsInBGRAFormat; + +- (PGByteColorVector)colorAtLocation:(CGPoint)locationInImage; +- (NSUInteger)bytesPerRowInOutput; + +- (void)lockFramebufferForReading; +- (void)unlockFramebufferAfterReading; + +@end \ No newline at end of file diff --git a/submodules/LegacyComponents/LegacyComponents/PGPhotoEditorRawDataOutput.m b/submodules/LegacyComponents/LegacyComponents/PGPhotoEditorRawDataOutput.m new file mode 100644 index 0000000000..ebdb30d5d2 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGPhotoEditorRawDataOutput.m @@ -0,0 +1,288 @@ +#import "PGPhotoEditorRawDataOutput.h" +#import "PGPhotoProcessPass.h" + +#import "GPUImageContext.h" +#import "GLProgram.h" +#import "GPUImageFilter.h" + +@interface PGPhotoEditorRawDataOutput () +{ + GPUImageFramebuffer *firstInputFramebuffer, *outputFramebuffer, *retainedFramebuffer; + + bool hasReadFromTheCurrentFrame; + + GLProgram *dataProgram; + GLint dataPositionAttribute, dataTextureCoordinateAttribute; + GLint dataInputTextureUniform; + + GLubyte *_rawBytesForImage; + + bool lockNextFramebuffer; +} + +@end + +@implementation PGPhotoEditorRawDataOutput + +@synthesize rawBytesForImage = _rawBytesForImage; +@synthesize newFrameAvailableBlock = _newFrameAvailableBlock; +@synthesize enabled; + +#pragma mark - +#pragma mark Initialization and teardown + +- (instancetype)initWithImageSize:(CGSize)newImageSize resultsInBGRAFormat:(bool)resultsInBGRAFormat +{ + if (!(self = [super init])) + { + return nil; + } + + self.enabled = true; + lockNextFramebuffer = false; + outputBGRA = resultsInBGRAFormat; + _imageSize = newImageSize; + hasReadFromTheCurrentFrame = false; + _rawBytesForImage = NULL; + inputRotation = kGPUImageNoRotation; + + [GPUImageContext useImageProcessingContext]; + if ( (outputBGRA && ![GPUImageContext supportsFastTextureUpload]) || (!outputBGRA && [GPUImageContext supportsFastTextureUpload]) ) + { + dataProgram = [[GPUImageContext sharedImageProcessingContext] programForVertexShaderString:kGPUImageVertexShaderString fragmentShaderString:PGPhotoEnhanceColorSwapShaderString]; + } + else + { + dataProgram = [[GPUImageContext sharedImageProcessingContext] programForVertexShaderString:kGPUImageVertexShaderString fragmentShaderString:kGPUImagePassthroughFragmentShaderString]; + } + + if (!dataProgram.initialized) + { + [dataProgram addAttribute:@"position"]; + [dataProgram addAttribute:@"inputTextureCoordinate"]; + + if (![dataProgram link]) + { + NSString *progLog = [dataProgram programLog]; + NSLog(@"Program link log: %@", progLog); + NSString *fragLog = [dataProgram fragmentShaderLog]; + NSLog(@"Fragment shader compile log: %@", fragLog); + NSString *vertLog = [dataProgram vertexShaderLog]; + NSLog(@"Vertex shader compile log: %@", vertLog); + dataProgram = nil; + NSAssert(NO, @"Filter shader link failed"); + } + } + + dataPositionAttribute = [dataProgram attributeIndex:@"position"]; + dataTextureCoordinateAttribute = [dataProgram attributeIndex:@"inputTextureCoordinate"]; + dataInputTextureUniform = [dataProgram uniformIndex:@"inputImageTexture"]; + + return self; +} + +- (void)dealloc +{ + if (_rawBytesForImage != NULL && (![GPUImageContext supportsFastTextureUpload])) + { + free(_rawBytesForImage); + _rawBytesForImage = NULL; + } +} + +#pragma mark - +#pragma mark Data access + +- (void)renderAtInternalSize +{ + [GPUImageContext setActiveShaderProgram:dataProgram]; + + outputFramebuffer = [[GPUImageContext sharedFramebufferCache] fetchFramebufferForSize:_imageSize onlyTexture:false]; + [outputFramebuffer activateFramebuffer]; + + if(lockNextFramebuffer) + { + retainedFramebuffer = outputFramebuffer; + [retainedFramebuffer lock]; + [retainedFramebuffer lockForReading]; + lockNextFramebuffer = NO; + } + + glClearColor(0.0f, 0.0f, 0.0f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + + static const GLfloat squareVertices[] = { + -1.0f, -1.0f, + 1.0f, -1.0f, + -1.0f, 1.0f, + 1.0f, 1.0f, + }; + + static const GLfloat textureCoordinates[] = { + 0.0f, 0.0f, + 1.0f, 0.0f, + 0.0f, 1.0f, + 1.0f, 1.0f, + }; + + glActiveTexture(GL_TEXTURE4); + glBindTexture(GL_TEXTURE_2D, [firstInputFramebuffer texture]); + glUniform1i(dataInputTextureUniform, 4); + + glVertexAttribPointer(dataPositionAttribute, 2, GL_FLOAT, 0, 0, squareVertices); + glVertexAttribPointer(dataTextureCoordinateAttribute, 2, GL_FLOAT, 0, 0, textureCoordinates); + + glEnableVertexAttribArray(dataPositionAttribute); + glEnableVertexAttribArray(dataTextureCoordinateAttribute); + + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); + [firstInputFramebuffer unlock]; +} + +- (PGByteColorVector)colorAtLocation:(CGPoint)locationInImage +{ + PGByteColorVector *imageColorBytes = (PGByteColorVector *)self.rawBytesForImage; + + CGPoint locationToPickFrom = CGPointZero; + locationToPickFrom.x = MIN(MAX(locationInImage.x, 0.0), (_imageSize.width - 1.0)); + locationToPickFrom.y = MIN(MAX((_imageSize.height - locationInImage.y), 0.0), (_imageSize.height - 1.0)); + + if (outputBGRA) + { + PGByteColorVector flippedColor = imageColorBytes[(int)(round((locationToPickFrom.y * _imageSize.width) + locationToPickFrom.x))]; + GLubyte temporaryRed = flippedColor.red; + + flippedColor.red = flippedColor.blue; + flippedColor.blue = temporaryRed; + + return flippedColor; + } + else + { + return imageColorBytes[(int)(round((locationToPickFrom.y * _imageSize.width) + locationToPickFrom.x))]; + } +} + +#pragma mark - +#pragma mark GPUImageInput protocol + +- (void)newFrameReadyAtTime:(CMTime)__unused frameTime atIndex:(NSInteger)__unused textureIndex +{ + hasReadFromTheCurrentFrame = NO; + + if (_newFrameAvailableBlock != nil) + _newFrameAvailableBlock(); +} + +- (NSInteger)nextAvailableTextureIndex +{ + return 0; +} + +- (void)setInputFramebuffer:(GPUImageFramebuffer *)newInputFramebuffer atIndex:(NSInteger)__unused textureIndex +{ + firstInputFramebuffer = newInputFramebuffer; + [firstInputFramebuffer lock]; +} + +- (void)setInputRotation:(GPUImageRotationMode)newInputRotation atIndex:(NSInteger)__unused textureIndex +{ + inputRotation = newInputRotation; +} + +- (void)setInputSize:(CGSize)__unused newSize atIndex:(NSInteger)__unused textureIndex +{ +} + +- (CGSize)maximumOutputSize +{ + return _imageSize; +} + +- (void)endProcessing +{ +} + +- (BOOL)shouldIgnoreUpdatesToThisTarget +{ + return NO; +} + +- (BOOL)wantsMonochromeInput +{ + return NO; +} + +- (void)setCurrentlyReceivingMonochromeInput:(BOOL)__unused newValue +{ + +} + +#pragma mark - +#pragma mark Accessors + +- (GLubyte *)rawBytesForImage +{ + if ((_rawBytesForImage == NULL) && (![GPUImageContext supportsFastTextureUpload])) + { + _rawBytesForImage = (GLubyte *) calloc((unsigned long)(_imageSize.width * _imageSize.height * 4), sizeof(GLubyte)); + hasReadFromTheCurrentFrame = NO; + } + + if (hasReadFromTheCurrentFrame) + { + return _rawBytesForImage; + } + else + { + runSynchronouslyOnVideoProcessingQueue(^ + { + [GPUImageContext useImageProcessingContext]; + [self renderAtInternalSize]; + + if ([GPUImageContext supportsFastTextureUpload]) + { + glFinish(); + _rawBytesForImage = [outputFramebuffer byteBuffer]; + } + else + { + glReadPixels(0, 0, (GLsizei)(_imageSize.width), (GLsizei)_imageSize.height, GL_RGBA, GL_UNSIGNED_BYTE, _rawBytesForImage); + } + + hasReadFromTheCurrentFrame = YES; + + }); + + return _rawBytesForImage; + } +} + +- (NSUInteger)bytesPerRowInOutput +{ + return [retainedFramebuffer bytesPerRow]; +} + +- (void)setImageSize:(CGSize)newImageSize +{ + _imageSize = newImageSize; + if (_rawBytesForImage != NULL && (![GPUImageContext supportsFastTextureUpload])) + { + free(_rawBytesForImage); + _rawBytesForImage = NULL; + } +} + +- (void)lockFramebufferForReading +{ + lockNextFramebuffer = YES; +} + +- (void)unlockFramebufferAfterReading +{ + [retainedFramebuffer unlockAfterReading]; + [retainedFramebuffer unlock]; + retainedFramebuffer = nil; +} + +@end \ No newline at end of file diff --git a/submodules/LegacyComponents/LegacyComponents/PGPhotoEditorValues.h b/submodules/LegacyComponents/LegacyComponents/PGPhotoEditorValues.h new file mode 100644 index 0000000000..28d0e71997 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGPhotoEditorValues.h @@ -0,0 +1,14 @@ +#import + +@class TGPaintingData; + +@interface PGPhotoEditorValues : NSObject + +@property (nonatomic, readonly) CGFloat cropRotation; +@property (nonatomic, readonly) NSDictionary *toolValues; + +- (bool)toolsApplied; + ++ (instancetype)editorValuesWithOriginalSize:(CGSize)originalSize cropRect:(CGRect)cropRect cropRotation:(CGFloat)cropRotation cropOrientation:(UIImageOrientation)cropOrientation cropLockedAspectRatio:(CGFloat)cropLockedAspectRatio cropMirrored:(bool)cropMirrored toolValues:(NSDictionary *)toolValues paintingData:(TGPaintingData *)paintingData sendAsGif:(bool)sendAsGif; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGPhotoEditorValues.m b/submodules/LegacyComponents/LegacyComponents/PGPhotoEditorValues.m new file mode 100644 index 0000000000..f56457fcd7 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGPhotoEditorValues.m @@ -0,0 +1,121 @@ +#import "PGPhotoEditorValues.h" +#import "TGPaintingData.h" + +#import "TGPhotoEditorUtils.h" + +@implementation PGPhotoEditorValues + +@synthesize originalSize = _originalSize; +@synthesize cropRect = _cropRect; +@synthesize cropOrientation = _cropOrientation; +@synthesize cropLockedAspectRatio = _cropLockedAspectRatio; +@synthesize cropMirrored = _cropMirrored; +@synthesize paintingData = _paintingData; +@synthesize sendAsGif = _sendAsGif; + ++ (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]; + values->_originalSize = originalSize; + values->_cropRect = cropRect; + values->_cropRotation = cropRotation; + values->_cropOrientation = cropOrientation; + values->_cropLockedAspectRatio = cropLockedAspectRatio; + values->_cropMirrored = cropMirrored; + values->_toolValues = toolValues; + values->_paintingData = paintingData; + values->_sendAsGif = sendAsGif; + + return values; +} + +- (bool)hasPainting +{ + return (_paintingData != nil); +} + +- (bool)cropAppliedForAvatar:(bool)forAvatar +{ + CGRect defaultCropRect = CGRectMake(0, 0, _originalSize.width, _originalSize.height); + if (forAvatar) + { + CGFloat shortSide = MIN(_originalSize.width, _originalSize.height); + defaultCropRect = CGRectMake((_originalSize.width - shortSide) / 2, (_originalSize.height - shortSide) / 2, shortSide, shortSide); + } + + if (!_CGRectEqualToRectWithEpsilon(self.cropRect, defaultCropRect, [self _cropRectEpsilon])) + return true; + + if (ABS(self.cropRotation) > FLT_EPSILON) + return true; + + if (self.cropOrientation != UIImageOrientationUp) + return true; + + if (self.cropLockedAspectRatio > FLT_EPSILON) + return true; + + if (self.cropMirrored) + return true; + + return false; +} + +- (bool)toolsApplied +{ + if (self.toolValues.count > 0) + return true; + + return false; +} + +- (bool)isDefaultValuesForAvatar:(bool)forAvatar +{ + return ![self cropAppliedForAvatar:forAvatar] && ![self toolsApplied] && ![self hasPainting]; +} + +- (bool)isCropEqualWith:(id)adjusments +{ + return (_CGRectEqualToRectWithEpsilon(self.cropRect, adjusments.cropRect, [self _cropRectEpsilon])); +} + +- (BOOL)isEqual:(id)object +{ + if (object == self) + return true; + + if (!object || ![object isKindOfClass:[self class]]) + return false; + + PGPhotoEditorValues *values = (PGPhotoEditorValues *)object; + + if (!_CGRectEqualToRectWithEpsilon(self.cropRect, values.cropRect, [self _cropRectEpsilon])) + return false; + + if (ABS(self.cropRotation - values.cropRotation) > FLT_EPSILON) + return false; + + if (self.cropOrientation != values.cropOrientation) + return false; + + if (ABS(self.cropLockedAspectRatio - values.cropLockedAspectRatio) > FLT_EPSILON) + return false; + + if (self.cropMirrored != values.cropMirrored) + return false; + + if (![self.toolValues isEqual:values.toolValues]) + return false; + + if (self.paintingData != values.paintingData && ![self.paintingData isEqual:values.paintingData]) + return false; + + return true; +} + +- (CGFloat)_cropRectEpsilon +{ + return MAX(_originalSize.width, _originalSize.height) * 0.005f; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGPhotoEditorView.h b/submodules/LegacyComponents/LegacyComponents/PGPhotoEditorView.h new file mode 100644 index 0000000000..e514b5c0fe --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGPhotoEditorView.h @@ -0,0 +1,9 @@ +#import + +#import "GPUImageContext.h" + +@interface PGPhotoEditorView : UIView + +@property (nonatomic, assign) bool enabled; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGPhotoEditorView.m b/submodules/LegacyComponents/LegacyComponents/PGPhotoEditorView.m new file mode 100644 index 0000000000..4f00bf82ff --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGPhotoEditorView.m @@ -0,0 +1,411 @@ +#import "PGPhotoEditorView.h" + +#import "GPUImageContext.h" +#import "GPUImageFilter.h" + +#import +#import +#import + +@interface PGPhotoEditorView () +{ + GPUImageRotationMode inputRotation; + CGSize _sizeInPixels; + + GPUImageFramebuffer *inputFramebufferForDisplay; + GLuint displayRenderbuffer; + GLuint displayFramebuffer; + + GLProgram *displayProgram; + GLint displayPositionAttribute; + GLint displayTextureCoordinateAttribute; + GLint displayInputTextureUniform; + + CGSize inputImageSize; + GLfloat imageVertices[8]; + GLfloat backgroundColorRed; + GLfloat backgroundColorGreen; + GLfloat backgroundColorBlue; + GLfloat backgroundColorAlpha; + + CGSize boundsSizeAtFrameBufferEpoch; +} + +@property (assign, nonatomic) NSUInteger aspectRatio; + +@end + +@implementation PGPhotoEditorView + +- (instancetype)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + if (self != nil) + { + [self commonInit]; + } + return self; +} + ++ (Class)layerClass +{ + return [CAEAGLLayer class]; +} + +- (void)commonInit +{ + if ([self respondsToSelector:@selector(setContentScaleFactor:)]) + self.contentScaleFactor = [[UIScreen mainScreen] scale]; + + self.enabled = true; + + inputRotation = kGPUImageNoRotation; + self.opaque = true; + + CAEAGLLayer *eaglLayer = (CAEAGLLayer *)self.layer; + eaglLayer.opaque = true; + eaglLayer.drawableProperties = @{ kEAGLDrawablePropertyRetainedBacking: @NO, + kEAGLDrawablePropertyColorFormat: kEAGLColorFormatRGBA8 }; + + runSynchronouslyOnVideoProcessingQueue(^ + { + [GPUImageContext useImageProcessingContext]; + + displayProgram = [[GPUImageContext sharedImageProcessingContext] programForVertexShaderString:kGPUImageVertexShaderString fragmentShaderString:kGPUImagePassthroughFragmentShaderString]; + + if (!displayProgram.initialized) + { + [displayProgram addAttribute:@"position"]; + [displayProgram addAttribute:@"inputTexCoord"]; + + if (![displayProgram link]) + { + NSString *progLog = [displayProgram programLog]; + NSLog(@"Program link log: %@", progLog); + NSString *fragLog = [displayProgram fragmentShaderLog]; + NSLog(@"Fragment shader compile log: %@", fragLog); + NSString *vertLog = [displayProgram vertexShaderLog]; + NSLog(@"Vertex shader compile log: %@", vertLog); + displayProgram = nil; + NSAssert(NO, @"Filter shader link failed"); + } + } + + displayPositionAttribute = [displayProgram attributeIndex:@"position"]; + displayTextureCoordinateAttribute = [displayProgram attributeIndex:@"inputTexCoord"]; + displayInputTextureUniform = [displayProgram uniformIndex:@"sourceImage"]; + + [GPUImageContext setActiveShaderProgram:displayProgram]; + glEnableVertexAttribArray(displayPositionAttribute); + glEnableVertexAttribArray(displayTextureCoordinateAttribute); + + [self setBackgroundColorRed:0.0 green:0.0 blue:0.0 alpha:1.0]; +// _fillMode = kGPUImageFillModePreserveAspectRatio; + [self createDisplayFramebuffer]; + }); +} + +- (void)layoutSubviews +{ + [super layoutSubviews]; + + if (!CGSizeEqualToSize(self.bounds.size, boundsSizeAtFrameBufferEpoch) && + !CGSizeEqualToSize(self.bounds.size, CGSizeZero)) + { + runSynchronouslyOnVideoProcessingQueue(^ + { + [self destroyDisplayFramebuffer]; + [self createDisplayFramebuffer]; + [self recalculateViewGeometry]; + }); + } +} + +- (void)dealloc +{ + runSynchronouslyOnVideoProcessingQueue(^ + { + [self destroyDisplayFramebuffer]; + }); +} + +- (void)createDisplayFramebuffer +{ + [GPUImageContext useImageProcessingContext]; + + glGenFramebuffers(1, &displayFramebuffer); + glBindFramebuffer(GL_FRAMEBUFFER, displayFramebuffer); + + glGenRenderbuffers(1, &displayRenderbuffer); + glBindRenderbuffer(GL_RENDERBUFFER, displayRenderbuffer); + + [[[GPUImageContext sharedImageProcessingContext] context] renderbufferStorage:GL_RENDERBUFFER fromDrawable:(CAEAGLLayer*)self.layer]; + + GLint backingWidth, backingHeight; + + glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &backingWidth); + glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &backingHeight); + + if ( (backingWidth == 0) || (backingHeight == 0) ) + { + [self destroyDisplayFramebuffer]; + return; + } + + _sizeInPixels.width = (CGFloat)backingWidth; + _sizeInPixels.height = (CGFloat)backingHeight; + + glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, displayRenderbuffer); + + GLuint framebufferCreationStatus = glCheckFramebufferStatus(GL_FRAMEBUFFER); + NSAssert(framebufferCreationStatus == GL_FRAMEBUFFER_COMPLETE, @"Failure with display framebuffer generation for display of size: %f, %f", self.bounds.size.width, self.bounds.size.height); + boundsSizeAtFrameBufferEpoch = self.bounds.size; +} + +- (void)destroyDisplayFramebuffer +{ + [GPUImageContext useImageProcessingContext]; + + if (displayFramebuffer) + { + glDeleteFramebuffers(1, &displayFramebuffer); + displayFramebuffer = 0; + } + + if (displayRenderbuffer) + { + glDeleteRenderbuffers(1, &displayRenderbuffer); + displayRenderbuffer = 0; + } +} + +- (void)setDisplayFramebuffer +{ + if (!displayFramebuffer) + { + [self createDisplayFramebuffer]; + } + + glBindFramebuffer(GL_FRAMEBUFFER, displayFramebuffer); + + glViewport(0, 0, (GLint)_sizeInPixels.width, (GLint)_sizeInPixels.height); +} + +- (void)presentFramebuffer +{ + glBindRenderbuffer(GL_RENDERBUFFER, displayRenderbuffer); + [[GPUImageContext sharedImageProcessingContext] presentBufferForDisplay]; +} + +#pragma mark - +#pragma mark Handling fill mode + +- (void)recalculateViewGeometry +{ + runSynchronouslyOnVideoProcessingQueue(^ + { + CGFloat heightScaling, widthScaling; + + widthScaling = 1.0; + heightScaling = 1.0; + + imageVertices[0] = (GLfloat)-widthScaling; + imageVertices[1] = (GLfloat)-heightScaling; + imageVertices[2] = (GLfloat)widthScaling; + imageVertices[3] = (GLfloat)-heightScaling; + imageVertices[4] = (GLfloat)-widthScaling; + imageVertices[5] = (GLfloat)heightScaling; + imageVertices[6] = (GLfloat)widthScaling; + imageVertices[7] = (GLfloat)heightScaling; + }); +} + +- (void)setBackgroundColorRed:(GLfloat)redComponent green:(GLfloat)greenComponent blue:(GLfloat)blueComponent alpha:(GLfloat)alphaComponent +{ + backgroundColorRed = redComponent; + backgroundColorGreen = greenComponent; + backgroundColorBlue = blueComponent; + backgroundColorAlpha = alphaComponent; +} + ++ (const GLfloat *)textureCoordinatesForRotation:(GPUImageRotationMode)rotationMode +{ + static const GLfloat noRotationTextureCoordinates[] = { + 0.0f, 1.0f, + 1.0f, 1.0f, + 0.0f, 0.0f, + 1.0f, 0.0f, + }; + + static const GLfloat rotateRightTextureCoordinates[] = { + 1.0f, 1.0f, + 1.0f, 0.0f, + 0.0f, 1.0f, + 0.0f, 0.0f, + }; + + static const GLfloat rotateLeftTextureCoordinates[] = { + 0.0f, 0.0f, + 0.0f, 1.0f, + 1.0f, 0.0f, + 1.0f, 1.0f, + }; + + static const GLfloat verticalFlipTextureCoordinates[] = { + 0.0f, 0.0f, + 1.0f, 0.0f, + 0.0f, 1.0f, + 1.0f, 1.0f, + }; + + static const GLfloat horizontalFlipTextureCoordinates[] = { + 1.0f, 1.0f, + 0.0f, 1.0f, + 1.0f, 0.0f, + 0.0f, 0.0f, + }; + + static const GLfloat rotateRightVerticalFlipTextureCoordinates[] = { + 1.0f, 0.0f, + 1.0f, 1.0f, + 0.0f, 0.0f, + 0.0f, 1.0f, + }; + + static const GLfloat rotateRightHorizontalFlipTextureCoordinates[] = { + 1.0f, 1.0f, + 1.0f, 0.0f, + 0.0f, 1.0f, + 0.0f, 0.0f, + }; + + static const GLfloat rotate180TextureCoordinates[] = { + 1.0f, 0.0f, + 0.0f, 0.0f, + 1.0f, 1.0f, + 0.0f, 1.0f, + }; + + switch(rotationMode) + { + case kGPUImageNoRotation: return noRotationTextureCoordinates; + case kGPUImageRotateLeft: return rotateLeftTextureCoordinates; + case kGPUImageRotateRight: return rotateRightTextureCoordinates; + case kGPUImageFlipVertical: return verticalFlipTextureCoordinates; + case kGPUImageFlipHorizonal: return horizontalFlipTextureCoordinates; + case kGPUImageRotateRightFlipVertical: return rotateRightVerticalFlipTextureCoordinates; + case kGPUImageRotateRightFlipHorizontal: return rotateRightHorizontalFlipTextureCoordinates; + case kGPUImageRotate180: return rotate180TextureCoordinates; + } +} + +#pragma mark - Input + +- (void)newFrameReadyAtTime:(CMTime)__unused frameTime atIndex:(NSInteger)__unused textureIndex +{ + runSynchronouslyOnVideoProcessingQueue(^ + { + [GPUImageContext setActiveShaderProgram:displayProgram]; + [self setDisplayFramebuffer]; + + glClearColor(backgroundColorRed, backgroundColorGreen, backgroundColorBlue, backgroundColorAlpha); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + + glActiveTexture(GL_TEXTURE4); + glBindTexture(GL_TEXTURE_2D, [inputFramebufferForDisplay texture]); + glUniform1i(displayInputTextureUniform, 4); + + glVertexAttribPointer(displayPositionAttribute, 2, GL_FLOAT, 0, 0, imageVertices); + glVertexAttribPointer(displayTextureCoordinateAttribute, 2, GL_FLOAT, 0, 0, [PGPhotoEditorView textureCoordinatesForRotation:inputRotation]); + + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); + + [self presentFramebuffer]; + [inputFramebufferForDisplay unlock]; + inputFramebufferForDisplay = nil; + }); +} + +- (NSInteger)nextAvailableTextureIndex +{ + return 0; +} + +- (void)setInputFramebuffer:(GPUImageFramebuffer *)newInputFramebuffer atIndex:(NSInteger)__unused textureIndex +{ + inputFramebufferForDisplay = newInputFramebuffer; + [inputFramebufferForDisplay lock]; +} + +- (void)setInputRotation:(GPUImageRotationMode)newInputRotation atIndex:(NSInteger)__unused textureIndex +{ + inputRotation = newInputRotation; +} + +- (void)setInputSize:(CGSize)newSize atIndex:(NSInteger)__unused textureIndex +{ + runSynchronouslyOnVideoProcessingQueue(^{ + CGSize rotatedSize = newSize; + + if (GPUImageRotationSwapsWidthAndHeight(inputRotation)) + { + rotatedSize.width = newSize.height; + rotatedSize.height = newSize.width; + } + + if (!CGSizeEqualToSize(inputImageSize, rotatedSize)) + { + inputImageSize = rotatedSize; + [self recalculateViewGeometry]; + } + }); +} + +- (CGSize)maximumOutputSize +{ + if ([self respondsToSelector:@selector(setContentScaleFactor:)]) + { + CGSize pointSize = self.bounds.size; + return CGSizeMake(self.contentScaleFactor * pointSize.width, self.contentScaleFactor * pointSize.height); + } + else + { + return self.bounds.size; + } +} + +- (void)endProcessing +{ + +} + +- (BOOL)shouldIgnoreUpdatesToThisTarget +{ + return NO; +} + +- (BOOL)wantsMonochromeInput +{ + return NO; +} + +- (void)setCurrentlyReceivingMonochromeInput:(BOOL)__unused newValue +{ + +} + +#pragma mark - + +- (CGSize)sizeInPixels +{ + if (CGSizeEqualToSize(_sizeInPixels, CGSizeZero)) + { + return [self maximumOutputSize]; + } + else + { + return _sizeInPixels; + } +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGPhotoEnhanceColorConversionFilter.h b/submodules/LegacyComponents/LegacyComponents/PGPhotoEnhanceColorConversionFilter.h new file mode 100644 index 0000000000..a4e2c94cce --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGPhotoEnhanceColorConversionFilter.h @@ -0,0 +1,13 @@ +#import "GPUImageFilter.h" + +typedef enum +{ + PGPhotoEnhanceColorConversionRGBToHSVMode, + PGPhotoEnhanceColorConversionHSVToRGBMode +} PGPhotoEnhanceColorConversionMode; + +@interface PGPhotoEnhanceColorConversionFilter : GPUImageFilter + +- (instancetype)initWithMode:(PGPhotoEnhanceColorConversionMode)mode; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGPhotoEnhanceColorConversionFilter.m b/submodules/LegacyComponents/LegacyComponents/PGPhotoEnhanceColorConversionFilter.m new file mode 100644 index 0000000000..2ae93f94c2 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGPhotoEnhanceColorConversionFilter.m @@ -0,0 +1,56 @@ +#import "PGPhotoEnhanceColorConversionFilter.h" + +#import "PGPhotoProcessPass.h" + +NSString *const PGPhotoEnhanceRGBToHSVShaderString = PGShaderString +( + precision highp float; + + varying vec2 texCoord; + uniform sampler2D sourceImage; + + vec3 rgb_to_hsv(vec3 c) { + vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0); + vec4 p = c.g < c.b ? vec4(c.bg, K.wz) : vec4(c.gb, K.xy); + vec4 q = c.r < p.x ? vec4(p.xyw, c.r) : vec4(c.r, p.yzx); + + float d = q.x - min(q.w, q.y); + float e = 1.0e-10; + return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x); + } + + void main() { + vec4 texel = texture2D(sourceImage, texCoord); + + gl_FragColor = vec4(rgb_to_hsv(texel.rgb), texel.a); + } +); + +NSString *const PGPhotoEnhanceHSVToRGBShaderString = PGShaderString +( + precision highp float; + + varying vec2 texCoord; + uniform sampler2D sourceImage; + + vec3 hsv_to_rgb(vec3 c) { + vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0); + vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www); + return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y); + } + + void main() { + vec4 texel = texture2D(sourceImage, texCoord); + + gl_FragColor = vec4(hsv_to_rgb(texel.rgb), texel.a); + } +); + +@implementation PGPhotoEnhanceColorConversionFilter + +- (instancetype)initWithMode:(PGPhotoEnhanceColorConversionMode)mode +{ + return [super initWithFragmentShaderFromString:(mode == PGPhotoEnhanceColorConversionRGBToHSVMode) ? PGPhotoEnhanceRGBToHSVShaderString : PGPhotoEnhanceHSVToRGBShaderString]; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGPhotoEnhanceInterpolationFilter.h b/submodules/LegacyComponents/LegacyComponents/PGPhotoEnhanceInterpolationFilter.h new file mode 100644 index 0000000000..46ca5dbcd3 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGPhotoEnhanceInterpolationFilter.h @@ -0,0 +1,7 @@ +#import "GPUImageTwoInputFilter.h" + +@interface PGPhotoEnhanceInterpolationFilter : GPUImageTwoInputFilter + +@property (nonatomic, assign) CGFloat intensity; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGPhotoEnhanceInterpolationFilter.m b/submodules/LegacyComponents/LegacyComponents/PGPhotoEnhanceInterpolationFilter.m new file mode 100644 index 0000000000..981cc2d8a6 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGPhotoEnhanceInterpolationFilter.m @@ -0,0 +1,87 @@ +#import "PGPhotoEnhanceInterpolationFilter.h" + +#import "PGPhotoProcessPass.h" + +NSString *const PGPhotoEnhanceToolInterpolationShaderString = PGShaderString +( + precision highp float; + + varying vec2 texCoord; + uniform sampler2D sourceImage; + uniform sampler2D inputImageTexture2; + uniform float intensity; + + vec3 hsv_to_rgb(vec3 c) { + vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0); + vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www); + return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y); + } + + float enhance(float value) { + const vec2 offset = vec2(0.001953125, 0.03125); // vec2(0.5 / 256.0, 0.5 / 16.0) + value = value + offset.x; + + vec2 coord = (clamp(texCoord, 0.125, 1.0 - 0.125001) - 0.125) * 4.0; + vec2 frac = fract(coord); + coord = floor(coord); // vec2(0..3, 0..3) + + // 1.0 / 16.0 = 0.0625 + float p00 = float(coord.y * 4.0 + coord.x) * 0.0625 + offset.y; + float p01 = float(coord.y * 4.0 + coord.x + 1.0) * 0.0625 + offset.y; + float p10 = float((coord.y + 1.0) * 4.0 + coord.x) * 0.0625 + offset.y; + float p11 = float((coord.y + 1.0) * 4.0 + coord.x + 1.0) * 0.0625 + offset.y; + + vec3 c00 = texture2D(inputImageTexture2, vec2(value, p00)).rgb; + vec3 c01 = texture2D(inputImageTexture2, vec2(value, p01)).rgb; + vec3 c10 = texture2D(inputImageTexture2, vec2(value, p10)).rgb; + vec3 c11 = texture2D(inputImageTexture2, vec2(value, p11)).rgb; + + // r - cdf, g - cdfMin, b - cdfMax + float c1 = ((c00.r - c00.g) / (c00.b - c00.g)); + float c2 = ((c01.r - c01.g) / (c01.b - c01.g)); + float c3 = ((c10.r - c10.g) / (c10.b - c10.g)); + float c4 = ((c11.r - c11.g) / (c11.b - c11.g)); + + float c1_2 = mix(c1, c2, frac.x); + float c3_4 = mix(c3, c4, frac.x); + + return mix(c1_2, c3_4, frac.y); + } + + void main() { + vec4 texel = texture2D(sourceImage, texCoord); + vec4 hsv = texel; + + hsv.y = min(1.0, hsv.y * 1.2); + hsv.z = min(1.0, enhance(hsv.z) * 1.1); + + gl_FragColor = vec4(hsv_to_rgb(mix(texel.xyz, hsv.xyz, intensity)), texel.w); + } +); + +@interface PGPhotoEnhanceInterpolationFilter () +{ + GLint _intensityUniform; +} +@end + +@implementation PGPhotoEnhanceInterpolationFilter + +@dynamic intensity; + +- (instancetype)init +{ + self = [self initWithFragmentShaderFromString:PGPhotoEnhanceToolInterpolationShaderString]; + if (self != nil) + { + _intensityUniform = [filterProgram uniformIndex:@"intensity"]; + } + return self; +} + +- (void)setIntensity:(CGFloat)intensity +{ + [self setFloat:(float)intensity forUniformName:@"intensity"]; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGPhotoEnhanceLUTGenerator.h b/submodules/LegacyComponents/LegacyComponents/PGPhotoEnhanceLUTGenerator.h new file mode 100644 index 0000000000..dbf59e8b00 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGPhotoEnhanceLUTGenerator.h @@ -0,0 +1,11 @@ +#import "GPUImageContext.h" + +@interface PGPhotoEnhanceLUTGenerator : NSObject + +@property (nonatomic, copy) void(^lutDataReady)(GLubyte *data); +@property (nonatomic, assign) bool skip; + +@end + +extern const NSUInteger PGPhotoEnhanceHistogramBins; +extern const NSUInteger PGPhotoEnhanceSegments; diff --git a/submodules/LegacyComponents/LegacyComponents/PGPhotoEnhanceLUTGenerator.m b/submodules/LegacyComponents/LegacyComponents/PGPhotoEnhanceLUTGenerator.m new file mode 100644 index 0000000000..c87e7f58e2 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGPhotoEnhanceLUTGenerator.m @@ -0,0 +1,334 @@ +#import "PGPhotoEnhanceLUTGenerator.h" + +#import "LegacyComponentsInternal.h" + +#import "PGPhotoProcessPass.h" + +const NSUInteger PGPhotoEnhanceHistogramBins = 256; +const NSUInteger PGPhotoEnhanceSegments = 4; + +@interface PGPhotoEnhanceLUTGenerator () +{ + CGFloat _clipLimit; + + CGSize _imageSize; + GPUImageRotationMode _inputRotation; + + GPUImageFramebuffer *_firstInputFramebuffer; + GPUImageFramebuffer *_outputFramebuffer; + GPUImageFramebuffer *_retainedFramebuffer; + + GLProgram *_dataProgram; + GLint _dataPositionAttribute; + GLint _dataTextureCoordinateAttribute; + GLint _dataInputTextureUniform; + + bool _hasReadCurrentFrame; + + GLubyte *_rawBytesForImage; + bool _lockNextFramebuffer; +} + +@property (nonatomic, assign) BOOL enabled; + +@end + +@implementation PGPhotoEnhanceLUTGenerator + +- (instancetype)init +{ + self = [super init]; + if (self != nil) + { + _clipLimit = 1.25f; + + self.enabled = true; + _lockNextFramebuffer = false; + _inputRotation = kGPUImageNoRotation; + + [GPUImageContext useImageProcessingContext]; + + if (([GPUImageContext supportsFastTextureUpload])) + { + _dataProgram = [[GPUImageContext sharedImageProcessingContext] programForVertexShaderString:kGPUImageVertexShaderString fragmentShaderString:PGPhotoEnhanceColorSwapShaderString]; + } + else + { + _dataProgram = [[GPUImageContext sharedImageProcessingContext] programForVertexShaderString:kGPUImageVertexShaderString fragmentShaderString:kGPUImagePassthroughFragmentShaderString]; + } + + if (!_dataProgram.initialized) + { + [_dataProgram addAttribute:@"position"]; + [_dataProgram addAttribute:@"inputTexCoord"]; + + if (![_dataProgram link]) + { + NSString *progLog = [_dataProgram programLog]; + NSLog(@"Program link log: %@", progLog); + NSString *fragLog = [_dataProgram fragmentShaderLog]; + NSLog(@"Fragment shader compile log: %@", fragLog); + NSString *vertLog = [_dataProgram vertexShaderLog]; + NSLog(@"Vertex shader compile log: %@", vertLog); + _dataProgram = nil; + NSAssert(NO, @"Filter shader link failed"); + } + } + + _dataPositionAttribute = [_dataProgram attributeIndex:@"position"]; + _dataTextureCoordinateAttribute = [_dataProgram attributeIndex:@"inputTexCoord"]; + _dataInputTextureUniform = [_dataProgram uniformIndex:@"sourceImage"]; + } + return self; +} + +- (void)dealloc +{ + if (_rawBytesForImage != NULL && (![GPUImageContext supportsFastTextureUpload])) + { + free(_rawBytesForImage); + _rawBytesForImage = NULL; + } +} + +#pragma mark - GPUImageInput + +- (void)newFrameReadyAtTime:(CMTime)__unused frameTime atIndex:(NSInteger)__unused textureIndex +{ + if (self.skip) + return; + + _hasReadCurrentFrame = false; + _lockNextFramebuffer = true; + + NSUInteger totalSegments = PGPhotoEnhanceSegments * PGPhotoEnhanceSegments; + CGSize tileSize = CGSizeMake(CGFloor(_imageSize.width / PGPhotoEnhanceSegments), CGFloor(_imageSize.height / PGPhotoEnhanceSegments)); + NSUInteger tileArea = (NSUInteger)(tileSize.width * tileSize.height); + NSUInteger clipLimit = (NSUInteger)MAX(1, _clipLimit * tileArea / (CGFloat)PGPhotoEnhanceHistogramBins); + CGFloat scale = 255.0f / (CGFloat)tileArea; + + GLubyte *bytes = [self _rawBytes]; + NSUInteger bytesPerRow = [_retainedFramebuffer bytesPerRow]; + + NSUInteger hist[totalSegments][PGPhotoEnhanceHistogramBins]; + NSUInteger cdfs[totalSegments][PGPhotoEnhanceHistogramBins]; + NSUInteger cdfsMin[totalSegments]; + NSUInteger cdfsMax[totalSegments]; + + memset(hist, 0, totalSegments * PGPhotoEnhanceHistogramBins * sizeof(NSUInteger)); + memset(cdfs, 0, totalSegments * PGPhotoEnhanceHistogramBins * sizeof(NSUInteger)); + memset(cdfsMin, 0, totalSegments * sizeof(NSUInteger)); + memset(cdfsMax, 0, totalSegments * sizeof(NSUInteger)); + + CGFloat xMul = PGPhotoEnhanceSegments / _imageSize.width; + CGFloat yMul = PGPhotoEnhanceSegments / _imageSize.height; + + for (NSUInteger y = 0; y < _imageSize.height; y++) + { + NSUInteger yOffset = y * bytesPerRow; + for (NSUInteger x = 0; x < _imageSize.width; x++) + { + NSUInteger index = x * 4 + yOffset; + + NSUInteger tx = (NSUInteger)(x * xMul); + NSUInteger ty = (NSUInteger)(y * yMul); + NSUInteger t = ty * PGPhotoEnhanceSegments + tx; + + GLubyte value = bytes[index + 2]; + hist[t][value]++; + } + } + + [_retainedFramebuffer unlockAfterReading]; + [_retainedFramebuffer unlock]; + _retainedFramebuffer = nil; + + for (NSUInteger i = 0; i < totalSegments; i++) + { + if (clipLimit > 0) + { + NSUInteger clipped = 0; + for (NSUInteger j = 0; j < PGPhotoEnhanceHistogramBins; ++j) + { + if (hist[i][j] > clipLimit) + { + clipped += hist[i][j] - clipLimit; + hist[i][j] = clipLimit; + } + } + + NSUInteger redistBatch = clipped / PGPhotoEnhanceHistogramBins; + NSUInteger residual = clipped - redistBatch * PGPhotoEnhanceHistogramBins; + + for (NSUInteger j = 0; j < PGPhotoEnhanceHistogramBins; ++j) + hist[i][j] += redistBatch; + + for (NSUInteger j = 0; j < residual; ++j) + hist[i][j]++; + } + + memcpy(&cdfs[i], &hist[i], PGPhotoEnhanceHistogramBins * sizeof(NSUInteger)); + + NSUInteger hMin = PGPhotoEnhanceHistogramBins - 1; + for (NSUInteger j = 0; j < hMin; ++j) + { + if (cdfs[j] != 0) + hMin = j; + } + + NSUInteger cdf = 0; + for (NSUInteger j = hMin; j < PGPhotoEnhanceHistogramBins; ++j) + { + cdf += cdfs[i][j]; + cdfs[i][j] = (uint8_t)MIN(255, cdf * scale); + } + + cdfsMin[i] = cdfs[i][hMin]; + cdfsMax[i] = cdfs[i][PGPhotoEnhanceHistogramBins - 1]; + } + + NSUInteger resultSize = 4 * PGPhotoEnhanceHistogramBins * totalSegments; + NSUInteger resultBytesPerRow = 4 * PGPhotoEnhanceHistogramBins; + + GLubyte *result = calloc(resultSize, sizeof(GLubyte)); + for (NSUInteger tile = 0; tile < totalSegments; tile++) + { + NSUInteger yOffset = tile * resultBytesPerRow; + for (NSUInteger i = 0; i < PGPhotoEnhanceHistogramBins; i++) + { + NSUInteger index = i * 4 + yOffset; + result[index] = (uint8_t)cdfs[tile][i]; + result[index + 1] = (uint8_t)cdfsMin[tile]; + result[index + 2] = (uint8_t)cdfsMax[tile]; + } + } + + if (self.lutDataReady != nil) + self.lutDataReady(result); +} + +- (NSInteger)nextAvailableTextureIndex +{ + return 0; +} + +- (void)setInputFramebuffer:(GPUImageFramebuffer *)newInputFramebuffer atIndex:(NSInteger)__unused textureIndex +{ + _firstInputFramebuffer = newInputFramebuffer; + [_firstInputFramebuffer lock]; +} + +- (void)setInputRotation:(GPUImageRotationMode)newInputRotation atIndex:(NSInteger)__unused textureIndex +{ + _inputRotation = newInputRotation; +} + +- (void)setInputSize:(CGSize)newSize atIndex:(NSInteger)__unused textureIndex +{ + _imageSize = newSize; +} + +- (CGSize)maximumOutputSize +{ + return CGSizeMake(PGPhotoEnhanceHistogramBins, PGPhotoEnhanceSegments * PGPhotoEnhanceSegments); +} + +- (void)endProcessing +{ + +} + +- (BOOL)shouldIgnoreUpdatesToThisTarget +{ + return false; +} + +- (BOOL)wantsMonochromeInput +{ + return false; +} + +- (void)setCurrentlyReceivingMonochromeInput:(BOOL)__unused newValue +{ + +} + +- (void)_render +{ + [GPUImageContext setActiveShaderProgram:_dataProgram]; + + _outputFramebuffer = [[GPUImageContext sharedFramebufferCache] fetchFramebufferForSize:_imageSize onlyTexture:false]; + [_outputFramebuffer activateFramebuffer]; + + if(_lockNextFramebuffer) + { + _retainedFramebuffer = _outputFramebuffer; + [_retainedFramebuffer lock]; + [_retainedFramebuffer lockForReading]; + _lockNextFramebuffer = false; + } + + glClearColor(0.0f, 0.0f, 0.0f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + + static const GLfloat squareVertices[] = { + -1.0f, -1.0f, + 1.0f, -1.0f, + -1.0f, 1.0f, + 1.0f, 1.0f, + }; + + static const GLfloat textureCoordinates[] = { + 0.0f, 0.0f, + 1.0f, 0.0f, + 0.0f, 1.0f, + 1.0f, 1.0f, + }; + + glActiveTexture(GL_TEXTURE4); + glBindTexture(GL_TEXTURE_2D, [_firstInputFramebuffer texture]); + glUniform1i(_dataInputTextureUniform, 4); + + glVertexAttribPointer(_dataPositionAttribute, 2, GL_FLOAT, 0, 0, squareVertices); + glVertexAttribPointer(_dataTextureCoordinateAttribute, 2, GL_FLOAT, 0, 0, textureCoordinates); + + glEnableVertexAttribArray(_dataPositionAttribute); + glEnableVertexAttribArray(_dataTextureCoordinateAttribute); + + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); + [_firstInputFramebuffer unlock]; +} + +- (GLubyte *)_rawBytes +{ + if ((_rawBytesForImage == NULL) && (![GPUImageContext supportsFastTextureUpload])) + { + _rawBytesForImage = (GLubyte *)calloc((NSInteger)(_imageSize.width * _imageSize.height) * 4, sizeof(GLubyte)); + _hasReadCurrentFrame = false; + } + + if (!_hasReadCurrentFrame) + { + runSynchronouslyOnVideoProcessingQueue(^ + { + [GPUImageContext useImageProcessingContext]; + [self _render]; + + if ([GPUImageContext supportsFastTextureUpload]) + { + glFinish(); + _rawBytesForImage = [_outputFramebuffer byteBuffer]; + } + else + { + glReadPixels(0, 0, (GLsizei)_imageSize.width, (GLsizei)_imageSize.height, GL_RGBA, GL_UNSIGNED_BYTE, _rawBytesForImage); + } + + _hasReadCurrentFrame = true; + }); + } + + return _rawBytesForImage; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGPhotoEnhancePass.h b/submodules/LegacyComponents/LegacyComponents/PGPhotoEnhancePass.h new file mode 100644 index 0000000000..8af1c95299 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGPhotoEnhancePass.h @@ -0,0 +1,7 @@ +#import "PGPhotoProcessPass.h" + +@interface PGPhotoEnhancePass : PGPhotoProcessPass + +@property (nonatomic, assign) CGFloat intensity; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGPhotoEnhancePass.m b/submodules/LegacyComponents/LegacyComponents/PGPhotoEnhancePass.m new file mode 100644 index 0000000000..9ca59767ee --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGPhotoEnhancePass.m @@ -0,0 +1,192 @@ +#import "PGPhotoEnhancePass.h" + +#import "PGPhotoEnhanceColorConversionFilter.h" +#import "PGPhotoEnhanceLUTGenerator.h" +#import "PGPhotoEnhanceInterpolationFilter.h" +#import "PGPhotoEditorRawDataInput.h" + +@interface PGPhotoEnhanceFilter : GPUImageOutput +{ + GPUImageOutput *_initialFilter; + + PGPhotoEnhanceColorConversionFilter *_rgbToHsvFilter; + PGPhotoEnhanceLUTGenerator *_lutGenerator; + PGPhotoEditorRawDataInput *_lutDataInput; + PGPhotoEnhanceInterpolationFilter *_interpolationFilter; + + bool _hasLutData; + + bool _endProcessing; +} + +@property (nonatomic, assign) CGFloat intensity; + +@end + +@implementation PGPhotoEnhanceFilter + +- (instancetype)init +{ + self = [super init]; + if (self != nil) + { + _rgbToHsvFilter = [[PGPhotoEnhanceColorConversionFilter alloc] initWithMode:PGPhotoEnhanceColorConversionRGBToHSVMode]; + _initialFilter = _rgbToHsvFilter; + + _lutGenerator = [[PGPhotoEnhanceLUTGenerator alloc] init]; + [_rgbToHsvFilter addTarget:_lutGenerator]; + + CGSize lutSize = CGSizeMake(PGPhotoEnhanceHistogramBins, PGPhotoEnhanceSegments * PGPhotoEnhanceSegments); + + _lutDataInput = [[PGPhotoEditorRawDataInput alloc] initWithBytes:NULL size:lutSize pixelFormat:GPUPixelFormatRGBA type:GPUPixelTypeUByte]; + + __weak PGPhotoEnhanceFilter *weakSelf = self; + _lutGenerator.lutDataReady = ^(GLubyte *lutData) + { + __strong PGPhotoEnhanceFilter *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [strongSelf->_lutDataInput updateDataWithBytes:lutData size:lutSize]; + strongSelf->_hasLutData = true; + [strongSelf->_lutDataInput processData]; + }; + + _interpolationFilter = [[PGPhotoEnhanceInterpolationFilter alloc] init]; + [_rgbToHsvFilter addTarget:_interpolationFilter atTextureLocation:0]; + [_lutDataInput addTarget:_interpolationFilter atTextureLocation:1]; + } + return self; +} + +- (void)setIntensity:(CGFloat)intensity +{ + _intensity = intensity; + + [_interpolationFilter setIntensity:intensity]; +} + +#pragma mark GPUImageOutput + +- (void)setTargetToIgnoreForUpdates:(id)targetToIgnoreForUpdates +{ + [_interpolationFilter setTargetToIgnoreForUpdates:targetToIgnoreForUpdates]; +} + +- (void)addTarget:(id)newTarget atTextureLocation:(NSInteger)textureLocation +{ + [_interpolationFilter addTarget:newTarget atTextureLocation:textureLocation]; +} + +- (void)removeTarget:(id)targetToRemove +{ + [_interpolationFilter removeTarget:targetToRemove]; +} + +- (void)removeAllTargets +{ + [_interpolationFilter removeAllTargets]; +} + +- (void)setFrameProcessingCompletionBlock:(void (^)(GPUImageOutput *, CMTime))frameProcessingCompletionBlock +{ + [_interpolationFilter setFrameProcessingCompletionBlock:frameProcessingCompletionBlock]; +} + +- (void (^)(GPUImageOutput *, CMTime))frameProcessingCompletionBlock +{ + return [_interpolationFilter frameProcessingCompletionBlock]; +} + +#pragma mark - GPUImageInput + +- (void)newFrameReadyAtTime:(CMTime)frameTime atIndex:(NSInteger)textureIndex +{ + _lutGenerator.skip = _hasLutData; + if (_hasLutData) + [_lutDataInput processData]; + + [_initialFilter newFrameReadyAtTime:frameTime atIndex:textureIndex]; +} + +- (void)setInputFramebuffer:(GPUImageFramebuffer *)newInputFramebuffer atIndex:(NSInteger)textureIndex +{ + [_initialFilter setInputFramebuffer:newInputFramebuffer atIndex:textureIndex]; +} + +- (NSInteger)nextAvailableTextureIndex +{ + return 0; +} + +- (void)setInputSize:(CGSize)newSize atIndex:(NSInteger)textureIndex +{ + [_initialFilter setInputSize:newSize atIndex:textureIndex]; +} + +- (void)setInputRotation:(GPUImageRotationMode)newInputRotation atIndex:(NSInteger)textureIndex +{ + [_initialFilter setInputRotation:newInputRotation atIndex:textureIndex]; +} + +- (CGSize)maximumOutputSize +{ + return CGSizeZero; +} + +- (void)endProcessing +{ + if (!_endProcessing) + { + _endProcessing = true; + [_initialFilter endProcessing]; + } +} + +- (BOOL)wantsMonochromeInput +{ + return false; +} + +- (void)setCurrentlyReceivingMonochromeInput:(BOOL)__unused newValue +{ + +} + +- (void)invalidate +{ + _hasLutData = false; +} + +@end + +@implementation PGPhotoEnhancePass + +- (instancetype)init +{ + self = [super init]; + if (self != nil) + { + PGPhotoEnhanceFilter *filter = [[PGPhotoEnhanceFilter alloc] init]; + _filter = filter; + } + return self; +} + +- (void)setIntensity:(CGFloat)intensity +{ + _intensity = intensity; + [self updateParameters]; +} + +- (void)updateParameters +{ + [(PGPhotoEnhanceFilter *)_filter setIntensity:_intensity]; +} + +- (void)invalidate +{ + [(PGPhotoEnhanceFilter *)_filter invalidate]; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGPhotoFilter.h b/submodules/LegacyComponents/LegacyComponents/PGPhotoFilter.h new file mode 100644 index 0000000000..eddd041ec5 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGPhotoFilter.h @@ -0,0 +1,19 @@ +#import "PGPhotoEditorItem.h" + +@class PGPhotoFilterDefinition; +@class PGPhotoProcessPass; + +@interface PGPhotoFilter : NSObject +{ + PGPhotoProcessPass *_pass; +} + +@property (nonatomic, readonly) PGPhotoFilterDefinition *definition; +@property (nonatomic, retain) PGPhotoProcessPass *pass; +@property (nonatomic, readonly) PGPhotoProcessPass *optimizedPass; + +- (void)invalidate; + ++ (PGPhotoFilter *)filterWithDefinition:(PGPhotoFilterDefinition *)definition; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGPhotoFilter.m b/submodules/LegacyComponents/LegacyComponents/PGPhotoFilter.m new file mode 100644 index 0000000000..f37c7bef8d --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGPhotoFilter.m @@ -0,0 +1,242 @@ +#import "PGPhotoFilter.h" + +#import "TGPhotoEditorGenericToolView.h" + +#import "PGPhotoFilterDefinition.h" + +#import "PGPhotoCustomFilterPass.h" +#import "PGPhotoLookupFilterPass.h" +#import "PGPhotoProcessPass.h" + +@interface PGPhotoFilter () +{ + PGPhotoProcessPass *_parameter; +} +@end + +@implementation PGPhotoFilter + +@synthesize value = _value; +@synthesize tempValue = _tempValue; +@synthesize parameters = _parameters; +@synthesize beingEdited = _beingEdited; +@synthesize shouldBeSkipped = _shouldBeSkipped; +@synthesize parametersChanged = _parametersChanged; +@synthesize disabled = _disabled; +@synthesize segmented = _segmented; + +- (instancetype)initWithDefinition:(PGPhotoFilterDefinition *)definition +{ + self = [super init]; + if (self != nil) + { + _definition = definition; + _value = @(self.defaultValue); + } + return self; +} + +- (instancetype)copyWithZone:(NSZone *)__unused zone +{ + PGPhotoFilter *filter = [[PGPhotoFilter alloc] initWithDefinition:self.definition]; + filter.value = self.value; + return filter; +} + +- (NSString *)title +{ + return _definition.title; +} + +- (NSString *)identifier +{ + return _definition.identifier; +} + +- (PGPhotoProcessPass *)pass +{ + if (_pass == nil) + { + switch (_definition.type) + { + case PGPhotoFilterTypeCustom: + { + _pass = [[PGPhotoCustomFilterPass alloc] initWithShaderFile:_definition.shaderFilename textureFiles:_definition.textureFilenames]; + } + break; + + case PGPhotoFilterTypeLookup: + { + _pass = [[PGPhotoLookupFilterPass alloc] initWithLookupImage:[UIImage imageNamed:[NSString stringWithFormat:@"%@.png", _definition.lookupFilename]]]; + } + break; + + default: + { + _pass = [[PGPhotoProcessPass alloc] init]; + } + break; + } + } + + [self updatePassParameters]; + + return _pass; +} + +- (PGPhotoProcessPass *)optimizedPass +{ + switch (_definition.type) + { + case PGPhotoFilterTypeCustom: + { + return [[PGPhotoCustomFilterPass alloc] initWithShaderFile:_definition.shaderFilename textureFiles:_definition.textureFilenames optimized:true]; + } + break; + + case PGPhotoFilterTypeLookup: + { + return [[PGPhotoLookupFilterPass alloc] initWithLookupImage:[UIImage imageNamed:[NSString stringWithFormat:@"%@.png", _definition.lookupFilename]]]; + } + break; + + default: + break; + } + + return [[PGPhotoProcessPass alloc] init]; +} + +- (Class)valueClass +{ + return [NSNumber class]; +} + +- (CGFloat)minimumValue +{ + return 0.0f; +} + +- (CGFloat)maximumValue +{ + return 100.0f; +} + +- (CGFloat)defaultValue +{ + return 100.0f; +} + +- (id)tempValue +{ + if (self.disabled) + { + if ([_tempValue isKindOfClass:[NSNumber class]]) + return @0; + } + + return _tempValue; +} + +- (id)displayValue +{ + if (self.beingEdited) + return self.tempValue; + + return self.value; +} + +- (void)setValue:(id)value +{ + _value = value; + + if (!self.beingEdited) + [self updateParameters]; +} + +- (void)setTempValue:(id)tempValue +{ + _tempValue = tempValue; + + if (self.beingEdited) + [self updateParameters]; +} + +- (NSArray *)parameters +{ + return _parameters; +} + +- (void)updateParameters +{ + +} + +- (void)updatePassParameters +{ + CGFloat value = ((NSNumber *)self.displayValue).floatValue / self.maximumValue; + + if ([_pass isKindOfClass:[PGPhotoLookupFilterPass class]]) + { + PGPhotoLookupFilterPass *pass = (PGPhotoLookupFilterPass *)_pass; + [pass setIntensity:value]; + } + else if ([_pass isKindOfClass:[PGPhotoCustomFilterPass class]]) + { + PGPhotoCustomFilterPass *pass = (PGPhotoCustomFilterPass *)_pass; + [pass setIntensity:value]; + } +} + +- (void)invalidate +{ + _pass = nil; + _value = @(self.defaultValue); +} + +- (void)reset +{ + [_pass.filter removeAllTargets]; +} + +- (id)itemControlViewWithChangeBlock:(void (^)(id newValue, bool animated))changeBlock +{ + __weak PGPhotoFilter *weakSelf = self; + + id view = [[TGPhotoEditorGenericToolView alloc] initWithEditorItem:self]; + view.valueChanged = ^(id newValue, bool animated) + { + __strong PGPhotoFilter *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + strongSelf.tempValue = newValue; + + if (changeBlock != nil) + changeBlock(newValue, animated); + }; + return view; +} + +- (UIView *)itemAreaViewWithChangeBlock:(void (^)(id newValue))__unused changeBlock +{ + return nil; +} + +- (BOOL)isEqual:(id)object +{ + if (object == self) + return YES; + + if (!object || ![object isKindOfClass:[self class]]) + return NO; + + return ([[(PGPhotoFilter *)object definition].identifier isEqualToString:self.definition.identifier]); +} + ++ (PGPhotoFilter *)filterWithDefinition:(PGPhotoFilterDefinition *)definition +{ + return [[[self class] alloc] initWithDefinition:definition]; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGPhotoFilterDefinition.h b/submodules/LegacyComponents/LegacyComponents/PGPhotoFilterDefinition.h new file mode 100644 index 0000000000..1c15ed0746 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGPhotoFilterDefinition.h @@ -0,0 +1,21 @@ +#import + +typedef enum { + PGPhotoFilterTypePassThrough, + PGPhotoFilterTypeLookup, + PGPhotoFilterTypeCustom +} PGPhotoFilterType; + +@interface PGPhotoFilterDefinition : NSObject + +@property (readonly, nonatomic) NSString *identifier; +@property (readonly, nonatomic) NSString *title; +@property (readonly, nonatomic) PGPhotoFilterType type; +@property (readonly, nonatomic) NSString *lookupFilename; +@property (readonly, nonatomic) NSString *shaderFilename; +@property (readonly, nonatomic) NSArray *textureFilenames; + ++ (PGPhotoFilterDefinition *)originalFilterDefinition; ++ (PGPhotoFilterDefinition *)definitionWithDictionary:(NSDictionary *)dictionary; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGPhotoFilterDefinition.m b/submodules/LegacyComponents/LegacyComponents/PGPhotoFilterDefinition.m new file mode 100644 index 0000000000..f2f3b27995 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGPhotoFilterDefinition.m @@ -0,0 +1,35 @@ +#import "PGPhotoFilterDefinition.h" + +@implementation PGPhotoFilterDefinition + ++ (PGPhotoFilterDefinition *)originalFilterDefinition +{ + PGPhotoFilterDefinition *definition = [[PGPhotoFilterDefinition alloc] init]; + definition->_type = PGPhotoFilterTypePassThrough; + definition->_identifier = @"0_0"; + definition->_title = @"Original"; + + return definition; +} + ++ (PGPhotoFilterDefinition *)definitionWithDictionary:(NSDictionary *)dictionary +{ + PGPhotoFilterDefinition *definition = [[PGPhotoFilterDefinition alloc] init]; + + if ([dictionary[@"type"] isEqualToString:@"lookup"]) + definition->_type = PGPhotoFilterTypeLookup; + else if ([dictionary[@"type"] isEqualToString:@"custom"]) + definition->_type = PGPhotoFilterTypeCustom; + else + return nil; + + definition->_identifier = dictionary[@"id"]; + definition->_title = dictionary[@"title"]; + definition->_lookupFilename = dictionary[@"lookup_name"]; + definition->_shaderFilename = dictionary[@"shader_name"]; + definition->_textureFilenames = dictionary[@"texture_names"]; + + return definition; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGPhotoFilterThumbnailManager.h b/submodules/LegacyComponents/LegacyComponents/PGPhotoFilterThumbnailManager.h new file mode 100644 index 0000000000..b215d589ce --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGPhotoFilterThumbnailManager.h @@ -0,0 +1,20 @@ +#import +#import + +@class PGPhotoEditor; +@class PGPhotoFilter; + +@interface PGPhotoFilterThumbnailManager : NSObject + +@property (nonatomic, weak) PGPhotoEditor *photoEditor; + +- (void)setThumbnailImage:(UIImage *)image; +- (void)requestThumbnailImageForFilter:(PGPhotoFilter *)filter completion:(void (^)(UIImage *thumbnailImage, bool cached, bool finished))completion; +- (void)startCachingThumbnailImagesForFilters:(NSArray *)filters; +- (void)stopCachingThumbnailImagesForFilters:(NSArray *)filters; +- (void)stopCachingThumbnailImagesForAllFilters; +- (void)invalidateThumbnailImages; + +- (void)haltCaching; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGPhotoFilterThumbnailManager.m b/submodules/LegacyComponents/LegacyComponents/PGPhotoFilterThumbnailManager.m new file mode 100644 index 0000000000..c1594aa32e --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGPhotoFilterThumbnailManager.m @@ -0,0 +1,277 @@ +#import "PGPhotoFilterThumbnailManager.h" + +#import +#import + +#import + +#import "PGPhotoEditor.h" +#import "PGPhotoFilter.h" +#import "PGPhotoFilterDefinition.h" +#import "PGPhotoProcessPass.h" +#import "PGPhotoEditorPicture.h" + +const NSUInteger TGFilterThumbnailCacheSoftMemoryLimit = 2 * 1024 * 1024; +const NSUInteger TGFilterThumbnailCacheHardMemoryLimit = 2 * 1024 * 1024; + +@interface PGPhotoFilterThumbnailManager () +{ + TGMemoryImageCache *_filterThumbnailCache; + SQueue *_cachingQueue; + + UIImage *_thumbnailImage; + PGPhotoEditorPicture *_thumbnailPicture; + + SQueue *_filteringQueue; + dispatch_queue_t _prepQueue; + + pthread_rwlock_t _callbackLock; + NSMutableDictionary *_callbacksForId; + + NSInteger _version; +} + +@end + +@implementation PGPhotoFilterThumbnailManager + +- (instancetype)init +{ + self = [super init]; + if (self != nil) + { + [self invalidateThumbnailImages]; + + _prepQueue = dispatch_queue_create("ph.pictogra.Pictograph.FilterThumbnailQueue", DISPATCH_QUEUE_CONCURRENT); + + _cachingQueue = [[SQueue alloc] init]; + _callbacksForId = [[NSMutableDictionary alloc] init]; + + _filteringQueue = [[SQueue alloc] init]; + pthread_rwlock_init(&_callbackLock, NULL); + } + return self; +} + +- (void)setThumbnailImage:(UIImage *)image +{ + [self invalidateThumbnailImages]; + _thumbnailImage = image; + + //_thumbnailPicture = [[PGPhotoEditorPicture alloc] initWithImage:_thumbnailImage]; +} + +- (void)requestThumbnailImageForFilter:(PGPhotoFilter *)filter completion:(void (^)(UIImage *image, bool cached, bool finished))completion +{ + if (filter.definition.type == PGPhotoFilterTypePassThrough) + { + if (completion != nil) + { + if (_thumbnailImage != nil) + completion(_thumbnailImage, true, true); + else + completion(nil, true, false); + } + + return; + } + + UIImage *cachedImage = [_filterThumbnailCache imageForKey:filter.identifier attributes:nil]; + if (cachedImage != nil) + { + if (completion != nil) + completion(cachedImage, true, true); + return; + } + + if (_thumbnailImage == nil) + { + completion(nil, true, true); + return; + } + + if (completion != nil) + completion(_thumbnailImage, true, false); + + NSInteger version = _version; + + __weak PGPhotoFilterThumbnailManager *weakSelf = self; + [self _addCallback:completion forId:filter.identifier createCallback:^ + { + __strong PGPhotoFilterThumbnailManager *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + if (version != strongSelf->_version) + return; + + [strongSelf renderFilterThumbnailWithPicture:strongSelf->_thumbnailPicture filter:filter completion:^(UIImage *result) + { + [strongSelf _processCompletionForId:filter.identifier withResult:result]; + }]; + }]; +} + +- (void)startCachingThumbnailImagesForFilters:(NSArray *)filters +{ + if (_thumbnailImage == nil) + return; + + NSMutableArray *filtersToStartCaching = [[NSMutableArray alloc] init]; + + for (PGPhotoFilter *filter in filters) + { + if (filter.definition.type != PGPhotoFilterTypePassThrough && [_filterThumbnailCache imageForKey:filter.identifier attributes:nil] == nil) + [filtersToStartCaching addObject:filter]; + } + + NSInteger version = _version; + + [_cachingQueue dispatch:^ + { + if (version != _version) + return; + + for (PGPhotoFilter *filter in filtersToStartCaching) + { + __weak PGPhotoFilterThumbnailManager *weakSelf = self; + [self _addCallback:nil forId:filter.identifier createCallback:^ + { + __strong PGPhotoFilterThumbnailManager *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + if (version != strongSelf->_version) + return; + + [strongSelf renderFilterThumbnailWithPicture:strongSelf->_thumbnailPicture filter:filter completion:^(UIImage *result) + { + [strongSelf _processCompletionForId:filter.identifier withResult:result]; + }]; + }]; + } + }]; +} + +- (void)stopCachingThumbnailImagesForFilters:(NSArray *)__unused filters +{ + +} + +- (void)stopCachingThumbnailImagesForAllFilters +{ + +} + +- (void)_processCompletionForId:(NSString *)filterId withResult:(UIImage *)result +{ + [_filterThumbnailCache setImage:result forKey:filterId attributes:nil]; + + NSArray *callbacks = [self _callbacksForId:filterId]; + [self _removeCallbacksForId:filterId]; + + for (id callback in callbacks) + { + void(^callbackBlock)(UIImage *image, bool cached, bool finished) = callback; + if (callbackBlock != nil) + callbackBlock(result, false, true); + } +} + +- (void)renderFilterThumbnailWithPicture:(PGPhotoEditorPicture *)picture filter:(PGPhotoFilter *)filter completion:(void (^)(UIImage *result))completion +{ + PGPhotoEditor *photoEditor = self.photoEditor; + if (photoEditor == nil) + return; + + NSInteger version = _version; + dispatch_async(_prepQueue, ^ + { + GPUImageOutput *gpuFilter = filter.optimizedPass.filter; + [_filteringQueue dispatch:^ + { + if (version != _version) + return; + + [picture addTarget:gpuFilter]; + [gpuFilter useNextFrameForImageCapture]; + [picture processSynchronous:true completion:^ + { + UIImage *image = [gpuFilter imageFromCurrentFramebufferWithOrientation:UIImageOrientationUp]; + [picture removeAllTargets]; + + if (completion != nil) + completion(image); + }]; + }]; + }); +} + +- (void)invalidateThumbnailImages +{ + _version = lrand48(); + + _filterThumbnailCache = [[TGMemoryImageCache alloc] initWithSoftMemoryLimit:TGFilterThumbnailCacheSoftMemoryLimit + hardMemoryLimit:TGFilterThumbnailCacheHardMemoryLimit]; +} + +- (void)haltCaching +{ + _version = lrand48(); +} + +- (void)_addCallback:(void (^)(UIImage *, bool, bool))callback forId:(NSString *)filterId createCallback:(void (^)(void))createCallback +{ + if (filterId == nil) + { + callback(nil, true, false); + return; + } + + pthread_rwlock_rdlock(&_callbackLock); + + bool isInitial = false; + if (_callbacksForId[filterId] == nil) + { + isInitial = true; + _callbacksForId[filterId] = [[NSMutableArray alloc] init]; + } + + if (callback != nil) + { + NSMutableArray *callbacksForId = _callbacksForId[filterId]; + [callbacksForId addObject:callback]; + _callbacksForId[filterId] = callbacksForId; + } + + if (isInitial && createCallback != nil) + createCallback(); + + pthread_rwlock_unlock(&_callbackLock); +} + +- (NSArray *)_callbacksForId:(NSString *)filterId +{ + if (filterId == nil) + return nil; + + __block NSArray *callbacksForId; + + pthread_rwlock_rdlock(&_callbackLock); + callbacksForId = _callbacksForId[filterId]; + pthread_rwlock_unlock(&_callbackLock); + + return [callbacksForId copy]; +} + +- (void)_removeCallbacksForId:(NSString *)filterId +{ + if (filterId == nil) + return; + + pthread_rwlock_rdlock(&_callbackLock); + [_callbacksForId removeObjectForKey:filterId]; + pthread_rwlock_unlock(&_callbackLock); +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGPhotoGaussianBlurFilter.h b/submodules/LegacyComponents/LegacyComponents/PGPhotoGaussianBlurFilter.h new file mode 100644 index 0000000000..e34f1cb089 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGPhotoGaussianBlurFilter.h @@ -0,0 +1,5 @@ +#import "GPUImageFilter.h" + +@interface PGPhotoGaussianBlurFilter : GPUImageFilter + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGPhotoGaussianBlurFilter.m b/submodules/LegacyComponents/LegacyComponents/PGPhotoGaussianBlurFilter.m new file mode 100644 index 0000000000..575f2a01b3 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGPhotoGaussianBlurFilter.m @@ -0,0 +1,497 @@ +#import "PGPhotoGaussianBlurFIlter.h" + +#import "PGPhotoProcessPass.h" + +@interface PGPhotoGaussianBlurFilter () +{ + GPUImageFramebuffer *_secondOutputFramebuffer; + + GLProgram *_secondFilterProgram; + GLint _secondFilterPositionAttribute; + GLint _secondFilterTextureCoordinateAttribute; + GLint _secondFilterInputTextureUniform; + + GLint _verticalPassTexelWidthOffsetUniform; + GLint _verticalPassTexelHeightOffsetUniform; + GLint _horizontalPassTexelWidthOffsetUniform; + GLint _horizontalPassTexelHeightOffsetUniform; + + GLfloat _verticalPassTexelWidthOffset; + GLfloat _verticalPassTexelHeightOffset; + GLfloat _horizontalPassTexelWidthOffset; + GLfloat _horizontalPassTexelHeightOffset; + + CGFloat _verticalTexelSpacing; + CGFloat _horizontalTexelSpacing; + + NSMutableDictionary *_secondProgramUniformStateRestorationBlocks; + + NSUInteger _currentRadius; +} + +@end + +@implementation PGPhotoGaussianBlurFilter + ++ (NSString *)vertexShaderForBlurOfRadius:(NSUInteger)blurRadius sigma:(CGFloat)sigma +{ + if (blurRadius < 1) + { + return kGPUImageVertexShaderString; + } + + // First, generate the normal Gaussian weights for a given sigma + GLfloat *standardGaussianWeights = calloc(blurRadius + 1, sizeof(GLfloat)); + GLfloat sumOfWeights = 0.0; + for (NSUInteger currentGaussianWeightIndex = 0; currentGaussianWeightIndex < blurRadius + 1; currentGaussianWeightIndex++) + { + standardGaussianWeights[currentGaussianWeightIndex] = (GLfloat)((1.0 / sqrt(2.0 * M_PI * pow(sigma, 2.0))) * exp(-pow(currentGaussianWeightIndex, 2.0) / (2.0 * pow(sigma, 2.0)))); + + if (currentGaussianWeightIndex == 0) + sumOfWeights += standardGaussianWeights[currentGaussianWeightIndex]; + else + sumOfWeights += 2.0 * standardGaussianWeights[currentGaussianWeightIndex]; + } + + for (NSUInteger currentGaussianWeightIndex = 0; currentGaussianWeightIndex < blurRadius + 1; currentGaussianWeightIndex++) + standardGaussianWeights[currentGaussianWeightIndex] = standardGaussianWeights[currentGaussianWeightIndex] / sumOfWeights; + + NSUInteger numberOfOptimizedOffsets = MIN(blurRadius / 2 + (blurRadius % 2), 7U); + GLfloat *optimizedGaussianOffsets = calloc(numberOfOptimizedOffsets, sizeof(GLfloat)); + + for (NSUInteger currentOptimizedOffset = 0; currentOptimizedOffset < numberOfOptimizedOffsets; currentOptimizedOffset++) + { + GLfloat firstWeight = standardGaussianWeights[currentOptimizedOffset*2 + 1]; + GLfloat secondWeight = standardGaussianWeights[currentOptimizedOffset*2 + 2]; + + GLfloat optimizedWeight = firstWeight + secondWeight; + + optimizedGaussianOffsets[currentOptimizedOffset] = (firstWeight * (currentOptimizedOffset*2 + 1) + secondWeight * (currentOptimizedOffset*2 + 2)) / optimizedWeight; + } + + NSMutableString *shaderString = [[NSMutableString alloc] init]; + [shaderString appendFormat:@"\ + attribute vec4 position;\n\ + attribute vec4 inputTexCoord;\n\ + \n\ + uniform float texelWidthOffset;\n\ + uniform float texelHeightOffset;\n\ + \n\ + varying vec2 blurCoordinates[%lu];\n\ + \n\ + void main()\n\ + {\n\ + gl_Position = position;\n\ + \n\ + vec2 singleStepOffset = vec2(texelWidthOffset, texelHeightOffset);\n", (unsigned long)(1 + (numberOfOptimizedOffsets * 2))]; + + [shaderString appendString:@"blurCoordinates[0] = inputTexCoord.xy;\n"]; + for (NSUInteger currentOptimizedOffset = 0; currentOptimizedOffset < numberOfOptimizedOffsets; currentOptimizedOffset++) + { + [shaderString appendFormat:@"\ + blurCoordinates[%lu] = inputTexCoord.xy + singleStepOffset * %f;\n\ + blurCoordinates[%lu] = inputTexCoord.xy - singleStepOffset * %f;\n", (unsigned long)((currentOptimizedOffset * 2) + 1), optimizedGaussianOffsets[currentOptimizedOffset], (unsigned long)((currentOptimizedOffset * 2) + 2), optimizedGaussianOffsets[currentOptimizedOffset]]; + } + + [shaderString appendString:@"}\n"]; + + free(optimizedGaussianOffsets); + free(standardGaussianWeights); + return shaderString; +} + ++ (NSString *)fragmentShaderForBlurOfRadius:(NSUInteger)blurRadius sigma:(CGFloat)sigma +{ + if (blurRadius < 1) + return kGPUImagePassthroughFragmentShaderString; + + GLfloat *standardGaussianWeights = calloc(blurRadius + 1, sizeof(GLfloat)); + GLfloat sumOfWeights = 0.0; + for (NSUInteger currentGaussianWeightIndex = 0; currentGaussianWeightIndex < blurRadius + 1; currentGaussianWeightIndex++) + { + standardGaussianWeights[currentGaussianWeightIndex] = (GLfloat)((1.0 / sqrt(2.0 * M_PI * pow(sigma, 2.0))) * exp(-pow(currentGaussianWeightIndex, 2.0) / (2.0 * pow(sigma, 2.0)))); + + if (currentGaussianWeightIndex == 0) + { + sumOfWeights += standardGaussianWeights[currentGaussianWeightIndex]; + } + else + { + sumOfWeights += 2.0 * standardGaussianWeights[currentGaussianWeightIndex]; + } + } + + for (NSUInteger currentGaussianWeightIndex = 0; currentGaussianWeightIndex < blurRadius + 1; currentGaussianWeightIndex++) + standardGaussianWeights[currentGaussianWeightIndex] = standardGaussianWeights[currentGaussianWeightIndex] / sumOfWeights; + + NSUInteger numberOfOptimizedOffsets = MIN(blurRadius / 2 + (blurRadius % 2), 7U); + NSUInteger trueNumberOfOptimizedOffsets = blurRadius / 2 + (blurRadius % 2); + + NSMutableString *shaderString = [[NSMutableString alloc] init]; + + [shaderString appendFormat:@"\ + uniform sampler2D sourceImage;\n\ + uniform highp float texelWidthOffset;\n\ + uniform highp float texelHeightOffset;\n\ + \n\ + varying highp vec2 blurCoordinates[%lu];\n\ + \n\ + void main()\n\ + {\n\ + lowp vec4 sum = vec4(0.0);\n", (unsigned long)(1 + (numberOfOptimizedOffsets * 2)) ]; + + [shaderString appendFormat:@"sum += texture2D(sourceImage, blurCoordinates[0]) * %f;\n", standardGaussianWeights[0]]; + + for (NSUInteger currentBlurCoordinateIndex = 0; currentBlurCoordinateIndex < numberOfOptimizedOffsets; currentBlurCoordinateIndex++) + { + GLfloat firstWeight = standardGaussianWeights[currentBlurCoordinateIndex * 2 + 1]; + GLfloat secondWeight = standardGaussianWeights[currentBlurCoordinateIndex * 2 + 2]; + GLfloat optimizedWeight = firstWeight + secondWeight; + + [shaderString appendFormat:@"sum += texture2D(sourceImage, blurCoordinates[%lu]) * %f;\n", (unsigned long)((currentBlurCoordinateIndex * 2) + 1), optimizedWeight]; + [shaderString appendFormat:@"sum += texture2D(sourceImage, blurCoordinates[%lu]) * %f;\n", (unsigned long)((currentBlurCoordinateIndex * 2) + 2), optimizedWeight]; + } + + if (trueNumberOfOptimizedOffsets > numberOfOptimizedOffsets) + { + [shaderString appendString:@"highp vec2 singleStepOffset = vec2(texelWidthOffset, texelHeightOffset);\n"]; + + for (NSUInteger currentOverlowTextureRead = numberOfOptimizedOffsets; currentOverlowTextureRead < trueNumberOfOptimizedOffsets; currentOverlowTextureRead++) + { + GLfloat firstWeight = standardGaussianWeights[currentOverlowTextureRead * 2 + 1]; + GLfloat secondWeight = standardGaussianWeights[currentOverlowTextureRead * 2 + 2]; + + GLfloat optimizedWeight = firstWeight + secondWeight; + GLfloat optimizedOffset = (firstWeight * (currentOverlowTextureRead * 2 + 1) + secondWeight * (currentOverlowTextureRead * 2 + 2)) / optimizedWeight; + + [shaderString appendFormat:@"sum += texture2D(sourceImage, blurCoordinates[0] + singleStepOffset * %f) * %f;\n", optimizedOffset, optimizedWeight]; + [shaderString appendFormat:@"sum += texture2D(sourceImage, blurCoordinates[0] - singleStepOffset * %f) * %f;\n", optimizedOffset, optimizedWeight]; + } + } + + [shaderString appendString:@"\ + gl_FragColor = sum;\n\ + }\n"]; + + free(standardGaussianWeights); + return shaderString; +} + +- (instancetype)init +{ + _currentRadius = 6; + NSUInteger calculatedSampleRadius = 0; + CGFloat minimumWeightToFindEdgeOfSamplingArea = 1.0/256.0; + calculatedSampleRadius = (NSUInteger)(floor(sqrt(-2.0 * pow(_currentRadius, 2.0) * log(minimumWeightToFindEdgeOfSamplingArea * sqrt(2.0 * M_PI * pow(_currentRadius, 2.0))) ))); + calculatedSampleRadius += calculatedSampleRadius % 2; + + NSString *vertexShader = [[self class] vertexShaderForBlurOfRadius:calculatedSampleRadius sigma:_currentRadius]; + NSString *fragmentShader = [[self class] fragmentShaderForBlurOfRadius:calculatedSampleRadius sigma:_currentRadius]; + + return [self initWithFirstStageVertexShaderFromString:vertexShader firstStageFragmentShaderFromString:fragmentShader secondStageVertexShaderFromString:vertexShader secondStageFragmentShaderFromString:fragmentShader]; +} + +- (instancetype)initWithFirstStageVertexShaderFromString:(NSString *)firstStageVertexShaderString firstStageFragmentShaderFromString:(NSString *)firstStageFragmentShaderString secondStageVertexShaderFromString:(NSString *)secondStageVertexShaderString secondStageFragmentShaderFromString:(NSString *)secondStageFragmentShaderString +{ + if (!(self = [super initWithVertexShaderFromString:firstStageVertexShaderString fragmentShaderFromString:firstStageFragmentShaderString])) + { + return nil; + } + + _secondProgramUniformStateRestorationBlocks = [NSMutableDictionary dictionaryWithCapacity:10]; + + runSynchronouslyOnVideoProcessingQueue(^{ + [GPUImageContext useImageProcessingContext]; + + _secondFilterProgram = [[GPUImageContext sharedImageProcessingContext] programForVertexShaderString:secondStageVertexShaderString fragmentShaderString:secondStageFragmentShaderString]; + + if (!_secondFilterProgram.initialized) + { + [self initializeSecondaryAttributes]; + + if (![_secondFilterProgram link]) + { + NSString *progLog = [_secondFilterProgram programLog]; + NSLog(@"Program link log: %@", progLog); + NSString *fragLog = [_secondFilterProgram fragmentShaderLog]; + NSLog(@"Fragment shader compile log: %@", fragLog); + NSString *vertLog = [_secondFilterProgram vertexShaderLog]; + NSLog(@"Vertex shader compile log: %@", vertLog); + _secondFilterProgram = nil; + NSAssert(NO, @"Filter shader link failed"); + } + } + + _secondFilterPositionAttribute = [_secondFilterProgram attributeIndex:@"position"]; + _secondFilterTextureCoordinateAttribute = [_secondFilterProgram attributeIndex:@"inputTexCoord"]; + _secondFilterInputTextureUniform = [_secondFilterProgram uniformIndex:@"sourceImage"]; + + _verticalPassTexelWidthOffsetUniform = [filterProgram uniformIndex:@"texelWidthOffset"]; + _verticalPassTexelHeightOffsetUniform = [filterProgram uniformIndex:@"texelHeightOffset"]; + + _horizontalPassTexelWidthOffsetUniform = [_secondFilterProgram uniformIndex:@"texelWidthOffset"]; + _horizontalPassTexelHeightOffsetUniform = [_secondFilterProgram uniformIndex:@"texelHeightOffset"]; + + [GPUImageContext setActiveShaderProgram:_secondFilterProgram]; + + glEnableVertexAttribArray(_secondFilterPositionAttribute); + glEnableVertexAttribArray(_secondFilterTextureCoordinateAttribute); + }); + + _verticalTexelSpacing = 1.0f; + _horizontalTexelSpacing = 1.0f; + + [self setupFilterForSize:[self sizeOfFBO]]; + + return self; +} + +- (instancetype)initWithFirstStageFragmentShaderFromString:(NSString *)firstStageFragmentShaderString secondStageFragmentShaderFromString:(NSString *)secondStageFragmentShaderString +{ + if (!(self = [self initWithFirstStageVertexShaderFromString:kGPUImageVertexShaderString firstStageFragmentShaderFromString:firstStageFragmentShaderString secondStageVertexShaderFromString:kGPUImageVertexShaderString secondStageFragmentShaderFromString:secondStageFragmentShaderString])) + { + return nil; + } + + return self; +} + +- (void)initializeSecondaryAttributes +{ + [_secondFilterProgram addAttribute:@"position"]; + [_secondFilterProgram addAttribute:@"inputTexCoord"]; +} + +- (void)setInputSize:(CGSize)newSize atIndex:(NSInteger)textureIndex +{ + [super setInputSize:newSize atIndex:textureIndex]; + + CGFloat maxSize = MAX(newSize.width, newSize.height); + NSUInteger blurRadius = (NSUInteger)ceil(maxSize * 0.008); + + if (_currentRadius != blurRadius) + { + _currentRadius = blurRadius; + + NSUInteger calculatedSampleRadius = 0; + if (_currentRadius >= 1) + { + CGFloat minimumWeightToFindEdgeOfSamplingArea = 1.0/256.0; + calculatedSampleRadius = (NSUInteger)(floor(sqrt(-2.0 * pow(_currentRadius, 2.0) * log(minimumWeightToFindEdgeOfSamplingArea * sqrt(2.0 * M_PI * pow(_currentRadius, 2.0))) ))); + calculatedSampleRadius += calculatedSampleRadius % 2; + } + + NSString *newGaussianBlurVertexShader = [[self class] vertexShaderForBlurOfRadius:calculatedSampleRadius sigma:_currentRadius]; + NSString *newGaussianBlurFragmentShader = [[self class] fragmentShaderForBlurOfRadius:calculatedSampleRadius sigma:_currentRadius]; + + [self switchToVertexShader:newGaussianBlurVertexShader fragmentShader:newGaussianBlurFragmentShader]; + } +} + +- (void)switchToVertexShader:(NSString *)newVertexShader fragmentShader:(NSString *)newFragmentShader +{ + runSynchronouslyOnVideoProcessingQueue(^{ + [GPUImageContext useImageProcessingContext]; + + filterProgram = [[GPUImageContext sharedImageProcessingContext] programForVertexShaderString:newVertexShader fragmentShaderString:newFragmentShader]; + + if (!filterProgram.initialized) + { + [self initializeAttributes]; + + if (![filterProgram link]) + { + NSString *progLog = [filterProgram programLog]; + NSLog(@"Program link log: %@", progLog); + NSString *fragLog = [filterProgram fragmentShaderLog]; + NSLog(@"Fragment shader compile log: %@", fragLog); + NSString *vertLog = [filterProgram vertexShaderLog]; + NSLog(@"Vertex shader compile log: %@", vertLog); + filterProgram = nil; + NSAssert(NO, @"Filter shader link failed"); + } + } + + filterPositionAttribute = [filterProgram attributeIndex:@"position"]; + filterTextureCoordinateAttribute = [filterProgram attributeIndex:@"inputTexCoord"]; + filterInputTextureUniform = [filterProgram uniformIndex:@"sourceImage"]; + + [GPUImageContext setActiveShaderProgram:filterProgram]; + + glEnableVertexAttribArray(filterPositionAttribute); + glEnableVertexAttribArray(filterTextureCoordinateAttribute); + + + _secondFilterProgram = [[GPUImageContext sharedImageProcessingContext] programForVertexShaderString:newVertexShader fragmentShaderString:newFragmentShader]; + + if (!_secondFilterProgram.initialized) + { + [self initializeSecondaryAttributes]; + + if (![_secondFilterProgram link]) + { + NSString *progLog = [_secondFilterProgram programLog]; + NSLog(@"Program link log: %@", progLog); + NSString *fragLog = [_secondFilterProgram fragmentShaderLog]; + NSLog(@"Fragment shader compile log: %@", fragLog); + NSString *vertLog = [_secondFilterProgram vertexShaderLog]; + NSLog(@"Vertex shader compile log: %@", vertLog); + _secondFilterProgram = nil; + NSAssert(NO, @"Filter shader link failed"); + } + } + + _secondFilterPositionAttribute = [_secondFilterProgram attributeIndex:@"position"]; + _secondFilterTextureCoordinateAttribute = [_secondFilterProgram attributeIndex:@"inputTexCoord"]; + _secondFilterInputTextureUniform = [_secondFilterProgram uniformIndex:@"sourceImage"]; + + _verticalPassTexelWidthOffsetUniform = [filterProgram uniformIndex:@"texelWidthOffset"]; + _verticalPassTexelHeightOffsetUniform = [filterProgram uniformIndex:@"texelHeightOffset"]; + + _horizontalPassTexelWidthOffsetUniform = [_secondFilterProgram uniformIndex:@"texelWidthOffset"]; + _horizontalPassTexelHeightOffsetUniform = [_secondFilterProgram uniformIndex:@"texelHeightOffset"]; + + [GPUImageContext setActiveShaderProgram:_secondFilterProgram]; + + glEnableVertexAttribArray(_secondFilterPositionAttribute); + glEnableVertexAttribArray(_secondFilterTextureCoordinateAttribute); + + [self setupFilterForSize:[self sizeOfFBO]]; + glFinish(); + }); +} + +- (GPUImageFramebuffer *)framebufferForOutput +{ + return _secondOutputFramebuffer; +} + +- (void)removeOutputFramebuffer +{ + _secondOutputFramebuffer = nil; +} + +- (void)renderToTextureWithVertices:(const GLfloat *)vertices textureCoordinates:(const GLfloat *)textureCoordinates +{ + if (self.preventRendering) + { + [firstInputFramebuffer unlock]; + return; + } + + [GPUImageContext setActiveShaderProgram:filterProgram]; + + outputFramebuffer = [[GPUImageContext sharedFramebufferCache] fetchFramebufferForSize:[self sizeOfFBO] + textureOptions:self.outputTextureOptions + onlyTexture:false]; + [outputFramebuffer activateFramebuffer]; + + [self setUniformsForProgramAtIndex:0]; + + glClearColor(backgroundColorRed, backgroundColorGreen, backgroundColorBlue, backgroundColorAlpha); + glClear(GL_COLOR_BUFFER_BIT); + + glActiveTexture(GL_TEXTURE2); + glBindTexture(GL_TEXTURE_2D, [firstInputFramebuffer texture]); + + glUniform1i(filterInputTextureUniform, 2); + + glVertexAttribPointer(filterPositionAttribute, 2, GL_FLOAT, 0, 0, vertices); + glVertexAttribPointer(filterTextureCoordinateAttribute, 2, GL_FLOAT, 0, 0, textureCoordinates); + + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); + + [firstInputFramebuffer unlock]; + firstInputFramebuffer = nil; + + _secondOutputFramebuffer = [[GPUImageContext sharedFramebufferCache] fetchFramebufferForSize:[self sizeOfFBO] + textureOptions:self.outputTextureOptions + onlyTexture:false]; + [_secondOutputFramebuffer activateFramebuffer]; + [GPUImageContext setActiveShaderProgram:_secondFilterProgram]; + if (usingNextFrameForImageCapture) + [_secondOutputFramebuffer lock]; + + [self setUniformsForProgramAtIndex:1]; + + glActiveTexture(GL_TEXTURE3); + glBindTexture(GL_TEXTURE_2D, [outputFramebuffer texture]); + glVertexAttribPointer(_secondFilterTextureCoordinateAttribute, 2, GL_FLOAT, 0, 0, [GPUImageFilter textureCoordinatesForRotation:kGPUImageNoRotation]); + + glUniform1i(_secondFilterInputTextureUniform, 3); + + glVertexAttribPointer(_secondFilterPositionAttribute, 2, GL_FLOAT, 0, 0, vertices); + + glClearColor(0.0f, 0.0f, 0.0f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); + + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); + [outputFramebuffer unlock]; + outputFramebuffer = nil; + + if (usingNextFrameForImageCapture) + dispatch_semaphore_signal(imageCaptureSemaphore); +} + +- (void)setAndExecuteUniformStateCallbackAtIndex:(GLint)uniform forProgram:(GLProgram *)shaderProgram toBlock:(dispatch_block_t)uniformStateBlock +{ + if (shaderProgram == filterProgram) + [uniformStateRestorationBlocks setObject:[uniformStateBlock copy] forKey:[NSNumber numberWithInt:uniform]]; + else + [_secondProgramUniformStateRestorationBlocks setObject:[uniformStateBlock copy] forKey:[NSNumber numberWithInt:uniform]]; + + uniformStateBlock(); +} + +- (void)setUniformsForProgramAtIndex:(NSUInteger)programIndex +{ + if (programIndex == 0) + { + [uniformStateRestorationBlocks enumerateKeysAndObjectsUsingBlock:^(__unused id key, id obj, __unused BOOL *stop) + { + dispatch_block_t currentBlock = obj; + currentBlock(); + }]; + } + else + { + [_secondProgramUniformStateRestorationBlocks enumerateKeysAndObjectsUsingBlock:^(__unused id key, id obj, __unused BOOL *stop) + { + dispatch_block_t currentBlock = obj; + currentBlock(); + }]; + } + + if (programIndex == 0) + { + glUniform1f(_verticalPassTexelWidthOffsetUniform, _verticalPassTexelWidthOffset); + glUniform1f(_verticalPassTexelHeightOffsetUniform, _verticalPassTexelHeightOffset); + } + else + { + glUniform1f(_horizontalPassTexelWidthOffsetUniform, _horizontalPassTexelWidthOffset); + glUniform1f(_horizontalPassTexelHeightOffsetUniform, _horizontalPassTexelHeightOffset); + } +} + +- (void)setupFilterForSize:(CGSize)filterFrameSize +{ + runSynchronouslyOnVideoProcessingQueue(^ + { + if (GPUImageRotationSwapsWidthAndHeight(inputRotation)) + { + _verticalPassTexelWidthOffset = (GLfloat)(_verticalTexelSpacing / filterFrameSize.height); + _verticalPassTexelHeightOffset = 0.0; + } + else + { + _verticalPassTexelWidthOffset = 0.0; + _verticalPassTexelHeightOffset = (GLfloat)(_verticalTexelSpacing / filterFrameSize.height); + } + + _horizontalPassTexelWidthOffset = (GLfloat)(_horizontalTexelSpacing / filterFrameSize.width); + _horizontalPassTexelHeightOffset = 0.0; + }); +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGPhotoHistogram.h b/submodules/LegacyComponents/LegacyComponents/PGPhotoHistogram.h new file mode 100644 index 0000000000..43e80eaaa7 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGPhotoHistogram.h @@ -0,0 +1,17 @@ +#import +#import "PGCurvesTool.h" + +@interface PGPhotoHistogramBins : NSObject + +- (id)objectAtIndexedSubscript:(NSUInteger)idx; +- (NSUInteger)count; + +@end + +@interface PGPhotoHistogram : NSObject + +- (instancetype)initWithLuminanceCArray:(NSUInteger *)luminanceArray redCArray:(NSUInteger *)redArray greenCArray:(NSUInteger *)greenArray blueCArray:(NSUInteger *)blueArray; + +- (PGPhotoHistogramBins *)histogramBinsForType:(PGCurvesType)type; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGPhotoHistogram.m b/submodules/LegacyComponents/LegacyComponents/PGPhotoHistogram.m new file mode 100644 index 0000000000..8be0ab82e5 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGPhotoHistogram.m @@ -0,0 +1,98 @@ +#import "PGPhotoHistogram.h" + +@interface PGPhotoHistogramBins () +{ + NSArray *_bins; + NSUInteger _max; +} +@end + +@implementation PGPhotoHistogramBins + +- (instancetype)initWithCArray:(NSUInteger *)array +{ + self = [super init]; + if (self != nil) + { + _max = 1; + + NSMutableArray *bins = [[NSMutableArray alloc] init]; + for (NSUInteger i = 0; i < 256; i++) + { + [bins addObject:@(array[i])]; + if (i != 0) + { + if (array[i] > _max) + _max = array[i]; + } + } + + _bins = bins; + } + return self; +} + +- (id)objectAtIndexedSubscript:(NSUInteger)idx +{ + if (idx == 0) + return @0; + + if (idx < _bins.count) + return @([_bins[idx] floatValue] / (CGFloat)_max); + + return nil; +} + +- (NSUInteger)count +{ + return _bins.count; +} + +@end + +@interface PGPhotoHistogram () +{ + PGPhotoHistogramBins *_luminance; + PGPhotoHistogramBins *_red; + PGPhotoHistogramBins *_green; + PGPhotoHistogramBins *_blue; +} +@end + +@implementation PGPhotoHistogram + +- (instancetype)initWithLuminanceCArray:(NSUInteger *)luminanceArray redCArray:(NSUInteger *)redArray greenCArray:(NSUInteger *)greenArray blueCArray:(NSUInteger *)blueArray +{ + self = [super init]; + if (self != nil) + { + _luminance = [[PGPhotoHistogramBins alloc] initWithCArray:luminanceArray]; + _red = [[PGPhotoHistogramBins alloc] initWithCArray:redArray]; + _green = [[PGPhotoHistogramBins alloc] initWithCArray:greenArray]; + _blue = [[PGPhotoHistogramBins alloc] initWithCArray:blueArray]; + } + return self; +} + +- (PGPhotoHistogramBins *)histogramBinsForType:(PGCurvesType)type +{ + switch (type) + { + case PGCurvesTypeLuminance: + return _luminance; + + case PGCurvesTypeRed: + return _red; + + case PGCurvesTypeGreen: + return _green; + + case PGCurvesTypeBlue: + return _blue; + + default: + break; + } +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGPhotoHistogramGenerator.h b/submodules/LegacyComponents/LegacyComponents/PGPhotoHistogramGenerator.h new file mode 100644 index 0000000000..51df4ef99e --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGPhotoHistogramGenerator.h @@ -0,0 +1,9 @@ +#import "PGPhotoEditorRawDataOutput.h" + +@class PGPhotoHistogram; + +@interface PGPhotoHistogramGenerator : PGPhotoEditorRawDataOutput + +@property (nonatomic, copy) void (^histogramReady)(PGPhotoHistogram *histogram); + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGPhotoHistogramGenerator.m b/submodules/LegacyComponents/LegacyComponents/PGPhotoHistogramGenerator.m new file mode 100644 index 0000000000..fc24d986b3 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGPhotoHistogramGenerator.m @@ -0,0 +1,60 @@ +#import "PGPhotoHistogramGenerator.h" +#import "PGPhotoHistogram.h" + +@implementation PGPhotoHistogramGenerator + +- (instancetype)init +{ + self = [super init]; + if (self != nil) + { + __weak PGPhotoHistogramGenerator *weakSelf = self; + + self.newFrameAvailableBlock = ^ + { + __strong PGPhotoHistogramGenerator *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + NSUInteger lumHist[256]; + NSUInteger redHist[256]; + NSUInteger greenHist[256]; + NSUInteger blueHist[256]; + + memset(lumHist, 0, 256 * sizeof(NSUInteger)); + memset(redHist, 0, 256 * sizeof(NSUInteger)); + memset(greenHist, 0, 256 * sizeof(NSUInteger)); + memset(blueHist, 0, 256 * sizeof(NSUInteger)); + + NSInteger width = (NSInteger)strongSelf.imageSize.width; + NSInteger height = (NSInteger)strongSelf.imageSize.height; + + for (NSInteger y = 0; y < height; y++) + { + NSInteger offset = width * y; + + for (NSInteger x = 0; x < width; x++) + { + PGByteColorVector color = ((PGByteColorVector *)strongSelf.rawBytesForImage)[offset + x]; + if ([GPUImageContext supportsFastTextureUpload]) + { + GLubyte tmp = color.red; + color.red = color.blue; + color.blue = tmp; + } + NSInteger luma = (NSInteger)(color.red * 0.3 + color.green * 0.59 + color.blue * 0.11); + lumHist[luma]++; + redHist[color.red]++; + greenHist[color.green]++; + blueHist[color.blue]++; + } + } + + if (strongSelf.histogramReady != nil) + strongSelf.histogramReady([[PGPhotoHistogram alloc] initWithLuminanceCArray:lumHist redCArray:redHist greenCArray:greenHist blueCArray:blueHist]); + }; + } + return self; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGPhotoLookupFilterPass.h b/submodules/LegacyComponents/LegacyComponents/PGPhotoLookupFilterPass.h new file mode 100644 index 0000000000..c836be286e --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGPhotoLookupFilterPass.h @@ -0,0 +1,7 @@ +#import "PGPhotoCustomFilterPass.h" + +@interface PGPhotoLookupFilterPass : PGPhotoCustomFilterPass + +- (instancetype)initWithLookupImage:(UIImage *)lookupImage; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGPhotoLookupFilterPass.m b/submodules/LegacyComponents/LegacyComponents/PGPhotoLookupFilterPass.m new file mode 100644 index 0000000000..439f24845b --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGPhotoLookupFilterPass.m @@ -0,0 +1,67 @@ +#import "PGPhotoLookupFilterPass.h" + +#import "GPUImageTwoInputFilter.h" +#import "PGPhotoEditorPicture.h" + +NSString *const PGPhotoLookupFilterFragmentShaderString = PGShaderString( + uniform sampler2D inputImageTexture2; + + vec4 filter(vec4 texel) + { + vec3 index = texel.rgb * 15.0; + vec3 frac = fract(index); + vec3 i1 = floor(index); + vec3 i2 = clamp(i1 + 1.0, 0.0, 15.0); + + const vec2 offset = vec2(0.001953125, 0.03125); // vec2(0.5 / 256.0, 0.5 / 16.0) + + vec3 color1 = i1 * 0.0625; // 1.0 * 0.0625 = 1.0 / 16.0; + color1.r = color1.r * 0.0625; + + vec3 color2 = i2 * 0.0625; + color2.r = color2.r * 0.0625; + + vec2 p000 = vec2(color1.r + color1.b, color1.g); + vec3 c000 = texture2D(inputImageTexture2, offset + p000).rgb; + + vec2 p001 = vec2(color1.r + color2.b, color1.g); + vec3 c001 = texture2D(inputImageTexture2, offset + p001).rgb; + + vec2 p010 = vec2(color1.r + color1.b, color2.g); + vec3 c010 = texture2D(inputImageTexture2, offset + p010).rgb; + + vec2 p011 = vec2(color1.r + color2.b, color2.g); + vec3 c011 = texture2D(inputImageTexture2, offset + p011).rgb; + + vec2 p100 = vec2(color2.r + color1.b, color1.g); + vec3 c100 = texture2D(inputImageTexture2, offset + p100).rgb; + + vec2 p101 = vec2(color2.r + color2.b, color1.g); + vec3 c101 = texture2D(inputImageTexture2, offset + p101).rgb; + + vec2 p110 = vec2(color2.r + color1.b, color2.g); + vec3 c110 = texture2D(inputImageTexture2, offset + p110).rgb; + + vec2 p111 = vec2(color2.r + color2.b, color2.g); + vec3 c111 = texture2D(inputImageTexture2, offset + p111).rgb; + + vec3 c1 = mix(c000, c100, frac.r); + vec3 c2 = mix(c010, c110, frac.r); + vec3 c3 = mix(c001, c101, frac.r); + vec3 c4 = mix(c011, c111, frac.r); + + vec3 c1_c2 = mix(c1, c2, frac.g); + vec3 c3_c4 = mix(c3, c4, frac.g); + + return vec4(clamp(mix(c1_c2, c3_c4, frac.b), 0.0, 1.0), texel.a); + } +); + +@implementation PGPhotoLookupFilterPass + +- (instancetype)initWithLookupImage:(UIImage *)lookupImage +{ + return [super initWithShaderString:PGPhotoLookupFilterFragmentShaderString textureImages:@[ lookupImage ]]; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGPhotoProcessPass.h b/submodules/LegacyComponents/LegacyComponents/PGPhotoProcessPass.h new file mode 100644 index 0000000000..4ed999347f --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGPhotoProcessPass.h @@ -0,0 +1,40 @@ +#import "GPUImageFilter.h" + +#define PGShaderString(text) @ STRINGIZE2(text) + +@interface PGPhotoProcessPassParameter : NSObject + +@property (nonatomic, readonly) NSString *name; +@property (nonatomic, readonly) bool isConst; +@property (nonatomic, readonly) bool isUniform; +@property (nonatomic, readonly) bool isVarying; +@property (nonatomic, assign) NSInteger count; + +- (void)setFloatValue:(CGFloat)floatValue; +- (void)setFloatArray:(NSArray *)floatArray; +- (void)setColorValue:(UIColor *)colorValue; + +- (void)storeFilter:(GPUImageFilter *)filter uniformIndex:(GLint)uniformIndex; +- (NSString *)shaderString; + ++ (instancetype)varyingWithName:(NSString *)name type:(NSString *)type; ++ (instancetype)parameterWithName:(NSString *)name type:(NSString *)type; ++ (instancetype)parameterWithName:(NSString *)name type:(NSString *)type count:(NSInteger)count; ++ (instancetype)constWithName:(NSString *)name type:(NSString *)type value:(NSString *)value; + +@end + +@interface PGPhotoProcessPass : NSObject +{ + GPUImageOutput *_filter; +} + +@property (nonatomic, readonly) GPUImageOutput *filter; + +- (void)updateParameters; +- (void)process; +- (void)invalidate; + +@end + +extern NSString *const PGPhotoEnhanceColorSwapShaderString; \ No newline at end of file diff --git a/submodules/LegacyComponents/LegacyComponents/PGPhotoProcessPass.m b/submodules/LegacyComponents/LegacyComponents/PGPhotoProcessPass.m new file mode 100644 index 0000000000..0fdf46835f --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGPhotoProcessPass.m @@ -0,0 +1,173 @@ +#import "PGPhotoProcessPass.h" + +NSString *const PGPhotoEnhanceColorSwapShaderString = PGShaderString +( + varying highp vec2 texCoord; + + uniform sampler2D sourceImage; + + void main() { + gl_FragColor = texture2D(sourceImage, texCoord).bgra; + } +); + +@interface PGPhotoProcessPassParameter () +{ + NSString *_type; + NSString *_value; + BOOL _isConst; +} + +@property (nonatomic, weak) GPUImageFilter *filter; +@property (nonatomic, assign) GLint uniformIndex; + +@end + +@implementation PGPhotoProcessPassParameter + +- (instancetype)initWithName:(NSString *)name type:(NSString *)type count:(NSInteger)count +{ + self = [super init]; + if (self != nil) + { + _name = name; + _type = type; + _count = count; + } + return self; +} + +- (bool)isConst +{ + return _isConst; +} + +- (bool)isUniform +{ + return !_isConst; +} + +- (void)storeFilter:(GPUImageFilter *)filter uniformIndex:(GLint)uniformIndex +{ + self.filter = filter; + self.uniformIndex = uniformIndex; +} + +- (void)setFloatValue:(CGFloat)floatValue +{ + GPUImageFilter *filter = self.filter; + [filter setFloat:(GLfloat)floatValue forUniform:self.uniformIndex program:filter.program]; +} + +- (void)setFloatArray:(NSArray *)floatArray +{ + GPUImageFilter *filter = self.filter; + + GLfloat *glArray = malloc(sizeof(GLfloat) * floatArray.count); + + [floatArray enumerateObjectsUsingBlock:^(NSNumber *value, NSUInteger index, __unused BOOL *stop) + { + glArray[index] = value.floatValue; + }]; + + [filter setFloatArray:glArray length:(GLsizei)floatArray.count forUniform:self.uniformIndex program:filter.program]; +} + +- (void)setColorValue:(UIColor *)colorValue +{ + GPUImageFilter *filter = self.filter; + GPUVector3 colorVector; + + const CGFloat *colors = CGColorGetComponents(colorValue.CGColor); + size_t componentCount = CGColorGetNumberOfComponents(colorValue.CGColor); + + if (componentCount == 4) { + colorVector.one = (GLfloat)colors[0]; + colorVector.two = (GLfloat)colors[1]; + colorVector.three = (GLfloat)colors[2]; + } else { + colorVector.one = (GLfloat)colors[0]; + colorVector.two = (GLfloat)colors[0]; + colorVector.three = (GLfloat)colors[0]; + } + + [filter setVec3:colorVector forUniform:self.uniformIndex program:filter.program]; +} + ++ (instancetype)varyingWithName:(NSString *)name type:(NSString *)type +{ + PGPhotoProcessPassParameter *parameter = [[[self class] alloc] initWithName:name type:type count:0]; + parameter->_isVarying = true; + return parameter; +} + ++ (instancetype)parameterWithName:(NSString *)name type:(NSString *)type +{ + return [[[self class] alloc] initWithName:name type:type count:0]; +} + ++ (instancetype)parameterWithName:(NSString *)name type:(NSString *)type count:(NSInteger)count +{ + return [[[self class] alloc] initWithName:name type:type count:count]; +} + ++ (instancetype)constWithName:(NSString *)name type:(NSString *)type value:(NSString *)value +{ + PGPhotoProcessPassParameter *parameter = [[[self class] alloc] initWithName:name type:type count:0]; + parameter->_isConst = true; + parameter->_value = value; + return parameter; +} + +- (NSString *)shaderString +{ + if (_isConst) + { + return [NSString stringWithFormat:@"const %@ %@ = %@", _type, _name, _value]; + } + else if (_isVarying) + { + return [NSString stringWithFormat:@"varying %@ %@", _type, _name]; + } + else + { + if (self.count > 0) + return [NSString stringWithFormat:@"uniform %@ %@[%ld]", _type, _name, _count]; + else + return [NSString stringWithFormat:@"uniform %@ %@", _type, _name]; + } +} + +@end + + +@implementation PGPhotoProcessPass + +- (void)dealloc +{ + [_filter removeAllTargets]; +} + +- (void)process +{ + +} + +- (void)invalidate +{ + +} + +- (void)updateParameters +{ +} + +- (GPUImageOutput *)filter +{ + if (_filter == nil) + _filter = [[GPUImageFilter alloc] init]; + + return _filter; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGPhotoSharpenPass.h b/submodules/LegacyComponents/LegacyComponents/PGPhotoSharpenPass.h new file mode 100644 index 0000000000..f33e500f8f --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGPhotoSharpenPass.h @@ -0,0 +1,7 @@ +#import "PGPhotoProcessPass.h" + +@interface PGPhotoSharpenPass : PGPhotoProcessPass + +@property (nonatomic, assign) CGFloat sharpness; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGPhotoSharpenPass.m b/submodules/LegacyComponents/LegacyComponents/PGPhotoSharpenPass.m new file mode 100644 index 0000000000..453cb7f2e7 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGPhotoSharpenPass.m @@ -0,0 +1,144 @@ +#import "PGPhotoSharpenPass.h" + +NSString *const kSharpenVertexShaderString = PGShaderString +( + attribute vec4 position; + attribute vec4 inputTexCoord; + + uniform float imageWidthFactor; + uniform float imageHeightFactor; + uniform float sharpness; + + varying vec2 texCoord; + varying vec2 leftTexCoord; + varying vec2 rightTexCoord; + varying vec2 topTexCoord; + varying vec2 bottomTexCoord; + + varying float centerMultiplier; + varying float edgeMultiplier; + + void main() + { + gl_Position = position; + + vec2 widthStep = vec2(imageWidthFactor, 0.0); + vec2 heightStep = vec2(0.0, imageHeightFactor); + + texCoord = inputTexCoord.xy; + leftTexCoord = inputTexCoord.xy - widthStep; + rightTexCoord = inputTexCoord.xy + widthStep; + topTexCoord = inputTexCoord.xy + heightStep; + bottomTexCoord = inputTexCoord.xy - heightStep; + + centerMultiplier = 1.0 + 4.0 * sharpness; + edgeMultiplier = sharpness; + } + ); + +NSString *const kSharpenFragmentShaderString = SHADER_STRING +( + precision highp float; + + varying highp vec2 texCoord; + varying highp vec2 leftTexCoord; + varying highp vec2 rightTexCoord; + varying highp vec2 topTexCoord; + varying highp vec2 bottomTexCoord; + + varying highp float centerMultiplier; + varying highp float edgeMultiplier; + + uniform sampler2D sourceImage; + + void main() + { + mediump vec3 textureColor = texture2D(sourceImage, texCoord).rgb; + mediump vec3 leftTextureColor = texture2D(sourceImage, leftTexCoord).rgb; + mediump vec3 rightTextureColor = texture2D(sourceImage, rightTexCoord).rgb; + mediump vec3 topTextureColor = texture2D(sourceImage, topTexCoord).rgb; + mediump vec3 bottomTextureColor = texture2D(sourceImage, bottomTexCoord).rgb; + + gl_FragColor = vec4((textureColor * centerMultiplier - (leftTextureColor * edgeMultiplier + rightTextureColor * edgeMultiplier + topTextureColor * edgeMultiplier + bottomTextureColor * edgeMultiplier)), texture2D(sourceImage, bottomTexCoord).w); + } + ); + +@interface PGSharpenFilter : GPUImageFilter + +@property (nonatomic, assign) CGFloat sharpness; + +@end + +@implementation PGSharpenFilter +{ + GLint sharpnessUniform; + GLint imageWidthFactorUniform; + GLint imageHeightFactorUniform; +} + +- (instancetype)init +{ + self = [super initWithVertexShaderFromString:kSharpenVertexShaderString fragmentShaderFromString:kSharpenFragmentShaderString]; + if (self != nil) + { + sharpnessUniform = [filterProgram uniformIndex:@"sharpness"]; + self.sharpness = 0.0f; + + imageWidthFactorUniform = [filterProgram uniformIndex:@"imageWidthFactor"]; + imageHeightFactorUniform = [filterProgram uniformIndex:@"imageHeightFactor"]; + } + return self; +} + +- (void)setupFilterForSize:(CGSize)filterFrameSize +{ + runSynchronouslyOnVideoProcessingQueue(^ + { + [GPUImageContext setActiveShaderProgram:filterProgram]; + + if (GPUImageRotationSwapsWidthAndHeight(inputRotation)) + { + glUniform1f(imageWidthFactorUniform, (GLfloat)(1.0f / filterFrameSize.height)); + glUniform1f(imageHeightFactorUniform, (GLfloat)(1.0f / filterFrameSize.width)); + } + else + { + glUniform1f(imageWidthFactorUniform, (GLfloat)(1.0f / filterFrameSize.width)); + glUniform1f(imageHeightFactorUniform, (GLfloat)(1.0f / filterFrameSize.height)); + } + }); +} + +- (void)setSharpness:(CGFloat)newValue +{ + _sharpness = newValue; + + [self setFloat:(float)_sharpness forUniform:sharpnessUniform program:filterProgram]; +} + +@end + +@implementation PGPhotoSharpenPass + +- (instancetype)init +{ + self = [super init]; + if (self != nil) + { + _filter = [[PGSharpenFilter alloc] init]; + } + return self; +} + +- (void)setSharpness:(CGFloat)sharpness +{ + _sharpness = sharpness; + [self updateParameters]; +} + +- (void)updateParameters +{ + [((PGSharpenFilter *) _filter) setSharpness:_sharpness]; +} + +@end \ No newline at end of file diff --git a/submodules/LegacyComponents/LegacyComponents/PGPhotoTool.h b/submodules/LegacyComponents/LegacyComponents/PGPhotoTool.h new file mode 100644 index 0000000000..c0fc9b9b6e --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGPhotoTool.h @@ -0,0 +1,55 @@ +#import "PGPhotoEditorItem.h" +#import "PGPhotoProcessPass.h" + +@class PGPhotoToolComposer; + +typedef enum +{ + PGPhotoToolTypePass, + PGPhotoToolTypeShader +} PGPhotoToolType; + +@protocol PGCustomToolValue + +- (id)cleanValue; + +@end + +@interface PGPhotoTool : NSObject +{ + PGPhotoProcessPass *_pass; + + NSString *_identifier; + PGPhotoToolType _type; + NSInteger _order; + + NSArray *_parameters; + NSArray *_constants; + + CGFloat _minimumValue; + CGFloat _maximumValue; + CGFloat _defaultValue; +} + +@property (nonatomic, readonly) PGPhotoToolType type; +@property (nonatomic, readonly) NSInteger order; + +@property (nonatomic, readonly) bool isHidden; + +@property (nonatomic, readonly) NSString *shaderString; +@property (nonatomic, readonly) NSString *ancillaryShaderString; +@property (nonatomic, readonly) PGPhotoProcessPass *pass; + +@property (nonatomic, readonly) bool isSimple; + +@property (nonatomic, weak) PGPhotoToolComposer *toolComposer; + +- (void)invalidate; + +- (NSString *)stringValue; +- (NSString *)stringValue:(bool)includeZero; + +- (UIView *)itemControlViewWithChangeBlock:(void (^)(id newValue, bool animated))changeBlock explicit:(bool)explicit nameWidth:(CGFloat)nameWidth; +- (UIView *)itemAreaViewWithChangeBlock:(void (^)(id))changeBlock explicit:(bool)explicit; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGPhotoTool.m b/submodules/LegacyComponents/LegacyComponents/PGPhotoTool.m new file mode 100644 index 0000000000..1e321d907c --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGPhotoTool.m @@ -0,0 +1,207 @@ +#import "PGPhotoTool.h" +#import "TGPhotoEditorGenericToolView.h" + +@implementation PGPhotoTool + +@synthesize title = _title; +@synthesize value = _value; +@synthesize tempValue = _tempValue; +@synthesize parameters = _parameters; +@synthesize beingEdited = _beingEdited; +@synthesize shouldBeSkipped = _shouldBeSkipped; +@synthesize parametersChanged = _parametersChanged; +@synthesize disabled = _disabled; +@synthesize segmented = _segmented; + +- (instancetype)init +{ + self = [super init]; + if (self != nil) + { + [self parameters]; + } + return self; +} + +- (NSString *)identifier +{ + return _identifier; +} + +- (PGPhotoToolType)type +{ + return _type; +} + +- (bool)isSimple +{ + return true; +} + +- (NSInteger)order +{ + return _order; +} + +- (Class)valueClass +{ + return [NSNumber class]; +} + +- (CGFloat)minimumValue +{ + return _minimumValue; +} + +- (CGFloat)maximumValue +{ + return _maximumValue; +} + +- (CGFloat)defaultValue +{ + return _defaultValue; +} + +- (id)tempValue +{ + if (self.disabled) + { + if ([_tempValue isKindOfClass:[NSNumber class]]) + return @0; + } + + return _tempValue; +} + +- (id)displayValue +{ + if (self.beingEdited) + return self.tempValue; + + return self.value; +} + +- (void)setDisabled:(bool)disabled +{ + _disabled = disabled; + + if (self.beingEdited) + [self updateParameters]; +} + +- (void)setBeingEdited:(bool)beingEdited +{ + _beingEdited = beingEdited; + + [self updateParameters]; +} + +- (void)setValue:(id)value +{ + _value = value; + + if (!self.beingEdited) + [self updateParameters]; +} + +- (void)setTempValue:(id)tempValue +{ + _tempValue = tempValue; + + if (self.beingEdited) + [self updateParameters]; +} + +- (bool)isHidden +{ + return false; +} + +- (void)updateParameters +{ + +} + +- (void)reset +{ + +} + +- (void)invalidate +{ + if (_pass != nil) + [_pass invalidate]; +} + +- (UIView *)itemControlViewWithChangeBlock:(void (^)(id newValue, bool animated))changeBlock +{ + return [self itemControlViewWithChangeBlock:changeBlock explicit:false nameWidth:0.0f]; +} + +- (UIView *)itemControlViewWithChangeBlock:(void (^)(id newValue, bool animated))changeBlock explicit:(bool)explicit nameWidth:(CGFloat)nameWidth +{ + __weak PGPhotoTool *weakSelf = self; + + UIView *view = [[TGPhotoEditorGenericToolView alloc] initWithEditorItem:self explicit:explicit nameWidth:nameWidth]; + view.valueChanged = ^(id newValue, bool animated) + { + __strong PGPhotoTool *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + if (!explicit && [strongSelf.tempValue isEqual:newValue]) + return; + + if (explicit && [strongSelf.value isEqual:newValue]) + return; + + if (!explicit) + strongSelf.tempValue = newValue; + else + strongSelf.value = newValue; + + if (changeBlock != nil) + changeBlock(newValue, animated); + }; + return view; +} + +- (UIView *)itemAreaViewWithChangeBlock:(void (^)(id newValue))__unused changeBlock +{ + return [self itemAreaViewWithChangeBlock:changeBlock explicit:false]; +} + +- (UIView *)itemAreaViewWithChangeBlock:(void (^)(id))__unused changeBlock explicit:(bool)__unused explicit +{ + return nil; +} + +- (NSString *)stringValue +{ + return [self stringValue:false]; +} + +- (NSString *)stringValue:(bool)includeZero +{ + if ([self.displayValue isKindOfClass:[NSNumber class]]) + { + NSNumber *value = (NSNumber *)self.displayValue; + CGFloat fractValue = value.floatValue / ABS(self.maximumValue); + if (floorf(ABS(value.floatValue)) == 0) + return includeZero ? @"0.00" : nil; + else if (fractValue > 0) + return [NSString stringWithFormat:@"+%0.2f", fractValue]; + else if (fractValue < 0) + return [NSString stringWithFormat:@"%0.2f", fractValue]; + } + + return nil; +} + +- (NSString *)ancillaryShaderString +{ + return nil; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGPhotoToolComposer.h b/submodules/LegacyComponents/LegacyComponents/PGPhotoToolComposer.h new file mode 100644 index 0000000000..fdf65a71a8 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGPhotoToolComposer.h @@ -0,0 +1,16 @@ +#import "PGPhotoProcessPass.h" + +@class PGPhotoTool; + +@interface PGPhotoToolComposer : PGPhotoProcessPass + +@property (nonatomic, readonly) NSArray *tools; +@property (nonatomic, readonly) NSArray *advancedTools; +@property (nonatomic, readonly) bool shouldBeSkipped; +@property (nonatomic, assign) CGSize imageSize; + +- (void)addPhotoTool:(PGPhotoTool *)tool; +- (void)addPhotoTools:(NSArray *)tools; +- (void)compose; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGPhotoToolComposer.m b/submodules/LegacyComponents/LegacyComponents/PGPhotoToolComposer.m new file mode 100644 index 0000000000..e4cf7a3248 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGPhotoToolComposer.m @@ -0,0 +1,352 @@ +#import "PGPhotoToolComposer.h" + +#import + +#import "PGPhotoProcessPass.h" +#import "PGPhotoTool.h" + +#define PGTick NSDate *startTime = [NSDate date] +#define PGTock NSLog(@"%s Time: %f", __func__, -[startTime timeIntervalSinceNow]) + +NSString *const PGPhotoToolAncillaryShaderString = PGShaderString +( + highp float getLuma(highp vec3 rgbP) { + return (0.299 * rgbP.r) + (0.587 * rgbP.g) + (0.114 * rgbP.b); + } + + lowp vec3 rgbToHsv(lowp vec3 c) { + highp vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0); + highp vec4 p = c.g < c.b ? vec4(c.bg, K.wz) : vec4(c.gb, K.xy); + highp vec4 q = c.r < p.x ? vec4(p.xyw, c.r) : vec4(c.r, p.yzx); + + highp float d = q.x - min(q.w, q.y); + highp float e = 1.0e-10; + return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x); + } + + lowp vec3 hsvToRgb(lowp vec3 c) { + highp vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0); + highp vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www); + return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y); + } + + highp vec3 rgbToHsl(highp vec3 color) { + highp vec3 hsl; + + highp float fmin = min(min(color.r, color.g), color.b); + highp float fmax = max(max(color.r, color.g), color.b); + highp float delta = fmax - fmin; + + hsl.z = (fmax + fmin) / 2.0; + + if (delta == 0.0) { + hsl.x = 0.0; + hsl.y = 0.0; + } + else { + if (hsl.z < 0.5) + hsl.y = delta / (fmax + fmin); + else + hsl.y = delta / (2.0 - fmax - fmin); + + highp float deltaR = (((fmax - color.r) / 6.0) + (delta / 2.0)) / delta; + highp float deltaG = (((fmax - color.g) / 6.0) + (delta / 2.0)) / delta; + highp float deltaB = (((fmax - color.b) / 6.0) + (delta / 2.0)) / delta; + + if (color.r == fmax ) + hsl.x = deltaB - deltaG; + else if (color.g == fmax) + hsl.x = (1.0 / 3.0) + deltaR - deltaB; + else if (color.b == fmax) + hsl.x = (2.0 / 3.0) + deltaG - deltaR; + + if (hsl.x < 0.0) + hsl.x += 1.0; + else if (hsl.x > 1.0) + hsl.x -= 1.0; + } + + return hsl; + } + + highp float hueToRgb(highp float f1, highp float f2, highp float hue) { + if (hue < 0.0) + hue += 1.0; + else if (hue > 1.0) + hue -= 1.0; + highp float res; + if ((6.0 * hue) < 1.0) + res = f1 + (f2 - f1) * 6.0 * hue; + else if ((2.0 * hue) < 1.0) + res = f2; + else if ((3.0 * hue) < 2.0) + res = f1 + (f2 - f1) * ((2.0 / 3.0) - hue) * 6.0; + else + res = f1; + return res; + } + + highp vec3 hslToRgb(highp vec3 hsl) { + highp vec3 rgb; + + if (hsl.y == 0.0) { + rgb = vec3(hsl.z); + } + else { + highp float f2; + + if (hsl.z < 0.5) + f2 = hsl.z * (1.0 + hsl.y); + else + f2 = (hsl.z + hsl.y) - (hsl.y * hsl.z); + + highp float f1 = 2.0 * hsl.z - f2; + + rgb.r = hueToRgb(f1, f2, hsl.x + (1.0/3.0)); + rgb.g = hueToRgb(f1, f2, hsl.x); + rgb.b = hueToRgb(f1, f2, hsl.x - (1.0/3.0)); + } + + return rgb; + } + + highp vec3 rgbToYuv(highp vec3 inP) { + highp vec3 outP; + outP.r = getLuma(inP); + outP.g = (1.0 / 1.772) * (inP.b - outP.r); + outP.b = (1.0 / 1.402) * (inP.r - outP.r); + return outP; + } + + lowp vec3 yuvToRgb(highp vec3 inP) { + highp float y = inP.r; + highp float u = inP.g; + highp float v = inP.b; + lowp vec3 outP; + outP.r = 1.402 * v + y; + outP.g = (y - (0.299 * 1.402 / 0.587) * v - (0.114 * 1.772 / 0.587) * u); + outP.b = 1.772 * u + y; + return outP; + } + + lowp float easeInOutSigmoid(lowp float value, lowp float strength) { + lowp float t = 1.0 / (1.0 - strength); + if (value > 0.5) { + return 1.0 - pow(2.0 - 2.0 * value, t) * 0.5; + } + else { + return pow(2.0 * value, t) * 0.5; + } + } + + lowp float powerCurve(lowp float inVal, lowp float mag) { + lowp float outVal; + highp float power = 1.0 + abs(mag); + + if (mag > 0.0) + power = 1.0 / power; + + inVal = 1.0 - inVal; + outVal = pow((1.0 - inVal), power); + + return outVal; + } +); + +@interface PGPhotoToolFilter : GPUImageFilter +{ + GLint _aspectRatioUniform; + GLint _widthUniform; + GLint _heightUniform; +} + +@property (nonatomic, assign) CGSize imageSize; + +@end + +@implementation PGPhotoToolFilter + +- (instancetype)initWithFragmentShaderFromString:(NSString *)fragmentShaderString +{ + self = [super initWithFragmentShaderFromString:fragmentShaderString]; + if (self != nil) + { + _aspectRatioUniform = [self.program uniformIndex:@"aspectRatio"]; + _widthUniform = [self.program uniformIndex:@"width"]; + _heightUniform = [self.program uniformIndex:@"height"]; + } + return self; +} + +- (void)setInputSize:(CGSize)newSize atIndex:(NSInteger)textureIndex +{ + [super setInputSize:newSize atIndex:textureIndex]; + inputTextureSize = newSize; + + [self setFloat:(float)(inputTextureSize.height / inputTextureSize.width) forUniform:_aspectRatioUniform program:self.program]; + + if (CGSizeEqualToSize(_imageSize, CGSizeZero)) + { + [self setFloat:(float)newSize.width forUniform:_widthUniform program:self.program]; + [self setFloat:(float)newSize.height forUniform:_heightUniform program:self.program]; + } +} + +- (void)setImageSize:(CGSize)imageSize +{ + _imageSize = imageSize; + [self setFloat:(float)_imageSize.width forUniform:_widthUniform program:self.program]; + [self setFloat:(float)_imageSize.height forUniform:_heightUniform program:self.program]; +} + +@end + +@implementation PGPhotoToolComposer +{ + NSMutableArray *_advancedTools; + NSMutableArray *_tools; +} + +- (instancetype)init +{ + self = [super init]; + if (self != nil) + { + _tools = [NSMutableArray array]; + _advancedTools = [NSMutableArray array]; + } + return self; +} + +- (NSArray *)tools +{ + return _tools; +} + +- (NSArray *)advancedTools +{ + return _advancedTools; +} + +- (void)setImageSize:(CGSize)imageSize +{ + _imageSize = imageSize; + [(PGPhotoToolFilter *)_filter setImageSize:imageSize]; +} + +- (void)addPhotoTool:(PGPhotoTool *)tool +{ + if (!tool) + return; + + [_tools addObject:tool]; +} + +- (void)addPhotoTools:(NSArray *)tools +{ + [_tools addObjectsFromArray:tools]; +} + +- (void)compose +{ + [_tools sortUsingComparator:^NSComparisonResult(id obj1, id obj2) { + PGPhotoTool *t1 = (PGPhotoTool *)obj1; + PGPhotoTool *t2 = (PGPhotoTool *)obj2; + + if (t1.order > t2.order) + return NSOrderedDescending; + else if (t1.order < t2.order) + return NSOrderedAscending; + else + return NSOrderedSame; + }]; + + NSMutableString *shaderString = [NSMutableString string]; + [shaderString appendString:@"varying highp vec2 texCoord;"]; + [shaderString appendString:@"uniform sampler2D sourceImage;"]; + [shaderString appendString:@"uniform highp float aspectRatio;"]; + [shaderString appendString:@"uniform highp float width;"]; + [shaderString appendString:@"uniform highp float height;"]; + + NSMutableString *definitionsString = [NSMutableString string]; + NSMutableString *ancillaryShaderString = [NSMutableString string]; + NSMutableString *mainShaderString = [NSMutableString string]; + + NSMutableArray *uniforms = [NSMutableArray array]; + + for (PGPhotoTool *tool in _tools) + { + switch (tool.type) + { + case PGPhotoToolTypeShader: + { + if (tool.parameters) + { + for (PGPhotoProcessPassParameter *parameter in tool.parameters) + { + [definitionsString appendString:[NSString stringWithFormat:@"%@;", parameter.shaderString]]; + + if (parameter.isUniform) + [uniforms addObject:parameter]; + } + } + if (tool.ancillaryShaderString != nil) + [ancillaryShaderString appendString:tool.ancillaryShaderString]; + + if (tool.shaderString != nil) + [mainShaderString appendString:tool.shaderString]; + + tool.toolComposer = self; + } + break; + + case PGPhotoToolTypePass: + { + [_advancedTools addObject:tool]; + } + break; + } + } + + [shaderString appendString:definitionsString]; + [shaderString appendString:PGPhotoToolAncillaryShaderString]; + + [shaderString appendString:ancillaryShaderString]; + + [shaderString appendString:@"void main() {"]; + [shaderString appendString:@"lowp vec4 source = texture2D(sourceImage, texCoord);"]; + [shaderString appendString:@"lowp vec4 result = source;"]; + [shaderString appendString:@"const lowp float toolEpsilon = 0.005;"]; + + [shaderString appendString:mainShaderString]; + + [shaderString appendString:@"gl_FragColor = result;"]; + [shaderString appendString:@"}"]; + + [_filter removeAllTargets]; + + PGPhotoToolFilter *filter = [[PGPhotoToolFilter alloc] initWithFragmentShaderFromString:shaderString];; + for (PGPhotoProcessPassParameter *parameter in uniforms) + { + GLint uniformIndex = [filter uniformIndexForName:parameter.name]; + [parameter storeFilter:filter uniformIndex:uniformIndex]; + } + + for (PGPhotoTool *tool in _tools) + [tool updateParameters]; + + _filter = filter; +} + +- (bool)shouldBeSkipped +{ + return false; +} + +- (void)invalidate +{ + for (PGPhotoTool *tool in _tools) + [tool invalidate]; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGSaturationTool.h b/submodules/LegacyComponents/LegacyComponents/PGSaturationTool.h new file mode 100644 index 0000000000..943a80a4de --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGSaturationTool.h @@ -0,0 +1,5 @@ +#import "PGPhotoTool.h" + +@interface PGSaturationTool : PGPhotoTool + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGSaturationTool.m b/submodules/LegacyComponents/LegacyComponents/PGSaturationTool.m new file mode 100644 index 0000000000..ec5193a0cc --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGSaturationTool.m @@ -0,0 +1,75 @@ +#import "PGSaturationTool.h" + +#import "LegacyComponentsInternal.h" + +@interface PGSaturationTool () +{ + PGPhotoProcessPassParameter *_parameter; +} +@end + +@implementation PGSaturationTool + +- (instancetype)init +{ + self = [super init]; + if (self != nil) + { + _identifier = @"saturation"; + _type = PGPhotoToolTypeShader; + _order = 8; + + _minimumValue = -100; + _maximumValue = 100; + _defaultValue = 0; + + self.value = @(_defaultValue); + } + return self; +} + +- (NSString *)title +{ + return TGLocalized(@"PhotoEditor.SaturationTool"); +} + +- (bool)shouldBeSkipped +{ + return (ABS(((NSNumber *)self.displayValue).floatValue - (float)self.defaultValue) < FLT_EPSILON); +} + +- (NSArray *)parameters +{ + if (!_parameters) + { + _parameter = [PGPhotoProcessPassParameter parameterWithName:@"saturation" type:@"lowp float"]; + _parameters = @[ [PGPhotoProcessPassParameter constWithName:@"satLuminanceWeighting" type:@"mediump vec3" value:@"vec3(0.2126, 0.7152, 0.0722)"], + _parameter ]; + } + + return _parameters; +} + +- (void)updateParameters +{ + NSNumber *value = (NSNumber *)self.displayValue; + + CGFloat parameterValue = (value.floatValue / 100.0f); + if (parameterValue > 0) + parameterValue *= 1.05f; + parameterValue += 1; + [_parameter setFloatValue:parameterValue]; +} + +- (NSString *)shaderString +{ + return PGShaderString + ( + lowp float satLuminance = dot(result.rgb, satLuminanceWeighting); + lowp vec3 greyScaleColor = vec3(satLuminance); + + result = vec4(clamp(mix(greyScaleColor, result.rgb, saturation), 0.0, 1.0), result.a); + ); +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGShadowsTool.h b/submodules/LegacyComponents/LegacyComponents/PGShadowsTool.h new file mode 100644 index 0000000000..048891b698 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGShadowsTool.h @@ -0,0 +1,5 @@ +#import "PGPhotoTool.h" + +@interface PGShadowsTool : PGPhotoTool + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGShadowsTool.m b/submodules/LegacyComponents/LegacyComponents/PGShadowsTool.m new file mode 100644 index 0000000000..0fbf5ca6c2 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGShadowsTool.m @@ -0,0 +1,60 @@ +#import "PGShadowsTool.h" + +#import "LegacyComponentsInternal.h" + +@interface PGShadowsTool () +{ + PGPhotoProcessPassParameter *_parameter; +} +@end + +@implementation PGShadowsTool + +- (instancetype)init +{ + self = [super init]; + if (self != nil) + { + _identifier = @"shadows"; + _type = PGPhotoToolTypeShader; + _order = 4; + + _minimumValue = -100; + _maximumValue = 100; + _defaultValue = 0; + + self.value = @(_defaultValue); + } + return self; +} + +- (NSString *)title +{ + return TGLocalized(@"PhotoEditor.ShadowsTool"); +} + +- (bool)shouldBeSkipped +{ + return (ABS(((NSNumber *)self.displayValue).floatValue - (float)self.defaultValue) < FLT_EPSILON); +} + +- (NSArray *)parameters +{ + if (!_parameters) + { + _parameter = [PGPhotoProcessPassParameter parameterWithName:@"shadows" type:@"lowp float"]; + _parameters = @[ _parameter ]; + } + + return _parameters; +} + +- (void)updateParameters +{ + NSNumber *value = (NSNumber *)self.displayValue; + + CGFloat parameterValue = (value.floatValue * 0.55f + 100.0f) / 100.0f; + [_parameter setFloatValue:parameterValue]; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGSharpenTool.h b/submodules/LegacyComponents/LegacyComponents/PGSharpenTool.h new file mode 100644 index 0000000000..d62b457841 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGSharpenTool.h @@ -0,0 +1,5 @@ +#import "PGPhotoTool.h" + +@interface PGSharpenTool : PGPhotoTool + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGSharpenTool.m b/submodules/LegacyComponents/LegacyComponents/PGSharpenTool.m new file mode 100644 index 0000000000..cfeb696d6d --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGSharpenTool.m @@ -0,0 +1,53 @@ +#import "PGSharpenTool.h" + +#import "LegacyComponentsInternal.h" + +#import "PGPhotoSharpenPass.h" + +@implementation PGSharpenTool + +- (instancetype)init +{ + self = [super init]; + if (self != nil) + { + _identifier = @"sharpen"; + _type = PGPhotoToolTypePass; + _order = 2; + + _pass = [[PGPhotoSharpenPass alloc] init]; + + _minimumValue = 0; + _maximumValue = 100; + _defaultValue = 0; + + self.value = @(_defaultValue); + } + return self; +} + +- (NSString *)title +{ + return TGLocalized(@"PhotoEditor.SharpenTool"); +} + +- (PGPhotoProcessPass *)pass +{ + [self updatePassParameters]; + + return _pass; +} + +- (bool)shouldBeSkipped +{ + return false; + //return (fabsf(((NSNumber *)self.displayValue).floatValue - self.defaultValue) < FLT_EPSILON); +} + +- (void)updatePassParameters +{ + NSNumber *value = (NSNumber *)self.displayValue; + [(PGPhotoSharpenPass *)_pass setSharpness:0.125f + value.floatValue / 100 * 0.6f]; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGTintTool.h b/submodules/LegacyComponents/LegacyComponents/PGTintTool.h new file mode 100644 index 0000000000..74e710aa38 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGTintTool.h @@ -0,0 +1,16 @@ +#import "PGPhotoTool.h" + +@interface PGTintToolValue : NSObject + +@property (nonatomic, assign) UIColor *shadowsColor; +@property (nonatomic, assign) UIColor *highlightsColor; +@property (nonatomic, assign) CGFloat shadowsIntensity; +@property (nonatomic, assign) CGFloat highlightsIntensity; + +@property (nonatomic, assign) bool editingHighlights; + +@end + +@interface PGTintTool : PGPhotoTool + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGTintTool.m b/submodules/LegacyComponents/LegacyComponents/PGTintTool.m new file mode 100644 index 0000000000..57d5ae28e8 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGTintTool.m @@ -0,0 +1,234 @@ +#import "PGTintTool.h" +#import "TGPhotoEditorTintToolView.h" + +#import "LegacyComponentsInternal.h" + +@implementation PGTintToolValue + +- (instancetype)copyWithZone:(NSZone *)__unused zone +{ + PGTintToolValue *value = [[PGTintToolValue alloc] init]; + value.shadowsColor = self.shadowsColor; + value.shadowsIntensity = self.shadowsIntensity; + value.highlightsColor = self.highlightsColor; + value.highlightsIntensity = self.highlightsIntensity; + value.editingHighlights = self.editingHighlights; + + return value; +} + +- (id)cleanValue +{ + PGTintToolValue *value = [[PGTintToolValue alloc] init]; + value.shadowsColor = self.shadowsColor; + value.shadowsIntensity = self.shadowsIntensity; + value.highlightsColor = self.highlightsColor; + value.highlightsIntensity = self.highlightsIntensity; + + return value; +} + +- (BOOL)isEqual:(id)object +{ + if (object == self) + return true; + + if (!object || ![object isKindOfClass:[self class]]) + return false; + + PGTintToolValue *value = (PGTintToolValue *)object; + + if (![value.shadowsColor isEqual:self.shadowsColor]) + return false; + + if (value.shadowsIntensity != self.shadowsIntensity) + return false; + + if (![value.highlightsColor isEqual:self.highlightsColor]) + return false; + + if (value.highlightsIntensity != self.highlightsIntensity) + return false; + + return true; +} + +@end + + +@interface PGTintTool () +{ + PGPhotoProcessPassParameter *_shadowsIntensityParameter; + PGPhotoProcessPassParameter *_highlightsIntensityParameter; + PGPhotoProcessPassParameter *_shadowsTintColorParameter; + PGPhotoProcessPassParameter *_highlightsTintColorParameter; +} +@end + +@implementation PGTintTool + +- (instancetype)init +{ + self = [super init]; + if (self != nil) + { + _identifier = @"tint"; + _type = PGPhotoToolTypeShader; + _order = 9; + + _minimumValue = 0; + _maximumValue = 100; + _defaultValue = 0; + + PGTintToolValue *value = [[PGTintToolValue alloc] init]; + value.shadowsColor = [UIColor clearColor]; + value.shadowsIntensity = 50.0f; + value.highlightsColor = [UIColor clearColor]; + value.highlightsIntensity = 50.0f; + + self.value = value; + } + return self; +} + +- (NSString *)title +{ + return TGLocalized(@"PhotoEditor.TintTool"); +} + +- (UIView *)itemControlViewWithChangeBlock:(void (^)(id, bool))changeBlock explicit:(bool)explicit nameWidth:(CGFloat)__unused nameWidth +{ + __weak PGTintTool *weakSelf = self; + + UIView *view = [[TGPhotoEditorTintToolView alloc] initWithEditorItem:self]; + view.valueChanged = ^(id newValue, bool animated) + { + __strong PGTintTool *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + if (!explicit && [strongSelf.tempValue isEqual:newValue]) + return; + + if (explicit && [strongSelf.value isEqual:newValue]) + return; + + if (!explicit) + strongSelf.tempValue = newValue; + else + strongSelf.value = newValue; + + if (changeBlock != nil) + changeBlock(newValue, animated); + }; + return view; +} + +- (bool)shouldBeSkipped +{ + PGTintToolValue *value = (PGTintToolValue *)self.displayValue; + return (value.highlightsIntensity < FLT_EPSILON && value.shadowsIntensity < FLT_EPSILON) || ([value.highlightsColor isEqual:[UIColor clearColor]] && [value.shadowsColor isEqual:[UIColor clearColor]]); +} + +- (Class)valueClass +{ + return [PGTintToolValue class]; +} + +- (NSArray *)parameters +{ + if (!_parameters) + { + _shadowsIntensityParameter = [PGPhotoProcessPassParameter parameterWithName:@"shadowsTintIntensity" type:@"lowp float"]; + _highlightsIntensityParameter = [PGPhotoProcessPassParameter parameterWithName:@"highlightsTintIntensity" type:@"lowp float"]; + _shadowsTintColorParameter = [PGPhotoProcessPassParameter parameterWithName:@"shadowsTintColor" type:@"lowp vec3"]; + _highlightsTintColorParameter = [PGPhotoProcessPassParameter parameterWithName:@"highlightsTintColor" type:@"lowp vec3"]; + _parameters = @[ _shadowsIntensityParameter, _highlightsIntensityParameter, + _shadowsTintColorParameter, _highlightsTintColorParameter ]; + } + + return _parameters; +} + +- (void)updateParameters +{ + PGTintToolValue *value = (PGTintToolValue *)self.displayValue; + if (value == nil) + return; + + [_shadowsTintColorParameter setColorValue:value.shadowsColor]; + CGFloat shadowsIntensity = [value.shadowsColor isEqual:[UIColor clearColor]] ? 0 : value.shadowsIntensity; + [_shadowsIntensityParameter setFloatValue:shadowsIntensity / 100.0f]; + + [_highlightsTintColorParameter setColorValue:value.highlightsColor]; + CGFloat highlightsIntensity = [value.highlightsColor isEqual:[UIColor clearColor]] ? 0 : value.highlightsIntensity; + [_highlightsIntensityParameter setFloatValue:highlightsIntensity / 100.0f]; +} + +- (NSString *)stringValue +{ + if (![self shouldBeSkipped]) + return @"◆"; + + return nil; +} + + +- (NSString *)ancillaryShaderString +{ + return PGShaderString + ( + lowp vec3 tintRaiseShadowsCurve(lowp vec3 color) { + highp vec3 co1 = vec3(-0.003671); + highp vec3 co2 = vec3(0.3842); + highp vec3 co3 = vec3(0.3764); + highp vec3 co4 = vec3(0.2515); + + highp vec3 comp1 = co1 * pow(color, vec3(3.0)); + highp vec3 comp2 = co2 * pow(color, vec3(2.0)); + highp vec3 comp3 = co3 * color; + highp vec3 comp4 = co4; + + return comp1 + comp2 + comp3 + comp4; + } + + lowp vec3 tintShadows(lowp vec3 texel, lowp vec3 tintColor, lowp float tintAmount) { + highp vec3 raisedShadows = tintRaiseShadowsCurve(texel); + + highp vec3 tintedShadows = mix(texel, raisedShadows, tintColor); + highp vec3 tintedShadowsWithAmount = mix(texel, tintedShadows, tintAmount); + + return clamp(tintedShadowsWithAmount, 0.0, 1.0); + } + + lowp vec3 tintHighlights(lowp vec3 texel, lowp vec3 tintColor, lowp float tintAmount) { + lowp vec3 loweredHighlights = vec3(1.0) - tintRaiseShadowsCurve(vec3(1.0) - texel); + + lowp vec3 tintedHighlights = mix(texel, loweredHighlights, (vec3(1.0) - tintColor)); + lowp vec3 tintedHighlightsWithAmount = mix(texel, tintedHighlights, tintAmount); + + return clamp(tintedHighlightsWithAmount, 0.0, 1.0); + } + ); +} + +- (NSString *)shaderString +{ + return PGShaderString + ( + if (abs(shadowsTintIntensity) > toolEpsilon) { + result.rgb = tintShadows(result.rgb, shadowsTintColor, shadowsTintIntensity * 2.0); + } + + if (abs(highlightsTintIntensity) > toolEpsilon) { + result.rgb = tintHighlights(result.rgb, highlightsTintColor, highlightsTintIntensity * 2.0); + } + ); +} + +- (bool)isSimple +{ + return false; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGVignetteTool.h b/submodules/LegacyComponents/LegacyComponents/PGVignetteTool.h new file mode 100644 index 0000000000..d93512e9a5 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGVignetteTool.h @@ -0,0 +1,5 @@ +#import "PGPhotoTool.h" + +@interface PGVignetteTool : PGPhotoTool + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGVignetteTool.m b/submodules/LegacyComponents/LegacyComponents/PGVignetteTool.m new file mode 100644 index 0000000000..3e64b56e5b --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGVignetteTool.m @@ -0,0 +1,75 @@ +#import "PGVignetteTool.h" + +#import "LegacyComponentsInternal.h" + +@interface PGVignetteTool () +{ + PGPhotoProcessPassParameter *_parameter; +} +@end + +@implementation PGVignetteTool + +- (instancetype)init +{ + self = [super init]; + if (self != nil) + { + _identifier = @"vignette"; + _type = PGPhotoToolTypeShader; + _order = 13; + + _minimumValue = 0; + _maximumValue = 100; + _defaultValue = 0; + + self.value = @(_defaultValue); + } + return self; +} + +- (NSString *)title +{ + return TGLocalized(@"PhotoEditor.VignetteTool"); +} + +- (bool)shouldBeSkipped +{ + return (ABS(((NSNumber *)self.displayValue).floatValue - (float)self.defaultValue) < FLT_EPSILON); +} + +- (NSArray *)parameters +{ + if (!_parameters) + { + _parameter = [PGPhotoProcessPassParameter parameterWithName:@"vignette" type:@"lowp float"]; + _parameters = @[ _parameter ]; + } + + return _parameters; +} + +- (void)updateParameters +{ + NSNumber *value = (NSNumber *)self.displayValue; + + CGFloat parameterValue = value.floatValue / 100.0f; + [_parameter setFloatValue:parameterValue]; +} + +- (NSString *)shaderString +{ + return PGShaderString + ( + if (abs(vignette) > toolEpsilon) { + const lowp float midpoint = 0.7; + const lowp float fuzziness = 0.62; + + lowp float radDist = length(texCoord - 0.5) / sqrt(0.5); + lowp float mag = easeInOutSigmoid(radDist * midpoint, fuzziness) * vignette * 0.645; + result.rgb = mix(pow(result.rgb, vec3(1.0 / (1.0 - mag))), vec3(0.0), mag * mag); + } + ); +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGWarmthTool.h b/submodules/LegacyComponents/LegacyComponents/PGWarmthTool.h new file mode 100644 index 0000000000..7d9c49d0fb --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGWarmthTool.h @@ -0,0 +1,5 @@ +#import "PGPhotoTool.h" + +@interface PGWarmthTool : PGPhotoTool + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PGWarmthTool.m b/submodules/LegacyComponents/LegacyComponents/PGWarmthTool.m new file mode 100644 index 0000000000..05fdce3c61 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PGWarmthTool.m @@ -0,0 +1,85 @@ +#import "PGWarmthTool.h" + +#import "LegacyComponentsInternal.h" + +@interface PGWarmthTool () +{ + PGPhotoProcessPassParameter *_parameter; +} +@end + +@implementation PGWarmthTool + +- (instancetype)init +{ + self = [super init]; + if (self != nil) + { + _identifier = @"warmth"; + _type = PGPhotoToolTypeShader; + _order = 11; + + _minimumValue = -100; + _maximumValue = 100; + _defaultValue = 0; + + self.value = @(_defaultValue); + } + return self; +} + +- (NSString *)title +{ + return TGLocalized(@"PhotoEditor.WarmthTool"); +} + +- (bool)shouldBeSkipped +{ + return (ABS(((NSNumber *)self.displayValue).floatValue - self.defaultValue) < FLT_EPSILON); +} + +- (NSArray *)parameters +{ + if (!_parameters) + { + _parameter = [PGPhotoProcessPassParameter parameterWithName:@"warmth" type:@"lowp float"]; + _parameters = @[ _parameter ]; + } + + return _parameters; +} + +- (void)updateParameters +{ + NSNumber *value = (NSNumber *)self.displayValue; + + CGFloat parameterValue = value.floatValue / 100.0f; + [_parameter setFloatValue:parameterValue]; +} + +- (NSString *)shaderString +{ + return PGShaderString + ( + if (abs(warmth) > toolEpsilon) { + highp vec3 yuvVec; + + if (warmth > 0.0 ) { + yuvVec = vec3(0.1765, -0.1255, 0.0902); + } + else { + yuvVec = -vec3(0.0588, 0.1569, -0.1255); + } + highp vec3 yuvColor = rgbToYuv(result.rgb); + + highp float luma = yuvColor.r; + + highp float curveScale = sin(luma * 3.14159); + + yuvColor += 0.375 * warmth * curveScale * yuvVec; + result.rgb = yuvToRgb(yuvColor); + } + ); +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/POPAction.h b/submodules/LegacyComponents/LegacyComponents/POPAction.h new file mode 100644 index 0000000000..aa51ef45ce --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/POPAction.h @@ -0,0 +1,66 @@ +/** + Copyright (c) 2014-present, Facebook, Inc. + All rights reserved. + + This source code is licensed under the BSD-style license found in the + LICENSE file in the root directory of this source tree. An additional grant + of patent rights can be found in the PATENTS file in the same directory. + */ + +#ifndef POPACTION_H +#define POPACTION_H + +#import +#import "POPDefines.h" + +#ifdef __cplusplus + +namespace POP { + + /** + @abstract Disables Core Animation actions using RAII. + @discussion The disablement of actions is scoped to the current transaction. + */ + class ActionDisabler + { + BOOL state; + + public: + ActionDisabler() POP_NOTHROW + { + state = [CATransaction disableActions]; + [CATransaction setDisableActions:YES]; + } + + ~ActionDisabler() + { + [CATransaction setDisableActions:state]; + } + }; + + /** + @abstract Enables Core Animation actions using RAII. + @discussion The enablement of actions is scoped to the current transaction. + */ + class ActionEnabler + { + BOOL state; + + public: + ActionEnabler() POP_NOTHROW + { + state = [CATransaction disableActions]; + [CATransaction setDisableActions:NO]; + } + + ~ActionEnabler() + { + [CATransaction setDisableActions:state]; + } + }; + +} + +#endif /* __cplusplus */ + +#endif /* POPACTION_H */ diff --git a/submodules/LegacyComponents/LegacyComponents/POPAnimatableProperty.h b/submodules/LegacyComponents/LegacyComponents/POPAnimatableProperty.h new file mode 100644 index 0000000000..0ac4cbd688 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/POPAnimatableProperty.h @@ -0,0 +1,223 @@ +/** + Copyright (c) 2014-present, Facebook, Inc. + All rights reserved. + + This source code is licensed under the BSD-style license found in the + LICENSE file in the root directory of this source tree. An additional grant + of patent rights can be found in the PATENTS file in the same directory. + */ + +#import +#import + +@class POPMutableAnimatableProperty; + +/** + @abstract Describes an animatable property. + */ +@interface POPAnimatableProperty : NSObject + +/** + @abstract Property accessor. + @param name The name of the property. + @return The animatable property with that name or nil if it does not exist. + @discussion Common animatable properties are included by default. Use the provided constants to reference. + */ ++ (id)propertyWithName:(NSString *)name; + +/** + @abstract The designated initializer. + @param name The name of the property. + @param block The block used to configure the property on creation. + @return The animatable property with name if it exists, otherwise a newly created instance configured by block. + @discussion Custom properties should use reverse-DNS naming. A newly created instance is only mutable in the scope of block. Once constructed, a property becomes immutable. + */ ++ (id)propertyWithName:(NSString *)name initializer:(void (^)(POPMutableAnimatableProperty *prop))block; + +/** + @abstract The name of the property. + @discussion Used to uniquely identify an animatable property. + */ +@property (readonly, nonatomic, copy) NSString *name; + +/** + @abstract Block used to read values from a property into an array of floats. + */ +@property (readonly, nonatomic, copy) void (^readBlock)(id obj, CGFloat values[]); + +/** + @abstract Block used to write values from an array of floats into a property. + */ +@property (readonly, nonatomic, copy) void (^writeBlock)(id obj, const CGFloat values[]); + +/** + @abstract The threshold value used when determining completion of dynamics simulations. + */ +@property (readonly, nonatomic, assign) CGFloat threshold; + +@end + +/** + @abstract A mutable animatable property intended for configuration. + */ +@interface POPMutableAnimatableProperty : POPAnimatableProperty + +/** + @abstract A read-write version of POPAnimatableProperty name property. + */ +@property (readwrite, nonatomic, copy) NSString *name; + +/** + @abstract A read-write version of POPAnimatableProperty readBlock property. + */ +@property (readwrite, nonatomic, copy) void (^readBlock)(id obj, CGFloat values[]); + +/** + @abstract A read-write version of POPAnimatableProperty writeBlock property. + */ +@property (readwrite, nonatomic, copy) void (^writeBlock)(id obj, const CGFloat values[]); + +/** + @abstract A read-write version of POPAnimatableProperty threshold property. + */ +@property (readwrite, nonatomic, assign) CGFloat threshold; + +@end + +/** + Common CALayer property names. + */ +extern NSString * const kPOPLayerBackgroundColor; +extern NSString * const kPOPLayerBounds; +extern NSString * const kPOPLayerCornerRadius; +extern NSString * const kPOPLayerBorderWidth; +extern NSString * const kPOPLayerBorderColor; +extern NSString * const kPOPLayerOpacity; +extern NSString * const kPOPLayerPosition; +extern NSString * const kPOPLayerPositionX; +extern NSString * const kPOPLayerPositionY; +extern NSString * const kPOPLayerRotation; +extern NSString * const kPOPLayerRotationX; +extern NSString * const kPOPLayerRotationY; +extern NSString * const kPOPLayerScaleX; +extern NSString * const kPOPLayerScaleXY; +extern NSString * const kPOPLayerScaleY; +extern NSString * const kPOPLayerSize; +extern NSString * const kPOPLayerSubscaleXY; +extern NSString * const kPOPLayerSubtranslationX; +extern NSString * const kPOPLayerSubtranslationXY; +extern NSString * const kPOPLayerSubtranslationY; +extern NSString * const kPOPLayerSubtranslationZ; +extern NSString * const kPOPLayerTranslationX; +extern NSString * const kPOPLayerTranslationXY; +extern NSString * const kPOPLayerTranslationY; +extern NSString * const kPOPLayerTranslationZ; +extern NSString * const kPOPLayerZPosition; +extern NSString * const kPOPLayerShadowColor; +extern NSString * const kPOPLayerShadowOffset; +extern NSString * const kPOPLayerShadowOpacity; +extern NSString * const kPOPLayerShadowRadius; +extern NSString * const kPOPShapeLayerLineWidth; + +/** + Common CAShapeLayer property names. + */ +extern NSString * const kPOPShapeLayerStrokeStart; +extern NSString * const kPOPShapeLayerStrokeEnd; +extern NSString * const kPOPShapeLayerStrokeColor; +extern NSString * const kPOPShapeLayerFillColor; + +/** + Common NSLayoutConstraint property names. + */ +extern NSString * const kPOPLayoutConstraintConstant; + + +#if TARGET_OS_IPHONE + +/** + Common UIView property names. + */ +extern NSString * const kPOPViewAlpha; +extern NSString * const kPOPViewBackgroundColor; +extern NSString * const kPOPViewBounds; +extern NSString * const kPOPViewCenter; +extern NSString * const kPOPViewFrame; +extern NSString * const kPOPViewScaleX; +extern NSString * const kPOPViewScaleXY; +extern NSString * const kPOPViewScaleY; +extern NSString * const kPOPViewSize; +extern NSString * const kPOPViewTintColor; + +/** + Common UIScrollView property names. + */ +extern NSString * const kPOPScrollViewContentOffset; +extern NSString * const kPOPScrollViewContentSize; +extern NSString * const kPOPScrollViewZoomScale; +extern NSString * const kPOPScrollViewContentInset; + +/** + Common UITableView property names. + */ +extern NSString * const kPOPTableViewContentOffset; +extern NSString * const kPOPTableViewContentSize; + +/** + Common UICollectionView property names. + */ +extern NSString * const kPOPCollectionViewContentOffset; +extern NSString * const kPOPCollectionViewContentSize; + +/** + Common UINavigationBar property names. + */ +extern NSString * const kPOPNavigationBarBarTintColor; + +/** + Common UIToolbar property names. + */ +extern NSString * const kPOPToolbarBarTintColor; + +/** + Common UITabBar property names. + */ +extern NSString * const kPOPTabBarBarTintColor; + +/** + Common UILabel property names. + */ +extern NSString * const kPOPLabelTextColor; + +/** + Common SceneKit property names. + */ +extern NSString * const kPOPSCNNodePosition; +extern NSString * const kPOPSCNNodePositionX; +extern NSString * const kPOPSCNNodePositionY; +extern NSString * const kPOPSCNNodePositionZ; +extern NSString * const kPOPSCNNodeTranslation; +extern NSString * const kPOPSCNNodeTranslationX; +extern NSString * const kPOPSCNNodeTranslationY; +extern NSString * const kPOPSCNNodeTranslationZ; +extern NSString * const kPOPSCNNodeRotation; +extern NSString * const kPOPSCNNodeRotationX; +extern NSString * const kPOPSCNNodeRotationY; +extern NSString * const kPOPSCNNodeRotationZ; +extern NSString * const kPOPSCNNodeRotationW; +extern NSString * const kPOPSCNNodeEulerAngles; +extern NSString * const kPOPSCNNodeEulerAnglesX; +extern NSString * const kPOPSCNNodeEulerAnglesY; +extern NSString * const kPOPSCNNodeEulerAnglesZ; +extern NSString * const kPOPSCNNodeOrientation; +extern NSString * const kPOPSCNNodeOrientationX; +extern NSString * const kPOPSCNNodeOrientationY; +extern NSString * const kPOPSCNNodeOrientationZ; +extern NSString * const kPOPSCNNodeOrientationW; +extern NSString * const kPOPSCNNodeScale; +extern NSString * const kPOPSCNNodeScaleX; +extern NSString * const kPOPSCNNodeScaleY; +extern NSString * const kPOPSCNNodeScaleZ; +extern NSString * const kPOPSCNNodeScaleXY; + +#endif diff --git a/submodules/LegacyComponents/LegacyComponents/POPAnimatableProperty.mm b/submodules/LegacyComponents/LegacyComponents/POPAnimatableProperty.mm new file mode 100644 index 0000000000..8cd843659a --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/POPAnimatableProperty.mm @@ -0,0 +1,1171 @@ +/** + Copyright (c) 2014-present, Facebook, Inc. + All rights reserved. + + This source code is licensed under the BSD-style license found in the + LICENSE file in the root directory of this source tree. An additional grant + of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "POPAnimatableProperty.h" +#import "POPCGUtils.h" +#import "POPAnimationRuntime.h" + +#import + +#if SCENEKIT_SDK_AVAILABLE +#import +#endif + +#import "POPLayerExtras.h" + +#if TARGET_OS_IPHONE +#import +#else +#import +#endif + +// common threshold definitions +static CGFloat const kPOPThresholdColor = 0.01; +static CGFloat const kPOPThresholdPoint = 1.0; +static CGFloat const kPOPThresholdOpacity = 0.01; +static CGFloat const kPOPThresholdScale = 0.005; +static CGFloat const kPOPThresholdRotation = 0.01; +static CGFloat const kPOPThresholdRadius = 0.01; + +#pragma mark - Static + +// CALayer +NSString * const kPOPLayerBackgroundColor = @"backgroundColor"; +NSString * const kPOPLayerBounds = @"bounds"; +NSString * const kPOPLayerCornerRadius = @"cornerRadius"; +NSString * const kPOPLayerBorderWidth = @"borderWidth"; +NSString * const kPOPLayerBorderColor = @"borderColor"; +NSString * const kPOPLayerOpacity = @"opacity"; +NSString * const kPOPLayerPosition = @"position"; +NSString * const kPOPLayerPositionX = @"positionX"; +NSString * const kPOPLayerPositionY = @"positionY"; +NSString * const kPOPLayerRotation = @"rotation"; +NSString * const kPOPLayerRotationX = @"rotationX"; +NSString * const kPOPLayerRotationY = @"rotationY"; +NSString * const kPOPLayerScaleX = @"scaleX"; +NSString * const kPOPLayerScaleXY = @"scaleXY"; +NSString * const kPOPLayerScaleY = @"scaleY"; +NSString * const kPOPLayerSize = @"size"; +NSString * const kPOPLayerSubscaleXY = @"subscaleXY"; +NSString * const kPOPLayerSubtranslationX = @"subtranslationX"; +NSString * const kPOPLayerSubtranslationXY = @"subtranslationXY"; +NSString * const kPOPLayerSubtranslationY = @"subtranslationY"; +NSString * const kPOPLayerSubtranslationZ = @"subtranslationZ"; +NSString * const kPOPLayerTranslationX = @"translationX"; +NSString * const kPOPLayerTranslationXY = @"translationXY"; +NSString * const kPOPLayerTranslationY = @"translationY"; +NSString * const kPOPLayerTranslationZ = @"translationZ"; +NSString * const kPOPLayerZPosition = @"zPosition"; +NSString * const kPOPLayerShadowColor = @"shadowColor"; +NSString * const kPOPLayerShadowOffset = @"shadowOffset"; +NSString * const kPOPLayerShadowOpacity = @"shadowOpacity"; +NSString * const kPOPLayerShadowRadius = @"shadowRadius"; + +// CAShapeLayer +NSString * const kPOPShapeLayerStrokeStart = @"shapeLayer.strokeStart"; +NSString * const kPOPShapeLayerStrokeEnd = @"shapeLayer.strokeEnd"; +NSString * const kPOPShapeLayerStrokeColor = @"shapeLayer.strokeColor"; +NSString * const kPOPShapeLayerFillColor = @"shapeLayer.fillColor"; +NSString * const kPOPShapeLayerLineWidth = @"shapeLayer.lineWidth"; + +// NSLayoutConstraint +NSString * const kPOPLayoutConstraintConstant = @"layoutConstraint.constant"; + +// UIView +NSString * const kPOPViewAlpha = @"view.alpha"; +NSString * const kPOPViewBackgroundColor = @"view.backgroundColor"; +NSString * const kPOPViewBounds = kPOPLayerBounds; +NSString * const kPOPViewCenter = @"view.center"; +NSString * const kPOPViewFrame = @"view.frame"; +NSString * const kPOPViewScaleX = @"view.scaleX"; +NSString * const kPOPViewScaleXY = @"view.scaleXY"; +NSString * const kPOPViewScaleY = @"view.scaleY"; +NSString * const kPOPViewSize = kPOPLayerSize; +NSString * const kPOPViewTintColor = @"view.tintColor"; + +// UIScrollView +NSString * const kPOPScrollViewContentOffset = @"scrollView.contentOffset"; +NSString * const kPOPScrollViewContentSize = @"scrollView.contentSize"; +NSString * const kPOPScrollViewZoomScale = @"scrollView.zoomScale"; +NSString * const kPOPScrollViewContentInset = @"scrollView.contentInset"; + +// UITableView +NSString * const kPOPTableViewContentOffset = kPOPScrollViewContentOffset; +NSString * const kPOPTableViewContentSize = kPOPScrollViewContentSize; + +// UICollectionView +NSString * const kPOPCollectionViewContentOffset = kPOPScrollViewContentOffset; +NSString * const kPOPCollectionViewContentSize = kPOPScrollViewContentSize; + +// UINavigationBar +NSString * const kPOPNavigationBarBarTintColor = @"navigationBar.barTintColor"; + +// UIToolbar +NSString * const kPOPToolbarBarTintColor = kPOPNavigationBarBarTintColor; + +// UITabBar +NSString * const kPOPTabBarBarTintColor = kPOPNavigationBarBarTintColor; + +//UILabel +NSString * const kPOPLabelTextColor = @"label.textColor"; + +//SceneKit +NSString * const kPOPSCNNodePosition = @"scnode.position"; +NSString * const kPOPSCNNodePositionX = @"scnnode.position.x"; +NSString * const kPOPSCNNodePositionY = @"scnnode.position.y"; +NSString * const kPOPSCNNodePositionZ = @"scnnode.position.z"; +NSString * const kPOPSCNNodeTranslation = @"scnnode.translation"; +NSString * const kPOPSCNNodeTranslationX = @"scnnode.translation.x"; +NSString * const kPOPSCNNodeTranslationY = @"scnnode.translation.y"; +NSString * const kPOPSCNNodeTranslationZ = @"scnnode.translation.z"; +NSString * const kPOPSCNNodeRotation = @"scnnode.rotation"; +NSString * const kPOPSCNNodeRotationX = @"scnnode.rotation.x"; +NSString * const kPOPSCNNodeRotationY = @"scnnode.rotation.y"; +NSString * const kPOPSCNNodeRotationZ = @"scnnode.rotation.z"; +NSString * const kPOPSCNNodeRotationW = @"scnnode.rotation.w"; +NSString * const kPOPSCNNodeEulerAngles = @"scnnode.eulerAngles"; +NSString * const kPOPSCNNodeEulerAnglesX = @"scnnode.eulerAngles.x"; +NSString * const kPOPSCNNodeEulerAnglesY = @"scnnode.eulerAngles.y"; +NSString * const kPOPSCNNodeEulerAnglesZ = @"scnnode.eulerAngles.z"; +NSString * const kPOPSCNNodeOrientation = @"scnnode.orientation"; +NSString * const kPOPSCNNodeOrientationX = @"scnnode.orientation.x"; +NSString * const kPOPSCNNodeOrientationY = @"scnnode.orientation.y"; +NSString * const kPOPSCNNodeOrientationZ = @"scnnode.orientation.z"; +NSString * const kPOPSCNNodeOrientationW = @"scnnode.orientation.w"; +NSString * const kPOPSCNNodeScale = @"scnnode.scale"; +NSString * const kPOPSCNNodeScaleX = @"scnnode.scale.x"; +NSString * const kPOPSCNNodeScaleY = @"scnnode.scale.y"; +NSString * const kPOPSCNNodeScaleZ = @"scnnode.scale.z"; +NSString * const kPOPSCNNodeScaleXY = @"scnnode.scale.xy"; + +/** + State structure internal to static animatable property. + */ +typedef struct +{ + NSString *name; + pop_animatable_read_block readBlock; + pop_animatable_write_block writeBlock; + CGFloat threshold; +} _POPStaticAnimatablePropertyState; +typedef _POPStaticAnimatablePropertyState POPStaticAnimatablePropertyState; + +static POPStaticAnimatablePropertyState _staticStates[] = +{ + /* CALayer */ + + {kPOPLayerBackgroundColor, + ^(CALayer *obj, CGFloat values[]) { + POPCGColorGetRGBAComponents(obj.backgroundColor, values); + }, + ^(CALayer *obj, const CGFloat values[]) { + CGColorRef color = POPCGColorRGBACreate(values); + [obj setBackgroundColor:color]; + CGColorRelease(color); + }, + kPOPThresholdColor + }, + + {kPOPLayerBounds, + ^(CALayer *obj, CGFloat values[]) { + values_from_rect(values, [obj bounds]); + }, + ^(CALayer *obj, const CGFloat values[]) { + [obj setBounds:values_to_rect(values)]; + }, + kPOPThresholdPoint + }, + + {kPOPLayerCornerRadius, + ^(CALayer *obj, CGFloat values[]) { + values[0] = [obj cornerRadius]; + }, + ^(CALayer *obj, const CGFloat values[]) { + [obj setCornerRadius:values[0]]; + }, + kPOPThresholdRadius + }, + + {kPOPLayerBorderWidth, + ^(CALayer *obj, CGFloat values[]) { + values[0] = [obj borderWidth]; + }, + ^(CALayer *obj, const CGFloat values[]) { + [obj setBorderWidth:values[0]]; + }, + 0.01 + }, + + {kPOPLayerBorderColor, + ^(CALayer *obj, CGFloat values[]) { + POPCGColorGetRGBAComponents(obj.borderColor, values); + }, + ^(CALayer *obj, const CGFloat values[]) { + CGColorRef color = POPCGColorRGBACreate(values); + [obj setBorderColor:color]; + CGColorRelease(color); + }, + kPOPThresholdColor + }, + + {kPOPLayerPosition, + ^(CALayer *obj, CGFloat values[]) { + values_from_point(values, [(CALayer *)obj position]); + }, + ^(CALayer *obj, const CGFloat values[]) { + [obj setPosition:values_to_point(values)]; + }, + kPOPThresholdPoint + }, + + {kPOPLayerPositionX, + ^(CALayer *obj, CGFloat values[]) { + values[0] = [(CALayer *)obj position].x; + }, + ^(CALayer *obj, const CGFloat values[]) { + CGPoint p = [(CALayer *)obj position]; + p.x = values[0]; + [obj setPosition:p]; + }, + kPOPThresholdPoint + }, + + {kPOPLayerPositionY, + ^(CALayer *obj, CGFloat values[]) { + values[0] = [(CALayer *)obj position].y; + }, + ^(CALayer *obj, const CGFloat values[]) { + CGPoint p = [(CALayer *)obj position]; + p.y = values[0]; + [obj setPosition:p]; + }, + kPOPThresholdPoint + }, + + {kPOPLayerOpacity, + ^(CALayer *obj, CGFloat values[]) { + values[0] = [obj opacity]; + }, + ^(CALayer *obj, const CGFloat values[]) { + [obj setOpacity:((float)values[0])]; + }, + kPOPThresholdOpacity + }, + + {kPOPLayerScaleX, + ^(CALayer *obj, CGFloat values[]) { + values[0] = POPLayerGetScaleX(obj); + }, + ^(CALayer *obj, const CGFloat values[]) { + POPLayerSetScaleX(obj, values[0]); + }, + kPOPThresholdScale + }, + + {kPOPLayerScaleY, + ^(CALayer *obj, CGFloat values[]) { + values[0] = POPLayerGetScaleY(obj); + }, + ^(CALayer *obj, const CGFloat values[]) { + POPLayerSetScaleY(obj, values[0]); + }, + kPOPThresholdScale + }, + + {kPOPLayerScaleXY, + ^(CALayer *obj, CGFloat values[]) { + values_from_point(values, POPLayerGetScaleXY(obj)); + }, + ^(CALayer *obj, const CGFloat values[]) { + POPLayerSetScaleXY(obj, values_to_point(values)); + }, + kPOPThresholdScale + }, + + {kPOPLayerSubscaleXY, + ^(CALayer *obj, CGFloat values[]) { + values_from_point(values, POPLayerGetSubScaleXY(obj)); + }, + ^(CALayer *obj, const CGFloat values[]) { + POPLayerSetSubScaleXY(obj, values_to_point(values)); + }, + kPOPThresholdScale + }, + + {kPOPLayerTranslationX, + ^(CALayer *obj, CGFloat values[]) { + values[0] = POPLayerGetTranslationX(obj); + }, + ^(CALayer *obj, const CGFloat values[]) { + POPLayerSetTranslationX(obj, values[0]); + }, + kPOPThresholdPoint + }, + + {kPOPLayerTranslationY, + ^(CALayer *obj, CGFloat values[]) { + values[0] = POPLayerGetTranslationY(obj); + }, + ^(CALayer *obj, const CGFloat values[]) { + POPLayerSetTranslationY(obj, values[0]); + }, + kPOPThresholdPoint + }, + + {kPOPLayerTranslationZ, + ^(CALayer *obj, CGFloat values[]) { + values[0] = POPLayerGetTranslationZ(obj); + }, + ^(CALayer *obj, const CGFloat values[]) { + POPLayerSetTranslationZ(obj, values[0]); + }, + kPOPThresholdPoint + }, + + {kPOPLayerTranslationXY, + ^(CALayer *obj, CGFloat values[]) { + values_from_point(values, POPLayerGetTranslationXY(obj)); + }, + ^(CALayer *obj, const CGFloat values[]) { + POPLayerSetTranslationXY(obj, values_to_point(values)); + }, + kPOPThresholdPoint + }, + + {kPOPLayerSubtranslationX, + ^(CALayer *obj, CGFloat values[]) { + values[0] = POPLayerGetSubTranslationX(obj); + }, + ^(CALayer *obj, const CGFloat values[]) { + POPLayerSetSubTranslationX(obj, values[0]); + }, + kPOPThresholdPoint + }, + + {kPOPLayerSubtranslationY, + ^(CALayer *obj, CGFloat values[]) { + values[0] = POPLayerGetSubTranslationY(obj); + }, + ^(CALayer *obj, const CGFloat values[]) { + POPLayerSetSubTranslationY(obj, values[0]); + }, + kPOPThresholdPoint + }, + + {kPOPLayerSubtranslationZ, + ^(CALayer *obj, CGFloat values[]) { + values[0] = POPLayerGetSubTranslationZ(obj); + }, + ^(CALayer *obj, const CGFloat values[]) { + POPLayerSetSubTranslationZ(obj, values[0]); + }, + kPOPThresholdPoint + }, + + {kPOPLayerSubtranslationXY, + ^(CALayer *obj, CGFloat values[]) { + values_from_point(values, POPLayerGetSubTranslationXY(obj)); + }, + ^(CALayer *obj, const CGFloat values[]) { + POPLayerSetSubTranslationXY(obj, values_to_point(values)); + }, + kPOPThresholdPoint + }, + + {kPOPLayerZPosition, + ^(CALayer *obj, CGFloat values[]) { + values[0] = [obj zPosition]; + }, + ^(CALayer *obj, const CGFloat values[]) { + [obj setZPosition:values[0]]; + }, + kPOPThresholdPoint + }, + + {kPOPLayerSize, + ^(CALayer *obj, CGFloat values[]) { + values_from_size(values, [obj bounds].size); + }, + ^(CALayer *obj, const CGFloat values[]) { + CGSize size = values_to_size(values); + if (size.width < 0. || size.height < 0.) + return; + + CGRect b = [obj bounds]; + b.size = size; + [obj setBounds:b]; + }, + kPOPThresholdPoint + }, + + {kPOPLayerRotation, + ^(CALayer *obj, CGFloat values[]) { + values[0] = POPLayerGetRotation(obj); + }, + ^(CALayer *obj, const CGFloat values[]) { + POPLayerSetRotation(obj, values[0]); + }, + kPOPThresholdRotation + }, + + {kPOPLayerRotationY, + ^(CALayer *obj, CGFloat values[]) { + values[0] = POPLayerGetRotationY(obj); + }, + ^(id obj, const CGFloat values[]) { + POPLayerSetRotationY(obj, values[0]); + }, + kPOPThresholdRotation + }, + + {kPOPLayerRotationX, + ^(CALayer *obj, CGFloat values[]) { + values[0] = POPLayerGetRotationX(obj); + }, + ^(CALayer *obj, const CGFloat values[]) { + POPLayerSetRotationX(obj, values[0]); + }, + kPOPThresholdRotation + }, + + {kPOPLayerShadowColor, + ^(CALayer *obj, CGFloat values[]) { + POPCGColorGetRGBAComponents(obj.shadowColor, values); + }, + ^(CALayer *obj, const CGFloat values[]) { + CGColorRef color = POPCGColorRGBACreate(values); + [obj setShadowColor:color]; + CGColorRelease(color); + }, + 0.01 + }, + + {kPOPLayerShadowOffset, + ^(CALayer *obj, CGFloat values[]) { + values_from_size(values, [obj shadowOffset]); + }, + ^(CALayer *obj, const CGFloat values[]) { + CGSize size = values_to_size(values); + [obj setShadowOffset:size]; + }, + 0.01 + }, + + {kPOPLayerShadowOpacity, + ^(CALayer *obj, CGFloat values[]) { + values[0] = [obj shadowOpacity]; + }, + ^(CALayer *obj, const CGFloat values[]) { + [obj setShadowOpacity:values[0]]; + }, + kPOPThresholdOpacity + }, + + {kPOPLayerShadowRadius, + ^(CALayer *obj, CGFloat values[]) { + values[0] = [obj shadowRadius]; + }, + ^(CALayer *obj, const CGFloat values[]) { + [obj setShadowRadius:values[0]]; + }, + kPOPThresholdRadius + }, + + /* CAShapeLayer */ + + {kPOPShapeLayerStrokeStart, + ^(CAShapeLayer *obj, CGFloat values[]) { + values[0] = obj.strokeStart; + }, + ^(CAShapeLayer *obj, const CGFloat values[]) { + obj.strokeStart = values[0]; + }, + 0.01 + }, + + {kPOPShapeLayerStrokeEnd, + ^(CAShapeLayer *obj, CGFloat values[]) { + values[0] = obj.strokeEnd; + }, + ^(CAShapeLayer *obj, const CGFloat values[]) { + obj.strokeEnd = values[0]; + }, + 0.01 + }, + + {kPOPShapeLayerStrokeColor, + ^(CAShapeLayer *obj, CGFloat values[]) { + POPCGColorGetRGBAComponents(obj.strokeColor, values); + }, + ^(CAShapeLayer *obj, const CGFloat values[]) { + CGColorRef color = POPCGColorRGBACreate(values); + [obj setStrokeColor:color]; + CGColorRelease(color); + }, + kPOPThresholdColor + }, + + {kPOPShapeLayerFillColor, + ^(CAShapeLayer *obj, CGFloat values[]) { + POPCGColorGetRGBAComponents(obj.fillColor, values); + }, + ^(CAShapeLayer *obj, const CGFloat values[]) { + CGColorRef color = POPCGColorRGBACreate(values); + [obj setFillColor:color]; + CGColorRelease(color); + }, + kPOPThresholdColor + }, + + {kPOPShapeLayerLineWidth, + ^(CAShapeLayer *obj, CGFloat values[]) { + values[0] = obj.lineWidth; + }, + ^(CAShapeLayer *obj, const CGFloat values[]) { + obj.lineWidth = values[0]; + }, + 0.01 + }, + + {kPOPLayoutConstraintConstant, + ^(NSLayoutConstraint *obj, CGFloat values[]) { + values[0] = obj.constant; + }, + ^(NSLayoutConstraint *obj, const CGFloat values[]) { + obj.constant = values[0]; + }, + 0.01 + }, +#if SCENEKIT_SDK_AVAILABLE + + /* SceneKit */ + + {kPOPSCNNodePosition, + ^(SCNNode *obj, CGFloat values[]) { + values_from_vec3(values, obj.position); + }, + ^(SCNNode *obj, const CGFloat values[]) { + obj.position = values_to_vec3(values); + }, + kPOPThresholdScale + }, + + {kPOPSCNNodePositionX, + ^(SCNNode *obj, CGFloat values[]) { + values[0] = obj.position.x; + }, + ^(SCNNode *obj, const CGFloat values[]) { + obj.position = SCNVector3Make(values[0], obj.position.y, obj.position.z); + }, + kPOPThresholdScale + }, + + {kPOPSCNNodePositionY, + ^(SCNNode *obj, CGFloat values[]) { + values[0] = obj.position.y; + }, + ^(SCNNode *obj, const CGFloat values[]) { + obj.position = SCNVector3Make(obj.position.x, values[0], obj.position.z); + }, + kPOPThresholdScale + }, + + {kPOPSCNNodePositionZ, + ^(SCNNode *obj, CGFloat values[]) { + values[0] = obj.position.z; + }, + ^(SCNNode *obj, const CGFloat values[]) { + obj.position = SCNVector3Make(obj.position.x, obj.position.y, values[0]); + }, + kPOPThresholdScale + }, + + {kPOPSCNNodeTranslation, + ^(SCNNode *obj, CGFloat values[]) { + values[0] = obj.transform.m41; + values[1] = obj.transform.m42; + values[2] = obj.transform.m43; + }, + ^(SCNNode *obj, const CGFloat values[]) { + obj.transform = SCNMatrix4MakeTranslation(values[0], values[1], values[2]); + }, + kPOPThresholdScale + }, + + {kPOPSCNNodeTranslationX, + ^(SCNNode *obj, CGFloat values[]) { + values[0] = obj.transform.m41; + }, + ^(SCNNode *obj, const CGFloat values[]) { + obj.transform = SCNMatrix4MakeTranslation(values[0], obj.transform.m42, obj.transform.m43); + }, + kPOPThresholdScale + }, + + {kPOPSCNNodeTranslationY, + ^(SCNNode *obj, CGFloat values[]) { + values[0] = obj.transform.m42; + }, + ^(SCNNode *obj, const CGFloat values[]) { + obj.transform = SCNMatrix4MakeTranslation(obj.transform.m41, values[0], obj.transform.m43); + }, + kPOPThresholdScale + }, + + {kPOPSCNNodeTranslationY, + ^(SCNNode *obj, CGFloat values[]) { + values[0] = obj.transform.m43; + }, + ^(SCNNode *obj, const CGFloat values[]) { + obj.transform = SCNMatrix4MakeTranslation(obj.transform.m41, obj.transform.m42, values[0]); + }, + kPOPThresholdScale + }, + + {kPOPSCNNodeRotation, + ^(SCNNode *obj, CGFloat values[]) { + values_from_vec4(values, obj.rotation); + }, + ^(SCNNode *obj, const CGFloat values[]) { + obj.rotation = values_to_vec4(values); + }, + kPOPThresholdScale + }, + + {kPOPSCNNodeRotationX, + ^(SCNNode *obj, CGFloat values[]) { + values[0] = obj.rotation.x; + }, + ^(SCNNode *obj, const CGFloat values[]) { + obj.rotation = SCNVector4Make(1.0, obj.rotation.y, obj.rotation.z, values[0]); + }, + kPOPThresholdScale + }, + + {kPOPSCNNodeRotationY, + ^(SCNNode *obj, CGFloat values[]) { + values[0] = obj.rotation.y; + }, + ^(SCNNode *obj, const CGFloat values[]) { + obj.rotation = SCNVector4Make(obj.rotation.x, 1.0, obj.rotation.z, values[0]); + }, + kPOPThresholdScale + }, + + {kPOPSCNNodeRotationZ, + ^(SCNNode *obj, CGFloat values[]) { + values[0] = obj.rotation.z; + }, + ^(SCNNode *obj, const CGFloat values[]) { + obj.rotation = SCNVector4Make(obj.rotation.x, obj.rotation.y, 1.0, values[0]); + }, + kPOPThresholdScale + }, + + {kPOPSCNNodeRotationW, + ^(SCNNode *obj, CGFloat values[]) { + values[0] = obj.rotation.w; + }, + ^(SCNNode *obj, const CGFloat values[]) { + obj.rotation = SCNVector4Make(obj.rotation.x, obj.rotation.y, obj.rotation.z, values[0]); + }, + kPOPThresholdScale + }, + + {kPOPSCNNodeEulerAngles, + ^(SCNNode *obj, CGFloat values[]) { + values_from_vec3(values, obj.eulerAngles); + }, + ^(SCNNode *obj, const CGFloat values[]) { + obj.eulerAngles = values_to_vec3(values); + }, + kPOPThresholdScale + }, + + {kPOPSCNNodeEulerAnglesX, + ^(SCNNode *obj, CGFloat values[]) { + values[0] = obj.eulerAngles.x; + }, + ^(SCNNode *obj, const CGFloat values[]) { + obj.eulerAngles = SCNVector3Make(values[0], obj.eulerAngles.y, obj.eulerAngles.z); + }, + kPOPThresholdScale + }, + + {kPOPSCNNodeEulerAnglesY, + ^(SCNNode *obj, CGFloat values[]) { + values[0] = obj.eulerAngles.y; + }, + ^(SCNNode *obj, const CGFloat values[]) { + obj.eulerAngles = SCNVector3Make(obj.eulerAngles.x, values[0], obj.eulerAngles.z); + }, + kPOPThresholdScale + }, + + {kPOPSCNNodeEulerAnglesZ, + ^(SCNNode *obj, CGFloat values[]) { + values[0] = obj.eulerAngles.z; + }, + ^(SCNNode *obj, const CGFloat values[]) { + obj.eulerAngles = SCNVector3Make(obj.eulerAngles.x, obj.eulerAngles.y, values[0]); + }, + kPOPThresholdScale + }, + + {kPOPSCNNodeOrientation, + ^(SCNNode *obj, CGFloat values[]) { + values_from_vec4(values, obj.orientation); + }, + ^(SCNNode *obj, const CGFloat values[]) { + obj.orientation = values_to_vec4(values); + }, + kPOPThresholdScale + }, + + {kPOPSCNNodeOrientationX, + ^(SCNNode *obj, CGFloat values[]) { + values[0] = obj.orientation.x; + }, + ^(SCNNode *obj, const CGFloat values[]) { + obj.orientation = SCNVector4Make(values[0], obj.orientation.y, obj.orientation.z, obj.orientation.w); + }, + kPOPThresholdScale + }, + + {kPOPSCNNodeOrientationY, + ^(SCNNode *obj, CGFloat values[]) { + values[0] = obj.orientation.y; + }, + ^(SCNNode *obj, const CGFloat values[]) { + obj.orientation = SCNVector4Make(obj.orientation.x, values[0], obj.orientation.z, obj.orientation.w); + }, + kPOPThresholdScale + }, + + {kPOPSCNNodeOrientationZ, + ^(SCNNode *obj, CGFloat values[]) { + values[0] = obj.orientation.z; + }, + ^(SCNNode *obj, const CGFloat values[]) { + obj.orientation = SCNVector4Make(obj.orientation.x, obj.orientation.y, values[0], obj.orientation.w); + }, + kPOPThresholdScale + }, + + {kPOPSCNNodeOrientationW, + ^(SCNNode *obj, CGFloat values[]) { + values[0] = obj.orientation.w; + }, + ^(SCNNode *obj, const CGFloat values[]) { + obj.orientation = SCNVector4Make(obj.orientation.x, obj.orientation.y, obj.orientation.z, values[0]); + }, + kPOPThresholdScale + }, + + {kPOPSCNNodeScale, + ^(SCNNode *obj, CGFloat values[]) { + values_from_vec3(values, obj.scale); + }, + ^(SCNNode *obj, const CGFloat values[]) { + obj.scale = values_to_vec3(values); + }, + kPOPThresholdScale + }, + + {kPOPSCNNodeScaleX, + ^(SCNNode *obj, CGFloat values[]) { + values[0] = obj.scale.x; + }, + ^(SCNNode *obj, const CGFloat values[]) { + obj.scale = SCNVector3Make(values[0], obj.scale.y, obj.scale.z); + }, + kPOPThresholdScale + }, + + {kPOPSCNNodeScaleY, + ^(SCNNode *obj, CGFloat values[]) { + values[0] = obj.scale.y; + }, + ^(SCNNode *obj, const CGFloat values[]) { + obj.position = SCNVector3Make(obj.scale.x, values[0], obj.scale.z); + }, + kPOPThresholdScale + }, + + {kPOPSCNNodeScaleZ, + ^(SCNNode *obj, CGFloat values[]) { + values[0] = obj.scale.z; + }, + ^(SCNNode *obj, const CGFloat values[]) { + obj.scale = SCNVector3Make(obj.scale.x, obj.scale.y, values[0]); + }, + kPOPThresholdScale + }, + + {kPOPSCNNodeScaleXY, + ^(SCNNode *obj, CGFloat values[]) { + values[0] = obj.scale.x; + values[1] = obj.scale.y; + }, + ^(SCNNode *obj, const CGFloat values[]) { + obj.scale = SCNVector3Make(values[0], values[1], obj.scale.z); + }, + kPOPThresholdScale + }, + +#endif + +#if TARGET_OS_IPHONE + + /* UIView */ + + {kPOPViewAlpha, + ^(UIView *obj, CGFloat values[]) { + values[0] = obj.alpha; + }, + ^(UIView *obj, const CGFloat values[]) { + obj.alpha = values[0]; + }, + kPOPThresholdOpacity + }, + + {kPOPViewBackgroundColor, + ^(UIView *obj, CGFloat values[]) { + POPUIColorGetRGBAComponents(obj.backgroundColor, values); + }, + ^(UIView *obj, const CGFloat values[]) { + obj.backgroundColor = POPUIColorRGBACreate(values); + }, + kPOPThresholdColor + }, + + {kPOPViewCenter, + ^(UIView *obj, CGFloat values[]) { + values_from_point(values, obj.center); + }, + ^(UIView *obj, const CGFloat values[]) { + obj.center = values_to_point(values); + }, + kPOPThresholdPoint + }, + + {kPOPViewFrame, + ^(UIView *obj, CGFloat values[]) { + values_from_rect(values, obj.frame); + }, + ^(UIView *obj, const CGFloat values[]) { + obj.frame = values_to_rect(values); + }, + kPOPThresholdPoint + }, + + {kPOPViewScaleX, + ^(UIView *obj, CGFloat values[]) { + values[0] = POPLayerGetScaleX(obj.layer); + }, + ^(UIView *obj, const CGFloat values[]) { + POPLayerSetScaleX(obj.layer, values[0]); + }, + kPOPThresholdScale + }, + + {kPOPViewScaleY, + ^(UIView *obj, CGFloat values[]) { + values[0] = POPLayerGetScaleY(obj.layer); + }, + ^(UIView *obj, const CGFloat values[]) { + POPLayerSetScaleY(obj.layer, values[0]); + }, + kPOPThresholdScale + }, + + {kPOPViewScaleXY, + ^(UIView *obj, CGFloat values[]) { + values_from_point(values, POPLayerGetScaleXY(obj.layer)); + }, + ^(UIView *obj, const CGFloat values[]) { + POPLayerSetScaleXY(obj.layer, values_to_point(values)); + }, + kPOPThresholdScale + }, + + {kPOPViewTintColor, + ^(UIView *obj, CGFloat values[]) { + POPUIColorGetRGBAComponents(obj.tintColor, values); + }, + ^(UIView *obj, const CGFloat values[]) { + obj.tintColor = POPUIColorRGBACreate(values); + }, + kPOPThresholdColor + }, + + /* UIScrollView */ + + {kPOPScrollViewContentOffset, + ^(UIScrollView *obj, CGFloat values[]) { + values_from_point(values, obj.contentOffset); + }, + ^(UIScrollView *obj, const CGFloat values[]) { + [obj setContentOffset:values_to_point(values) animated:NO]; + }, + kPOPThresholdPoint + }, + + {kPOPScrollViewContentSize, + ^(UIScrollView *obj, CGFloat values[]) { + values_from_size(values, obj.contentSize); + }, + ^(UIScrollView *obj, const CGFloat values[]) { + obj.contentSize = values_to_size(values); + }, + kPOPThresholdPoint + }, + + {kPOPScrollViewZoomScale, + ^(UIScrollView *obj, CGFloat values[]) { + values[0]=obj.zoomScale; + }, + ^(UIScrollView *obj, const CGFloat values[]) { + obj.zoomScale=values[0]; + }, + kPOPThresholdScale + }, + + {kPOPScrollViewContentInset, + ^(UIScrollView *obj, CGFloat values[]) { + values[0] = obj.contentInset.top; + values[1] = obj.contentInset.left; + values[2] = obj.contentInset.bottom; + values[3] = obj.contentInset.right; + }, + ^(UIScrollView *obj, const CGFloat values[]) { + obj.contentInset = values_to_edge_insets(values); + }, + kPOPThresholdPoint + }, + + /* UINavigationBar */ + + {kPOPNavigationBarBarTintColor, + ^(UINavigationBar *obj, CGFloat values[]) { + POPUIColorGetRGBAComponents(obj.barTintColor, values); + }, + ^(UINavigationBar *obj, const CGFloat values[]) { + obj.barTintColor = POPUIColorRGBACreate(values); + }, + kPOPThresholdColor + }, + + /* UILabel */ + + {kPOPLabelTextColor, + ^(UILabel *obj, CGFloat values[]) { + POPUIColorGetRGBAComponents(obj.textColor, values); + }, + ^(UILabel *obj, const CGFloat values[]) { + obj.textColor = POPUIColorRGBACreate(values); + }, + kPOPThresholdColor + }, + +#endif + +}; + +static NSUInteger staticIndexWithName(NSString *aName) +{ + NSUInteger idx = 0; + + while (idx < POP_ARRAY_COUNT(_staticStates)) { + if ([_staticStates[idx].name isEqualToString:aName]) + return idx; + idx++; + } + + return NSNotFound; +} + +/** + Concrete static property class. + */ +@interface POPStaticAnimatableProperty : POPAnimatableProperty +{ +@public + POPStaticAnimatablePropertyState *_state; +} +@end + +@implementation POPStaticAnimatableProperty + +- (NSString *)name +{ + return _state->name; +} + +- (pop_animatable_read_block)readBlock +{ + return _state->readBlock; +} + +- (pop_animatable_write_block)writeBlock +{ + return _state->writeBlock; +} + +- (CGFloat)threshold +{ + return _state->threshold; +} + +@end + +#pragma mark - Concrete + +/** + Concrete immutable property class. + */ +@interface POPConcreteAnimatableProperty : POPAnimatableProperty +- (instancetype)initWithName:(NSString *)name readBlock:(pop_animatable_read_block)read writeBlock:(pop_animatable_write_block)write threshold:(CGFloat)threshold; +@end + +@implementation POPConcreteAnimatableProperty + +// default synthesis +@synthesize name, readBlock, writeBlock, threshold; + +- (instancetype)initWithName:(NSString *)aName readBlock:(pop_animatable_read_block)aReadBlock writeBlock:(pop_animatable_write_block)aWriteBlock threshold:(CGFloat)aThreshold +{ + self = [super init]; + if (nil != self) { + name = [aName copy]; + readBlock = [aReadBlock copy]; + writeBlock = [aWriteBlock copy]; + threshold = aThreshold; + } + return self; +} +@end + +#pragma mark - Mutable + +@implementation POPMutableAnimatableProperty + +// default synthesis +@synthesize name, readBlock, writeBlock, threshold; + +@end + +#pragma mark - Cluster + +/** + Singleton placeholder property class to support class cluster. + */ +@interface POPPlaceholderAnimatableProperty : POPAnimatableProperty + +@end + +@implementation POPPlaceholderAnimatableProperty + +// default synthesis +@synthesize name, readBlock, writeBlock, threshold; + +@end + +/** + Cluster class. + */ +@implementation POPAnimatableProperty + +// avoid creating backing ivars +@dynamic name, readBlock, writeBlock, threshold; + +static POPAnimatableProperty *placeholder = nil; + ++ (void)initialize +{ + if (self == [POPAnimatableProperty class]) { + placeholder = [POPPlaceholderAnimatableProperty alloc]; + } +} + ++ (id)allocWithZone:(struct _NSZone *)zone +{ + if (self == [POPAnimatableProperty class]) { + if (nil == placeholder) { + placeholder = [super allocWithZone:zone]; + } + return placeholder; + } + return [super allocWithZone:zone]; +} + +- (id)copyWithZone:(NSZone *)zone +{ + if ([self isKindOfClass:[POPMutableAnimatableProperty class]]) { + POPConcreteAnimatableProperty *copyProperty = [[POPConcreteAnimatableProperty alloc] initWithName:self.name readBlock:self.readBlock writeBlock:self.writeBlock threshold:self.threshold]; + return copyProperty; + } else { + return self; + } +} + +- (id)mutableCopyWithZone:(NSZone *)zone +{ + POPMutableAnimatableProperty *copyProperty = [[POPMutableAnimatableProperty alloc] init]; + copyProperty.name = self.name; + copyProperty.readBlock = self.readBlock; + copyProperty.writeBlock = self.writeBlock; + copyProperty.threshold = self.threshold; + return copyProperty; +} + ++ (id)propertyWithName:(NSString *)aName +{ + return [self propertyWithName:aName initializer:NULL]; +} + ++ (id)propertyWithName:(NSString *)aName initializer:(void (^)(POPMutableAnimatableProperty *prop))aBlock +{ + POPAnimatableProperty *prop = nil; + + static NSMutableDictionary *_propertyDict = nil; + if (nil == _propertyDict) { + _propertyDict = [[NSMutableDictionary alloc] initWithCapacity:10]; + } + + prop = _propertyDict[aName]; + if (nil != prop) { + return prop; + } + + NSUInteger staticIdx = staticIndexWithName(aName); + + if (NSNotFound != staticIdx) { + POPStaticAnimatableProperty *staticProp = [[POPStaticAnimatableProperty alloc] init]; + staticProp->_state = &_staticStates[staticIdx]; + _propertyDict[aName] = staticProp; + prop = staticProp; + } else if (NULL != aBlock) { + POPMutableAnimatableProperty *mutableProp = [[POPMutableAnimatableProperty alloc] init]; + mutableProp.name = aName; + mutableProp.threshold = 1.0; + aBlock(mutableProp); + prop = [mutableProp copy]; + } + + return prop; +} + +- (NSString *)description +{ + NSMutableString *s = [NSMutableString stringWithFormat:@"%@ name:%@ threshold:%f", super.description, self.name, self.threshold]; + return s; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/POPAnimation.h b/submodules/LegacyComponents/LegacyComponents/POPAnimation.h new file mode 100644 index 0000000000..2e75c09a8c --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/POPAnimation.h @@ -0,0 +1,177 @@ +/** + Copyright (c) 2014-present, Facebook, Inc. + All rights reserved. + + This source code is licensed under the BSD-style license found in the + LICENSE file in the root directory of this source tree. An additional grant + of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import +#import + +@class CAMediaTimingFunction; + +/** + @abstract The abstract animation base class. + @discussion Instantiate and use one of the concrete animation subclasses. + */ +@interface POPAnimation : NSObject + +/** + @abstract The name of the animation. + @discussion Optional property to help identify the animation. + */ +@property (copy, nonatomic) NSString *name; + +/** + @abstract The beginTime of the animation in media time. + @discussion Defaults to 0 and starts immediately. + */ +@property (assign, nonatomic) CFTimeInterval beginTime; + +/** + @abstract The animation delegate. + @discussion See {@ref POPAnimationDelegate} for details. + */ +@property (weak, nonatomic) id delegate; + +/** + @abstract The animation tracer. + @discussion Returns the existing tracer, creating one if needed. Call start/stop on the tracer to toggle event collection. + */ +@property (readonly, nonatomic) POPAnimationTracer *tracer; + +/** + @abstract Optional block called on animation start. + */ +@property (copy, nonatomic) void (^animationDidStartBlock)(POPAnimation *anim); + +/** + @abstract Optional block called when value meets or exceeds to value. + */ +@property (copy, nonatomic) void (^animationDidReachToValueBlock)(POPAnimation *anim); + +/** + @abstract Optional block called on animation completion. + */ +@property (copy, nonatomic) void (^completionBlock)(POPAnimation *anim, BOOL finished); + +/** + @abstract Optional block called each frame animation is applied. + */ +@property (copy, nonatomic) void (^animationDidApplyBlock)(POPAnimation *anim); + +/** + @abstract Flag indicating whether animation should be removed on completion. + @discussion Setting to NO can facilitate animation reuse. Defaults to YES. + */ +@property (assign, nonatomic) BOOL removedOnCompletion; + +/** + @abstract Flag indicating whether animation is paused. + @discussion A paused animation is excluded from the list of active animations. On initial creation, defaults to YES. On animation addition, the animation is implicity unpaused. On animation completion, the animation is implicity paused including for animations with removedOnCompletion set to NO. + */ +@property (assign, nonatomic, getter = isPaused) BOOL paused; + +/** + @abstract Flag indicating whether animation autoreverses. + @discussion An animation that autoreverses will have twice the duration before it is considered finished. It will animate to the toValue, stop, then animate back to the original fromValue. The delegate methods are called as follows: + + 1) animationDidStart: is called at the beginning, as usual, and then after each toValue is reached and the autoreverse is going to start. + 2) animationDidReachToValue: is called every time the toValue is reached. The toValue is swapped with the fromValue at the end of each animation segment. This means that with autoreverses set to YES, the animationDidReachToValue: delegate method will be called a minimum of twice. + 3) animationDidStop:finished: is called every time the toValue is reached, the finished argument will be NO if the autoreverse is not yet complete. + */ +@property (assign, nonatomic) BOOL autoreverses; + +/** + @abstract The number of times to repeat the animation. + @discussion A repeatCount of 0 or 1 means that the animation will not repeat, just like Core Animation. A repeatCount of 2 or greater means that the animation will run that many times before stopping. The delegate methods are called as follows: + + 1) animationDidStart: is called at the beginning of each animation repeat. + 2) animationDidReachToValue: is called every time the toValue is reached. + 3) animationDidStop:finished: is called every time the toValue is reached, the finished argument will be NO if the autoreverse is not yet complete. + +When combined with the autoreverses property, a singular animation is effectively twice as long. + */ +@property (assign, nonatomic) NSInteger repeatCount; + +/** + @abstract Repeat the animation forever. + @discussion This property will make the animation repeat forever. The value of the repeatCount property is undefined when this property is set. The finished parameter of the delegate callback animationDidStop:finished: will always be NO. + */ +@property (assign, nonatomic) BOOL repeatForever; + +@end + +/** + @abstract The animation delegate. + */ +@protocol POPAnimationDelegate +@optional + +/** + @abstract Called on animation start. + @param anim The relevant animation. + */ +- (void)pop_animationDidStart:(POPAnimation *)anim; + +/** + @abstract Called when value meets or exceeds to value. + @param anim The relevant animation. + */ +- (void)pop_animationDidReachToValue:(POPAnimation *)anim; + +/** + @abstract Called on animation stop. + @param anim The relevant animation. + @param finished Flag indicating finished state. Flag is true if the animation reached completion before being removed. + */ +- (void)pop_animationDidStop:(POPAnimation *)anim finished:(BOOL)finished; + +/** + @abstract Called each frame animation is applied. + @param anim The relevant animation. + */ +- (void)pop_animationDidApply:(POPAnimation *)anim; + +@end + + +@interface NSObject (POP) + +/** + @abstract Add an animation to the reciver. + @param anim The animation to add. + @param key The key used to identify the animation. + @discussion The 'key' may be any string such that only one animation per unique key is added per object. + */ +- (void)pop_addAnimation:(POPAnimation *)anim forKey:(NSString *)key; + +/** + @abstract Remove all animations attached to the receiver. + */ +- (void)pop_removeAllAnimations; + +/** + @abstract Remove any animation attached to the receiver for 'key'. + @param key The key used to identify the animation. + */ +- (void)pop_removeAnimationForKey:(NSString *)key; + +/** + @abstract Returns an array containing the keys of all animations currently attached to the receiver. + @param The order of keys reflects the order in which animations will be applied. + */ +- (NSArray *)pop_animationKeys; + +/** + @abstract Returns any animation attached to the receiver. + @param key The key used to identify the animation. + @returns The animation currently attached, or nil if no such animation exists. + */ +- (id)pop_animationForKey:(NSString *)key; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/POPAnimation.mm b/submodules/LegacyComponents/LegacyComponents/POPAnimation.mm new file mode 100644 index 0000000000..fddc406ddd --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/POPAnimation.mm @@ -0,0 +1,272 @@ +/** + Copyright (c) 2014-present, Facebook, Inc. + All rights reserved. + + This source code is licensed under the BSD-style license found in the + LICENSE file in the root directory of this source tree. An additional grant + of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "POPAnimationInternal.h" +#import "POPAnimationTracerInternal.h" + +#import + +#import "POPAnimationExtras.h" +#import "POPAnimationRuntime.h" +#import "POPAnimatorPrivate.h" +#import "POPAction.h" + +using namespace POP; + +#pragma mark - POPAnimation + +@implementation POPAnimation +@synthesize solver = _solver; +@synthesize currentValue = _currentValue; +@synthesize progressMarkers = _progressMarkers; + +#pragma mark - Lifecycle + +- (id)init +{ + [NSException raise:NSStringFromClass([self class]) format:@"Attempting to instantiate an abstract class. Use a concrete subclass instead."]; + return nil; +} + +- (id)_init +{ + self = [super init]; + if (nil != self) { + [self _initState]; + } + return self; +} + +- (void)_initState +{ + _state = new POPAnimationState(self); +} + +- (void)dealloc +{ + if (_state) { + delete _state; + _state = NULL; + }; +} + +#pragma mark - Properties + +- (id)delegate +{ + return _state->delegate; +} + +- (void)setDelegate:(id)delegate +{ + _state->setDelegate(delegate); +} + +- (BOOL)isPaused +{ + return _state->paused; +} + +- (void)setPaused:(BOOL)paused +{ + _state->setPaused(paused ? true : false); +} + +- (NSInteger)repeatCount +{ + if (_state->autoreverses) { + return _state->repeatCount / 2; + } else { + return _state->repeatCount; + } +} + +- (void)setRepeatCount:(NSInteger)repeatCount +{ + if (repeatCount > 0) { + if (repeatCount > NSIntegerMax / 2) { + repeatCount = NSIntegerMax / 2; + } + + if (_state->autoreverses) { + _state->repeatCount = (repeatCount * 2); + } else { + _state->repeatCount = repeatCount; + } + } +} + +- (BOOL)autoreverses +{ + return _state->autoreverses; +} + +- (void)setAutoreverses:(BOOL)autoreverses +{ + _state->autoreverses = autoreverses; + if (autoreverses) { + if (_state->repeatCount == 0) { + [self setRepeatCount:1]; + } + } +} + +FB_PROPERTY_GET(POPAnimationState, type, POPAnimationType); +DEFINE_RW_PROPERTY_OBJ_COPY(POPAnimationState, animationDidStartBlock, setAnimationDidStartBlock:, POPAnimationDidStartBlock); +DEFINE_RW_PROPERTY_OBJ_COPY(POPAnimationState, animationDidReachToValueBlock, setAnimationDidReachToValueBlock:, POPAnimationDidReachToValueBlock); +DEFINE_RW_PROPERTY_OBJ_COPY(POPAnimationState, completionBlock, setCompletionBlock:, POPAnimationCompletionBlock); +DEFINE_RW_PROPERTY_OBJ_COPY(POPAnimationState, animationDidApplyBlock, setAnimationDidApplyBlock:, POPAnimationDidApplyBlock); +DEFINE_RW_PROPERTY_OBJ_COPY(POPAnimationState, name, setName:, NSString*); +DEFINE_RW_PROPERTY(POPAnimationState, beginTime, setBeginTime:, CFTimeInterval); +DEFINE_RW_FLAG(POPAnimationState, removedOnCompletion, removedOnCompletion, setRemovedOnCompletion:); +DEFINE_RW_FLAG(POPAnimationState, repeatForever, repeatForever, setRepeatForever:); + +- (id)valueForUndefinedKey:(NSString *)key +{ + return _state->dict[key]; +} + +- (void)setValue:(id)value forUndefinedKey:(NSString *)key +{ + if (!value) { + [_state->dict removeObjectForKey:key]; + } else { + if (!_state->dict) + _state->dict = [[NSMutableDictionary alloc] init]; + _state->dict[key] = value; + } +} + +- (POPAnimationTracer *)tracer +{ + if (!_state->tracer) { + _state->tracer = [[POPAnimationTracer alloc] initWithAnimation:self]; + } + return _state->tracer; +} + +- (NSString *)description +{ + NSMutableString *s = [NSMutableString stringWithFormat:@"<%@:%p", NSStringFromClass([self class]), self]; + [self _appendDescription:s debug:NO]; + [s appendString:@">"]; + return s; +} + +- (NSString *)debugDescription +{ + NSMutableString *s = [NSMutableString stringWithFormat:@"<%@:%p", NSStringFromClass([self class]), self]; + [self _appendDescription:s debug:YES]; + [s appendString:@">"]; + return s; +} + +#pragma mark - Utility + +POPAnimationState *POPAnimationGetState(POPAnimation *a) +{ + return a->_state; +} + +- (BOOL)_advance:(id)object currentTime:(CFTimeInterval)currentTime elapsedTime:(CFTimeInterval)elapsedTime +{ + return YES; +} + +- (void)_appendDescription:(NSMutableString *)s debug:(BOOL)debug +{ + if (_state->name) + [s appendFormat:@"; name = %@", _state->name]; + + if (!self.removedOnCompletion) + [s appendFormat:@"; removedOnCompletion = %@", POPStringFromBOOL(self.removedOnCompletion)]; + + if (debug) { + if (_state->active) + [s appendFormat:@"; active = %@", POPStringFromBOOL(_state->active)]; + + if (_state->paused) + [s appendFormat:@"; paused = %@", POPStringFromBOOL(_state->paused)]; + } + + if (_state->beginTime) { + [s appendFormat:@"; beginTime = %f", _state->beginTime]; + } + + for (NSString *key in _state->dict) { + [s appendFormat:@"; %@ = %@", key, _state->dict[key]]; + } +} + +@end + + +#pragma mark - POPPropertyAnimation + +#pragma mark - POPBasicAnimation + +#pragma mark - POPDecayAnimation + +@implementation NSObject (POP) + +- (void)pop_addAnimation:(POPAnimation *)anim forKey:(NSString *)key +{ + [[POPAnimator sharedAnimator] addAnimation:anim forObject:self key:key]; +} + +- (void)pop_removeAllAnimations +{ + [[POPAnimator sharedAnimator] removeAllAnimationsForObject:self]; +} + +- (void)pop_removeAnimationForKey:(NSString *)key +{ + [[POPAnimator sharedAnimator] removeAnimationForObject:self key:key]; +} + +- (NSArray *)pop_animationKeys +{ + return [[POPAnimator sharedAnimator] animationKeysForObject:self]; +} + +- (id)pop_animationForKey:(NSString *)key +{ + return [[POPAnimator sharedAnimator] animationForObject:self key:key]; +} + +@end + +@implementation NSProxy (POP) + +- (void)pop_addAnimation:(POPAnimation *)anim forKey:(NSString *)key +{ + [[POPAnimator sharedAnimator] addAnimation:anim forObject:self key:key]; +} + +- (void)pop_removeAllAnimations +{ + [[POPAnimator sharedAnimator] removeAllAnimationsForObject:self]; +} + +- (void)pop_removeAnimationForKey:(NSString *)key +{ + [[POPAnimator sharedAnimator] removeAnimationForObject:self key:key]; +} + +- (NSArray *)pop_animationKeys +{ + return [[POPAnimator sharedAnimator] animationKeysForObject:self]; +} + +- (id)pop_animationForKey:(NSString *)key +{ + return [[POPAnimator sharedAnimator] animationForObject:self key:key]; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/POPAnimationEvent.h b/submodules/LegacyComponents/LegacyComponents/POPAnimationEvent.h new file mode 100644 index 0000000000..e761091f4c --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/POPAnimationEvent.h @@ -0,0 +1,69 @@ +/** + Copyright (c) 2014-present, Facebook, Inc. + All rights reserved. + + This source code is licensed under the BSD-style license found in the + LICENSE file in the root directory of this source tree. An additional grant + of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +/** + @abstract Enumeraton of animation event types. + */ +typedef NS_ENUM(NSUInteger, POPAnimationEventType) { + kPOPAnimationEventPropertyRead = 0, + kPOPAnimationEventPropertyWrite, + kPOPAnimationEventToValueUpdate, + kPOPAnimationEventFromValueUpdate, + kPOPAnimationEventVelocityUpdate, + kPOPAnimationEventBouncinessUpdate, + kPOPAnimationEventSpeedUpdate, + kPOPAnimationEventFrictionUpdate, + kPOPAnimationEventMassUpdate, + kPOPAnimationEventTensionUpdate, + kPOPAnimationEventDidStart, + kPOPAnimationEventDidStop, + kPOPAnimationEventDidReachToValue, + kPOPAnimationEventAutoreversed +}; + +/** + @abstract The base animation event class. + */ +@interface POPAnimationEvent : NSObject + +/** + @abstract The event type. See {@ref POPAnimationEventType} for possible values. + */ +@property (readonly, nonatomic, assign) POPAnimationEventType type; + +/** + @abstract The time of event. + */ +@property (readonly, nonatomic, assign) CFTimeInterval time; + +/** + @abstract Optional string describing the animation at time of event. + */ +@property (readonly, nonatomic, copy) NSString *animationDescription; + +@end + +/** + @abstract An animation event subclass for recording value and velocity. + */ +@interface POPAnimationValueEvent : POPAnimationEvent + +/** + @abstract The value recorded. + */ +@property (readonly, nonatomic, strong) id value; + +/** + @abstract The velocity recorded, if any. + */ +@property (readonly, nonatomic, strong) id velocity; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/POPAnimationEvent.mm b/submodules/LegacyComponents/LegacyComponents/POPAnimationEvent.mm new file mode 100644 index 0000000000..d3a13b69c6 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/POPAnimationEvent.mm @@ -0,0 +1,108 @@ +/** + Copyright (c) 2014-present, Facebook, Inc. + All rights reserved. + + This source code is licensed under the BSD-style license found in the + LICENSE file in the root directory of this source tree. An additional grant + of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "POPAnimationEvent.h" +#import "POPAnimationEventInternal.h" + +static NSString *stringFromType(POPAnimationEventType aType) +{ + switch (aType) { + case kPOPAnimationEventPropertyRead: + return @"read"; + case kPOPAnimationEventPropertyWrite: + return @"write"; + case kPOPAnimationEventToValueUpdate: + return @"toValue"; + case kPOPAnimationEventFromValueUpdate: + return @"fromValue"; + case kPOPAnimationEventVelocityUpdate: + return @"velocity"; + case kPOPAnimationEventSpeedUpdate: + return @"speed"; + case kPOPAnimationEventBouncinessUpdate: + return @"bounciness"; + case kPOPAnimationEventFrictionUpdate: + return @"friction"; + case kPOPAnimationEventMassUpdate: + return @"mass"; + case kPOPAnimationEventTensionUpdate: + return @"tension"; + case kPOPAnimationEventDidStart: + return @"didStart"; + case kPOPAnimationEventDidStop: + return @"didStop"; + case kPOPAnimationEventDidReachToValue: + return @"didReachToValue"; + case kPOPAnimationEventAutoreversed: + return @"autoreversed"; + default: + return nil; + } +} + +@implementation POPAnimationEvent +@synthesize type = _type; +@synthesize time = _time; +@synthesize animationDescription = _animationDescription; + +- (instancetype)initWithType:(POPAnimationEventType)aType time:(CFTimeInterval)aTime +{ + self = [super init]; + if (nil != self) { + _type = aType; + _time = aTime; + } + return self; +} + +- (NSString *)description +{ + NSMutableString *s = [NSMutableString stringWithFormat:@""]; + return s; +} + +// subclass override +- (void)_appendDescription:(NSMutableString *)s +{ + if (0 != _animationDescription.length) { + [s appendFormat:@"; animation = %@", _animationDescription]; + } +} + +@end + +@implementation POPAnimationValueEvent +@synthesize value = _value; +@synthesize velocity = _velocity; + +- (instancetype)initWithType:(POPAnimationEventType)aType time:(CFTimeInterval)aTime value:(id)aValue +{ + self = [self initWithType:aType time:aTime]; + if (nil != self) { + _value = aValue; + } + return self; +} + +- (void)_appendDescription:(NSMutableString *)s +{ + [super _appendDescription:s]; + + if (nil != _value) { + [s appendFormat:@"; value = %@", _value]; + } + + if (nil != _velocity) { + [s appendFormat:@"; velocity = %@", _velocity]; + } +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/POPAnimationEventInternal.h b/submodules/LegacyComponents/LegacyComponents/POPAnimationEventInternal.h new file mode 100644 index 0000000000..398d59bd1b --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/POPAnimationEventInternal.h @@ -0,0 +1,41 @@ +/** + Copyright (c) 2014-present, Facebook, Inc. + All rights reserved. + + This source code is licensed under the BSD-style license found in the + LICENSE file in the root directory of this source tree. An additional grant + of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "POPAnimationEvent.h" + +@interface POPAnimationEvent () + +/** + @abstract Default initializer. + */ +- (instancetype)initWithType:(POPAnimationEventType)type time:(CFTimeInterval)time; + +/** + @abstract Readwrite redefinition of public property. + */ +@property (readwrite, nonatomic, copy) NSString *animationDescription; + +@end + +@interface POPAnimationValueEvent () + +/** + @abstract Default initializer. + */ +- (instancetype)initWithType:(POPAnimationEventType)type time:(CFTimeInterval)time value:(id)value; + +/** + @abstract Readwrite redefinition of public property. + */ +@property (readwrite, nonatomic, strong) id velocity; + +@end + diff --git a/submodules/LegacyComponents/LegacyComponents/POPAnimationExtras.h b/submodules/LegacyComponents/LegacyComponents/POPAnimationExtras.h new file mode 100644 index 0000000000..8535909f53 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/POPAnimationExtras.h @@ -0,0 +1,43 @@ +/** + Copyright (c) 2014-present, Facebook, Inc. + All rights reserved. + + This source code is licensed under the BSD-style license found in the + LICENSE file in the root directory of this source tree. An additional grant + of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "POPDefines.h" +#import "POPSpringAnimation.h" + +/** + @abstract The current drag coefficient. + @discussion A value greater than 1.0 indicates Simulator slow-motion animations are enabled. Defaults to 1.0. + */ +extern CGFloat POPAnimationDragCoefficient(); + +@interface CAAnimation (POPAnimationExtras) + +/** + @abstract Apply the current drag coefficient to animation speed. + @discussion Convenience utility to respect Simulator slow-motion animation settings. + */ +- (void)pop_applyDragCoefficient; + +@end + +@interface POPSpringAnimation (POPAnimationExtras) + +/** + @abstract Converts from spring bounciness and speed to tension, friction and mass dynamics values. + */ ++ (void)convertBounciness:(CGFloat)bounciness speed:(CGFloat)speed toTension:(CGFloat *)outTension friction:(CGFloat *)outFriction mass:(CGFloat *)outMass; + +/** + @abstract Converts from dynamics tension, friction and mass to spring bounciness and speed values. + */ ++ (void)convertTension:(CGFloat)tension friction:(CGFloat)friction toBounciness:(CGFloat *)outBounciness speed:(CGFloat *)outSpeed; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/POPAnimationExtras.mm b/submodules/LegacyComponents/LegacyComponents/POPAnimationExtras.mm new file mode 100644 index 0000000000..941aaaf445 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/POPAnimationExtras.mm @@ -0,0 +1,117 @@ +/** + Copyright (c) 2014-present, Facebook, Inc. + All rights reserved. + + This source code is licensed under the BSD-style license found in the + LICENSE file in the root directory of this source tree. An additional grant + of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "POPAnimationExtras.h" + +#import "POPAnimationPrivate.h" +#import "POPMath.h" + +#if TARGET_OS_IPHONE +#import +#endif + +#if TARGET_IPHONE_SIMULATOR +UIKIT_EXTERN CGFloat UIAnimationDragCoefficient(); // UIKit private drag coeffient, use judiciously +#endif + +CGFloat POPAnimationDragCoefficient() +{ +#if TARGET_IPHONE_SIMULATOR + return UIAnimationDragCoefficient(); +#else + return 1.0; +#endif +} + +@implementation CAAnimation (POPAnimationExtras) + +- (void)pop_applyDragCoefficient +{ + CGFloat k = POPAnimationDragCoefficient(); + if (k != 0 && k != 1) + self.speed = 1 / k; +} + +@end + +@implementation POPSpringAnimation (POPAnimationExtras) + +static const CGFloat POPBouncy3NormalizationRange = 20.0; +static const CGFloat POPBouncy3NormalizationScale = 1.7; +static const CGFloat POPBouncy3BouncinessNormalizedMin = 0.0; +static const CGFloat POPBouncy3BouncinessNormalizedMax = 0.8; +static const CGFloat POPBouncy3SpeedNormalizedMin = 0.5; +static const CGFloat POPBouncy3SpeedNormalizedMax = 200; +static const CGFloat POPBouncy3FrictionInterpolationMax = 0.01; + ++ (void)convertBounciness:(CGFloat)bounciness speed:(CGFloat)speed toTension:(CGFloat *)outTension friction:(CGFloat *)outFriction mass:(CGFloat *)outMass +{ + double b = POPNormalize(bounciness / POPBouncy3NormalizationScale, 0, POPBouncy3NormalizationRange); + b = POPProjectNormal(b, POPBouncy3BouncinessNormalizedMin, POPBouncy3BouncinessNormalizedMax); + + double s = POPNormalize(speed / POPBouncy3NormalizationScale, 0, POPBouncy3NormalizationRange); + + CGFloat tension = POPProjectNormal(s, POPBouncy3SpeedNormalizedMin, POPBouncy3SpeedNormalizedMax); + CGFloat friction = POPQuadraticOutInterpolation(b, POPBouncy3NoBounce(tension), POPBouncy3FrictionInterpolationMax); + + tension = POP_ANIMATION_TENSION_FOR_QC_TENSION(tension); + friction = POP_ANIMATION_FRICTION_FOR_QC_FRICTION(friction); + + if (outTension) { + *outTension = tension; + } + + if (outFriction) { + *outFriction = friction; + } + + if (outMass) { + *outMass = 1.0; + } +} + ++ (void)convertTension:(CGFloat)tension friction:(CGFloat)friction toBounciness:(CGFloat *)outBounciness speed:(CGFloat *)outSpeed +{ + // Convert to QC values, in which our calculations are done. + CGFloat qcFriction = QC_FRICTION_FOR_POP_ANIMATION_FRICTION(friction); + CGFloat qcTension = QC_TENSION_FOR_POP_ANIMATION_TENSION(tension); + + // Friction is a function of bounciness and tension, according to the following: + // friction = POPQuadraticOutInterpolation(b, POPBouncy3NoBounce(tension), POPBouncy3FrictionInterpolationMax); + // Solve for bounciness, given a tension and friction. + + CGFloat nobounceTension = POPBouncy3NoBounce(qcTension); + CGFloat bounciness1, bounciness2; + + POPQuadraticSolve((nobounceTension - POPBouncy3FrictionInterpolationMax), // a + 2 * (POPBouncy3FrictionInterpolationMax - nobounceTension), // b + (nobounceTension - qcFriction), // c + bounciness1, // x1 + bounciness2); // x2 + + + // Choose the quadratic solution within the normalized bounciness range + CGFloat projectedNormalizedBounciness = (bounciness2 < POPBouncy3BouncinessNormalizedMax) ? bounciness2 : bounciness1; + CGFloat projectedNormalizedSpeed = qcTension; + + // Reverse projection + normalization + CGFloat bounciness = ((POPBouncy3NormalizationRange * POPBouncy3NormalizationScale) / (POPBouncy3BouncinessNormalizedMax - POPBouncy3BouncinessNormalizedMin)) * (projectedNormalizedBounciness - POPBouncy3BouncinessNormalizedMin); + CGFloat speed = ((POPBouncy3NormalizationRange * POPBouncy3NormalizationScale) / (POPBouncy3SpeedNormalizedMax - POPBouncy3SpeedNormalizedMin)) * (projectedNormalizedSpeed - POPBouncy3SpeedNormalizedMin); + + // Write back results + if (outBounciness) { + *outBounciness = bounciness; + } + + if (outSpeed) { + *outSpeed = speed; + } +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/POPAnimationInternal.h b/submodules/LegacyComponents/LegacyComponents/POPAnimationInternal.h new file mode 100644 index 0000000000..1c0882c6b9 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/POPAnimationInternal.h @@ -0,0 +1,504 @@ +/** + Copyright (c) 2014-present, Facebook, Inc. + All rights reserved. + + This source code is licensed under the BSD-style license found in the + LICENSE file in the root directory of this source tree. An additional grant + of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "POPAnimation.h" +#import "POPAnimationRuntime.h" +#import "POPAnimationTracerInternal.h" +#import "POPSpringSolver.h" +#import "POPAction.h" + +using namespace POP; + +/** + Enumeration of supported animation types. + */ +enum POPAnimationType +{ + kPOPAnimationSpring, + kPOPAnimationDecay, + kPOPAnimationBasic, + kPOPAnimationCustom, +}; + +typedef struct +{ + CGFloat progress; + bool reached; +} POPProgressMarker; + +typedef void (^POPAnimationDidStartBlock)(POPAnimation *anim); +typedef void (^POPAnimationDidReachToValueBlock)(POPAnimation *anim); +typedef void (^POPAnimationCompletionBlock)(POPAnimation *anim, BOOL finished); +typedef void (^POPAnimationDidApplyBlock)(POPAnimation *anim); + +@interface POPAnimation() +- (instancetype)_init; + +@property (assign, nonatomic) SpringSolver4d *solver; +@property (readonly, nonatomic) POPAnimationType type; + +/** + The current animation value, updated while animation is progressing. + */ +@property (copy, nonatomic, readonly) id currentValue; + +/** + An array of optional progress markers. For each marker specified, the animation delegate will be informed when progress meets or exceeds the value specified. Specifying values outside of the [0, 1] range will give undefined results. + */ +@property (copy, nonatomic) NSArray *progressMarkers; + +/** + Return YES to indicate animation should continue animating. + */ +- (BOOL)_advance:(id)object currentTime:(CFTimeInterval)currentTime elapsedTime:(CFTimeInterval)elapsedTime; + +/** + Subclass override point to append animation description. + */ +- (void)_appendDescription:(NSMutableString *)s debug:(BOOL)debug; + +@end + +NS_INLINE NSString *describe(VectorConstRef vec) +{ + return NULL == vec ? @"null" : vec->toString(); +} + +NS_INLINE Vector4r vector4(VectorConstRef vec) +{ + return NULL == vec ? Vector4r::Zero() : vec->vector4r(); +} + +NS_INLINE Vector4d vector4d(VectorConstRef vec) +{ + if (NULL == vec) { + return Vector4d::Zero(); + } else { + return vec->vector4r().cast(); + } +} + +NS_INLINE bool vec_equal(VectorConstRef v1, VectorConstRef v2) +{ + if (v1 == v2) { + return true; + } + if (!v1 || !v2) { + return false; + } + return *v1 == *v2; +} + +NS_INLINE CGFloat * vec_data(VectorRef vec) +{ + return NULL == vec ? NULL : vec->data(); +} + +template +struct ComputeProgressFunctor { + CGFloat operator()(const T &value, const T &start, const T &end) const { + return 0; + } +}; + +template<> +struct ComputeProgressFunctor { + CGFloat operator()(const Vector4r &value, const Vector4r &start, const Vector4r &end) const { + CGFloat s = (value - start).squaredNorm(); // distance from start + CGFloat e = (value - end).squaredNorm(); // distance from end + CGFloat d = (end - start).squaredNorm(); // distance from start to end + + if (0 == d) { + return 1; + } else if (s > e) { + // s -------- p ---- e OR s ------- e ---- p + return sqrtr(s/d); + } else { + // s --- p --------- e OR p ---- s ------- e + return 1 - sqrtr(e/d); + } + } +}; + +struct _POPAnimationState; +struct _POPDecayAnimationState; +struct _POPPropertyAnimationState; + +extern _POPAnimationState *POPAnimationGetState(POPAnimation *a); + + +#define FB_FLAG_GET(stype, flag, getter) \ +- (BOOL)getter { \ + return ((stype *)_state)->flag; \ +} + +#define FB_FLAG_SET(stype, flag, mutator) \ +- (void)mutator (BOOL)value { \ + if (value == ((stype *)_state)->flag) \ + return; \ + ((stype *)_state)->flag = value; \ +} + +#define DEFINE_RW_FLAG(stype, flag, getter, mutator) \ + FB_FLAG_GET (stype, flag, getter) \ + FB_FLAG_SET (stype, flag, mutator) + +#define FB_PROPERTY_GET(stype, property, ctype) \ +- (ctype)property { \ + return ((stype *)_state)->property; \ +} + +#define FB_PROPERTY_SET(stype, property, mutator, ctype, ...) \ +- (void)mutator (ctype)value { \ + if (value == ((stype *)_state)->property) \ + return; \ + ((stype *)_state)->property = value; \ + __VA_ARGS__ \ +} + +#define FB_PROPERTY_SET_OBJ_COPY(stype, property, mutator, ctype, ...) \ +- (void)mutator (ctype)value { \ + if (value == ((stype *)_state)->property) \ + return; \ + ((stype *)_state)->property = [value copy]; \ + __VA_ARGS__ \ +} + +#define DEFINE_RW_PROPERTY(stype, flag, mutator, ctype, ...) \ + FB_PROPERTY_GET (stype, flag, ctype) \ + FB_PROPERTY_SET (stype, flag, mutator, ctype, __VA_ARGS__) + +#define DEFINE_RW_PROPERTY_OBJ(stype, flag, mutator, ctype, ...) \ + FB_PROPERTY_GET (stype, flag, ctype) \ + FB_PROPERTY_SET (stype, flag, mutator, ctype, __VA_ARGS__) + +#define DEFINE_RW_PROPERTY_OBJ_COPY(stype, flag, mutator, ctype, ...) \ + FB_PROPERTY_GET (stype, flag, ctype) \ + FB_PROPERTY_SET_OBJ_COPY (stype, flag, mutator, ctype, __VA_ARGS__) + + +/** + Internal delegate definition. + */ +@interface NSObject (POPAnimationDelegateInternal) +- (void)pop_animation:(POPAnimation *)anim didReachProgress:(CGFloat)progress; +@end + +struct _POPAnimationState +{ + id __unsafe_unretained self; + POPAnimationType type; + NSString *name; + NSUInteger ID; + CFTimeInterval beginTime; + CFTimeInterval startTime; + CFTimeInterval lastTime; + id __weak delegate; + POPAnimationDidStartBlock animationDidStartBlock; + POPAnimationDidReachToValueBlock animationDidReachToValueBlock; + POPAnimationCompletionBlock completionBlock; + POPAnimationDidApplyBlock animationDidApplyBlock; + NSMutableDictionary *dict; + POPAnimationTracer *tracer; + CGFloat progress; + NSInteger repeatCount; + + bool active:1; + bool paused:1; + bool removedOnCompletion:1; + + bool delegateDidStart:1; + bool delegateDidStop:1; + bool delegateDidProgress:1; + bool delegateDidApply:1; + bool delegateDidReachToValue:1; + + bool additive:1; + bool didReachToValue:1; + bool tracing:1; // corresponds to tracer started + bool userSpecifiedDynamics:1; + bool autoreverses:1; + bool repeatForever:1; + bool customFinished:1; + + _POPAnimationState(id __unsafe_unretained anim) : + self(anim), + type((POPAnimationType)0), + name(nil), + ID(0), + beginTime(0), + startTime(0), + lastTime(0), + delegate(nil), + animationDidStartBlock(nil), + animationDidReachToValueBlock(nil), + completionBlock(nil), + animationDidApplyBlock(nil), + dict(nil), + tracer(nil), + progress(0), + repeatCount(0), + active(false), + paused(true), + removedOnCompletion(true), + delegateDidStart(false), + delegateDidStop(false), + delegateDidProgress(false), + delegateDidApply(false), + delegateDidReachToValue(false), + additive(false), + didReachToValue(false), + tracing(false), + userSpecifiedDynamics(false), + autoreverses(false), + repeatForever(false), + customFinished(false) {} + + virtual ~_POPAnimationState() + { + name = nil; + dict = nil; + tracer = nil; + animationDidStartBlock = NULL; + animationDidReachToValueBlock = NULL; + completionBlock = NULL; + animationDidApplyBlock = NULL; + } + + bool isCustom() { + return kPOPAnimationCustom == type; + } + + bool isStarted() { + return 0 != startTime; + } + + id getDelegate() { + return delegate; + } + + void setDelegate(id d) { + if (d != delegate) { + delegate = d; + delegateDidStart = [d respondsToSelector:@selector(pop_animationDidStart:)]; + delegateDidStop = [d respondsToSelector:@selector(pop_animationDidStop:finished:)]; + delegateDidProgress = [d respondsToSelector:@selector(pop_animation:didReachProgress:)]; + delegateDidApply = [d respondsToSelector:@selector(pop_animationDidApply:)]; + delegateDidReachToValue = [d respondsToSelector:@selector(pop_animationDidReachToValue:)]; + } + } + + bool getPaused() { + return paused; + } + + void setPaused(bool f) { + if (f != paused) { + paused = f; + if (!paused) { + reset(false); + } + } + } + + CGFloat getProgress() { + return progress; + } + + /* returns true if started */ + bool startIfNeeded(id obj, CFTimeInterval time, CFTimeInterval offset) + { + bool started = false; + + // detect start based on time + if (0 == startTime && time >= beginTime + offset) { + + // activate & unpause + active = true; + setPaused(false); + + // note start time + startTime = lastTime = time; + started = true; + } + + // ensure values for running animation + bool running = active && !paused; + if (running) { + willRun(started, obj); + } + + // handle start + if (started) { + handleDidStart(); + } + + return started; + } + + void stop(bool removing, bool done) { + if (active) + { + // delegate progress one last time + if (done) { + delegateProgress(); + } + + if (removing) { + active = false; + } + + handleDidStop(done); + } else { + + // stopped before even started + // delegate start and stop regardless; matches CA behavior + if (!isStarted()) { + handleDidStart(); + handleDidStop(false); + } + } + + setPaused(true); + } + + virtual void handleDidStart() + { + if (delegateDidStart) { + ActionEnabler enabler; + [delegate pop_animationDidStart:self]; + } + + POPAnimationDidStartBlock block = animationDidStartBlock; + if (block != NULL) { + ActionEnabler enabler; + block(self); + } + + if (tracing) { + [tracer didStart]; + } + } + + void handleDidStop(BOOL done) + { + if (delegateDidStop) { + ActionEnabler enabler; + [delegate pop_animationDidStop:self finished:done]; + } + + // add another strong reference to completion block before callout + POPAnimationCompletionBlock block = completionBlock; + if (block != NULL) { + ActionEnabler enabler; + block(self, done); + } + + if (tracing) { + [tracer didStop:done]; + } + } + + /* virtual functions */ + virtual bool isDone() { + if (isCustom()) { + return customFinished; + } + + return false; + } + + bool advanceTime(CFTimeInterval time, id obj) { + bool advanced = false; + bool computedProgress = false; + CFTimeInterval dt = time - lastTime; + + switch (type) { + case kPOPAnimationSpring: + advanced = advance(time, dt, obj); + break; + case kPOPAnimationDecay: + advanced = advance(time, dt, obj); + break; + case kPOPAnimationBasic: { + advanced = advance(time, dt, obj); + computedProgress = true; + break; + } + case kPOPAnimationCustom: { + customFinished = [self _advance:obj currentTime:time elapsedTime:dt] ? false : true; + advanced = true; + break; + } + default: + break; + } + + if (advanced) { + + // estimate progress + if (!computedProgress) { + computeProgress(); + } + + // delegate progress + delegateProgress(); + + // update time + lastTime = time; + } + + return advanced; + } + + virtual void willRun(bool started, id obj) {} + virtual bool advance(CFTimeInterval time, CFTimeInterval dt, id obj) { return false; } + virtual void computeProgress() {} + virtual void delegateProgress() {} + + virtual void delegateApply() { + if (delegateDidApply) { + ActionEnabler enabler; + [delegate pop_animationDidApply:self]; + } + + POPAnimationDidApplyBlock block = animationDidApplyBlock; + if (block != NULL) { + ActionEnabler enabler; + block(self); + } + } + + virtual void reset(bool all) { + startTime = 0; + lastTime = 0; + } +}; + +typedef struct _POPAnimationState POPAnimationState; + + +@interface POPAnimation () +{ +@protected + struct _POPAnimationState *_state; +} + +@end + +// NSProxy extensions, for testing pursposes +@interface NSProxy (POP) +- (void)pop_addAnimation:(POPAnimation *)anim forKey:(NSString *)key; +- (void)pop_removeAllAnimations; +- (void)pop_removeAnimationForKey:(NSString *)key; +- (NSArray *)pop_animationKeys; +- (POPAnimation *)pop_animationForKey:(NSString *)key; +@end diff --git a/submodules/LegacyComponents/LegacyComponents/POPAnimationPrivate.h b/submodules/LegacyComponents/LegacyComponents/POPAnimationPrivate.h new file mode 100644 index 0000000000..dc1d83931a --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/POPAnimationPrivate.h @@ -0,0 +1,16 @@ +/** + Copyright (c) 2014-present, Facebook, Inc. + All rights reserved. + + This source code is licensed under the BSD-style license found in the + LICENSE file in the root directory of this source tree. An additional grant + of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "POPAnimation.h" + +#define POP_ANIMATION_FRICTION_FOR_QC_FRICTION(qcFriction) (25.0 + (((qcFriction - 8.0) / 2.0) * (25.0 - 19.0))) +#define POP_ANIMATION_TENSION_FOR_QC_TENSION(qcTension) (194.0 + (((qcTension - 30.0) / 50.0) * (375.0 - 194.0))) + +#define QC_FRICTION_FOR_POP_ANIMATION_FRICTION(fbFriction) (8.0 + 2.0 * ((fbFriction - 25.0)/(25.0 - 19.0))) +#define QC_TENSION_FOR_POP_ANIMATION_TENSION(fbTension) (30.0 + 50.0 * ((fbTension - 194.0)/(375.0 - 194.0))) diff --git a/submodules/LegacyComponents/LegacyComponents/POPAnimationRuntime.h b/submodules/LegacyComponents/LegacyComponents/POPAnimationRuntime.h new file mode 100644 index 0000000000..902c3121f7 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/POPAnimationRuntime.h @@ -0,0 +1,103 @@ +/** + Copyright (c) 2014-present, Facebook, Inc. + All rights reserved. + + This source code is licensed under the BSD-style license found in the + LICENSE file in the root directory of this source tree. An additional grant + of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import + +#import "POPVector.h" + +enum POPValueType +{ + kPOPValueUnknown = 0, + kPOPValueInteger, + kPOPValueFloat, + kPOPValuePoint, + kPOPValueSize, + kPOPValueRect, + kPOPValueEdgeInsets, + kPOPValueAffineTransform, + kPOPValueTransform, + kPOPValueRange, + kPOPValueColor, + kPOPValueSCNVector3, + kPOPValueSCNVector4, +}; + +using namespace POP; + +/** + Returns value type based on objc type description, given list of supported value types and length. + */ +extern POPValueType POPSelectValueType(const char *objctype, const POPValueType *types, size_t length); + +/** + Returns value type based on objc object, given a list of supported value types and length. + */ +extern POPValueType POPSelectValueType(id obj, const POPValueType *types, size_t length); + +/** + Array of all value types. + */ +extern const POPValueType kPOPAnimatableAllTypes[12]; + +/** + Array of all value types supported for animation. + */ +extern const POPValueType kPOPAnimatableSupportTypes[10]; + +/** + Returns a string description of a value type. + */ +extern NSString *POPValueTypeToString(POPValueType t); + +/** + Returns a mutable dictionary of weak pointer keys to weak pointer values. + */ +extern CFMutableDictionaryRef POPDictionaryCreateMutableWeakPointerToWeakPointer(NSUInteger capacity) CF_RETURNS_RETAINED; + +/** + Returns a mutable dictionary of weak pointer keys to weak pointer values. + */ +extern CFMutableDictionaryRef POPDictionaryCreateMutableWeakPointerToStrongObject(NSUInteger capacity) CF_RETURNS_RETAINED; + +/** + Box a vector. + */ +extern id POPBox(VectorConstRef vec, POPValueType type, bool force = false); + +/** + Unbox a vector. + */ +extern VectorRef POPUnbox(id value, POPValueType &type, NSUInteger &count, bool validate); + +/** + Read/write block typedefs for convenience. + */ +typedef void(^pop_animatable_read_block)(id obj, CGFloat *value); +typedef void(^pop_animatable_write_block)(id obj, const CGFloat *value); + +/** + Read object value and return a Vector4r. + */ +NS_INLINE Vector4r read_values(pop_animatable_read_block read, id obj, size_t count) +{ + Vector4r vec = Vector4r::Zero(); + if (0 == count) + return vec; + + read(obj, vec.data()); + + return vec; +} + +NS_INLINE NSString *POPStringFromBOOL(BOOL value) +{ + return value ? @"YES" : @"NO"; +} diff --git a/submodules/LegacyComponents/LegacyComponents/POPAnimationRuntime.mm b/submodules/LegacyComponents/LegacyComponents/POPAnimationRuntime.mm new file mode 100644 index 0000000000..28412a0e8b --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/POPAnimationRuntime.mm @@ -0,0 +1,325 @@ +/** + Copyright (c) 2014-present, Facebook, Inc. + All rights reserved. + + This source code is licensed under the BSD-style license found in the + LICENSE file in the root directory of this source tree. An additional grant + of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "POPAnimationRuntime.h" + +#import + +#import + +#if TARGET_OS_IPHONE +#import +#endif + +#import "POPVector.h" +#import "POPAnimationRuntime.h" +#import "POPCGUtils.h" +#import "POPGeometry.h" + +static Boolean pointerEqual(const void *ptr1, const void *ptr2) { + return ptr1 == ptr2; +} + +static CFHashCode pointerHash(const void *ptr) { + return (CFHashCode)(ptr); +} + +CFMutableDictionaryRef POPDictionaryCreateMutableWeakPointerToWeakPointer(NSUInteger capacity) +{ + CFDictionaryKeyCallBacks kcb = kCFTypeDictionaryKeyCallBacks; + + // weak, pointer keys + kcb.retain = NULL; + kcb.retain = NULL; + kcb.equal = pointerEqual; + kcb.hash = pointerHash; + + CFDictionaryValueCallBacks vcb = kCFTypeDictionaryValueCallBacks; + + // weak, pointer values + vcb.retain = NULL; + vcb.release = NULL; + vcb.equal = pointerEqual; + + return CFDictionaryCreateMutable(NULL, capacity, &kcb, &vcb); +} + +CFMutableDictionaryRef POPDictionaryCreateMutableWeakPointerToStrongObject(NSUInteger capacity) +{ + CFDictionaryKeyCallBacks kcb = kCFTypeDictionaryKeyCallBacks; + + // weak, pointer keys + kcb.retain = NULL; + kcb.release = NULL; + kcb.equal = pointerEqual; + kcb.hash = pointerHash; + + // strong, object values + CFDictionaryValueCallBacks vcb = kCFTypeDictionaryValueCallBacks; + + return CFDictionaryCreateMutable(NULL, capacity, &kcb, &vcb); +} + +static bool FBCompareTypeEncoding(const char *objctype, POPValueType type) +{ + switch (type) + { + case kPOPValueFloat: + return (strcmp(objctype, @encode(float)) == 0 + || strcmp(objctype, @encode(double)) == 0 + ); + + case kPOPValuePoint: + return (strcmp(objctype, @encode(CGPoint)) == 0 +#if !TARGET_OS_IPHONE + || strcmp(objctype, @encode(NSPoint)) == 0 +#endif + ); + + case kPOPValueSize: + return (strcmp(objctype, @encode(CGSize)) == 0 +#if !TARGET_OS_IPHONE + || strcmp(objctype, @encode(NSSize)) == 0 +#endif + ); + + case kPOPValueRect: + return (strcmp(objctype, @encode(CGRect)) == 0 +#if !TARGET_OS_IPHONE + || strcmp(objctype, @encode(NSRect)) == 0 +#endif + ); + case kPOPValueEdgeInsets: +#if TARGET_OS_IPHONE + return strcmp(objctype, @encode(UIEdgeInsets)) == 0; +#else + return false; +#endif + + case kPOPValueAffineTransform: + return strcmp(objctype, @encode(CGAffineTransform)) == 0; + + case kPOPValueTransform: + return strcmp(objctype, @encode(CATransform3D)) == 0; + + case kPOPValueRange: + return strcmp(objctype, @encode(CFRange)) == 0 + || strcmp(objctype, @encode (NSRange)) == 0; + + case kPOPValueInteger: + return (strcmp(objctype, @encode(int)) == 0 + || strcmp(objctype, @encode(unsigned int)) == 0 + || strcmp(objctype, @encode(short)) == 0 + || strcmp(objctype, @encode(unsigned short)) == 0 + || strcmp(objctype, @encode(long)) == 0 + || strcmp(objctype, @encode(unsigned long)) == 0 + || strcmp(objctype, @encode(long long)) == 0 + || strcmp(objctype, @encode(unsigned long long)) == 0 + ); + + case kPOPValueSCNVector3: +#if SCENEKIT_SDK_AVAILABLE + return strcmp(objctype, @encode(SCNVector3)) == 0; +#else + return false; +#endif + + case kPOPValueSCNVector4: +#if SCENEKIT_SDK_AVAILABLE + return strcmp(objctype, @encode(SCNVector4)) == 0; +#else + return false; +#endif + + default: + return false; + } +} + +POPValueType POPSelectValueType(const char *objctype, const POPValueType *types, size_t length) +{ + if (NULL != objctype) { + for (size_t idx = 0; idx < length; idx++) { + if (FBCompareTypeEncoding(objctype, types[idx])) + return types[idx]; + } + } + return kPOPValueUnknown; +} + +POPValueType POPSelectValueType(id obj, const POPValueType *types, size_t length) +{ + if ([obj isKindOfClass:[NSValue class]]) { + return POPSelectValueType([obj objCType], types, length); + } else if (NULL != POPCGColorWithColor(obj)) { + return kPOPValueColor; + } + return kPOPValueUnknown; +} + +const POPValueType kPOPAnimatableAllTypes[12] = {kPOPValueInteger, kPOPValueFloat, kPOPValuePoint, kPOPValueSize, kPOPValueRect, kPOPValueEdgeInsets, kPOPValueAffineTransform, kPOPValueTransform, kPOPValueRange, kPOPValueColor, kPOPValueSCNVector3, kPOPValueSCNVector4}; + +const POPValueType kPOPAnimatableSupportTypes[10] = {kPOPValueInteger, kPOPValueFloat, kPOPValuePoint, kPOPValueSize, kPOPValueRect, kPOPValueEdgeInsets, kPOPValueColor, kPOPValueSCNVector3, kPOPValueSCNVector4}; + +NSString *POPValueTypeToString(POPValueType t) +{ + switch (t) { + case kPOPValueUnknown: + return @"unknown"; + case kPOPValueInteger: + return @"int"; + case kPOPValueFloat: + return @"CGFloat"; + case kPOPValuePoint: + return @"CGPoint"; + case kPOPValueSize: + return @"CGSize"; + case kPOPValueRect: + return @"CGRect"; + case kPOPValueEdgeInsets: + return @"UIEdgeInsets"; + case kPOPValueAffineTransform: + return @"CGAffineTransform"; + case kPOPValueTransform: + return @"CATransform3D"; + case kPOPValueRange: + return @"CFRange"; + case kPOPValueColor: + return @"CGColorRef"; + case kPOPValueSCNVector3: + return @"SCNVector3"; + case kPOPValueSCNVector4: + return @"SCNVector4"; + default: + return nil; + } +} + +id POPBox(VectorConstRef vec, POPValueType type, bool force) +{ + if (NULL == vec) + return nil; + + switch (type) { + case kPOPValueInteger: + case kPOPValueFloat: + return @(vec->data()[0]); + break; + case kPOPValuePoint: + return [NSValue valueWithCGPoint:vec->cg_point()]; + break; + case kPOPValueSize: + return [NSValue valueWithCGSize:vec->cg_size()]; + break; + case kPOPValueRect: + return [NSValue valueWithCGRect:vec->cg_rect()]; + break; +#if TARGET_OS_IPHONE + case kPOPValueEdgeInsets: + return [NSValue valueWithUIEdgeInsets:vec->ui_edge_insets()]; + break; +#endif + case kPOPValueColor: { + return (__bridge_transfer id)vec->cg_color(); + break; + } +#if SCENEKIT_SDK_AVAILABLE + case kPOPValueSCNVector3: { + return [NSValue valueWithSCNVector3:vec->scn_vector3()]; + break; + } + case kPOPValueSCNVector4: { + return [NSValue valueWithSCNVector4:vec->scn_vector4()]; + break; + } +#endif + default: + return force ? [NSValue valueWithCGPoint:vec->cg_point()] : nil; + break; + } +} + +static VectorRef vectorize(id value, POPValueType type) +{ + Vector *vec = NULL; + + switch (type) { + case kPOPValueInteger: + case kPOPValueFloat: + vec = Vector::new_cg_float([value floatValue]); + break; + case kPOPValuePoint: + vec = Vector::new_cg_point([value CGPointValue]); + break; + case kPOPValueSize: + vec = Vector::new_cg_size([value CGSizeValue]); + break; + case kPOPValueRect: + vec = Vector::new_cg_rect([value CGRectValue]); + break; +#if TARGET_OS_IPHONE + case kPOPValueEdgeInsets: + vec = Vector::new_ui_edge_insets([value UIEdgeInsetsValue]); + break; +#endif + case kPOPValueAffineTransform: + vec = Vector::new_cg_affine_transform([value CGAffineTransformValue]); + break; + case kPOPValueColor: + vec = Vector::new_cg_color(POPCGColorWithColor(value)); + break; +#if SCENEKIT_SDK_AVAILABLE + case kPOPValueSCNVector3: + vec = Vector::new_scn_vector3([value SCNVector3Value]); + break; + case kPOPValueSCNVector4: + vec = Vector::new_scn_vector4([value SCNVector4Value]); + break; +#endif + default: + break; + } + + return VectorRef(vec); +} + +VectorRef POPUnbox(id value, POPValueType &animationType, NSUInteger &count, bool validate) +{ + if (nil == value) { + count = 0; + return VectorRef(NULL); + } + + // determine type of value + POPValueType valueType = POPSelectValueType(value, kPOPAnimatableSupportTypes, POP_ARRAY_COUNT(kPOPAnimatableSupportTypes)); + + // handle unknown types + if (kPOPValueUnknown == valueType) { + NSString *valueDesc = kPOPValueUnknown != valueType ? POPValueTypeToString(valueType) : [[value class] description]; + [NSException raise:@"Unsuported value" format:@"Animating %@ values is not supported", valueDesc]; + } + + // vectorize + VectorRef vec = vectorize(value, valueType); + + if (kPOPValueUnknown == animationType || 0 == count) { + // update animation type based on value type + animationType = valueType; + if (NULL != vec) { + count = vec->size(); + } + } else if (validate) { + // allow for mismatched types, so long as vector size matches + if (count != vec->size()) { + [NSException raise:@"Invalid value" format:@"%@ should be of type %@", value, POPValueTypeToString(animationType)]; + } + } + + return vec; +} diff --git a/submodules/LegacyComponents/LegacyComponents/POPAnimationTracer.h b/submodules/LegacyComponents/LegacyComponents/POPAnimationTracer.h new file mode 100644 index 0000000000..ce0e64d819 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/POPAnimationTracer.h @@ -0,0 +1,60 @@ +/** + Copyright (c) 2014-present, Facebook, Inc. + All rights reserved. + + This source code is licensed under the BSD-style license found in the + LICENSE file in the root directory of this source tree. An additional grant + of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import + +@class POPAnimation; + +/** + @abstract Tracer of animation events to fasciliate unit testing & debugging. + */ +@interface POPAnimationTracer : NSObject + +/** + @abstract Start recording events. + */ +- (void)start; + +/** + @abstract Stop recording events. + */ +- (void)stop; + +/** + @abstract Resets any recoded events. Continues recording events if already started. + */ +- (void)reset; + +/** + @abstract Property representing all recorded events. + @discussion Events are returned in order of occurence. + */ +@property (nonatomic, assign, readonly) NSArray *allEvents; + +/** + @abstract Property representing all recorded write events for convenience. + @discussion Events are returned in order of occurence. + */ +@property (nonatomic, assign, readonly) NSArray *writeEvents; + +/** + @abstract Queries for events of specified type. + @param type The type of event to return. + @returns An array of events of specified type in order of occurence. + */ +- (NSArray *)eventsWithType:(POPAnimationEventType)type; + +/** + @abstract Property indicating whether tracer should automatically log events and reset collection on animation completion. + */ +@property (nonatomic, assign) BOOL shouldLogAndResetOnCompletion; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/POPAnimationTracer.mm b/submodules/LegacyComponents/LegacyComponents/POPAnimationTracer.mm new file mode 100644 index 0000000000..243fb9b8f9 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/POPAnimationTracer.mm @@ -0,0 +1,191 @@ +/** + Copyright (c) 2014-present, Facebook, Inc. + All rights reserved. + + This source code is licensed under the BSD-style license found in the + LICENSE file in the root directory of this source tree. An additional grant + of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "POPAnimationTracer.h" + +#import + +#import "POPAnimationEventInternal.h" +#import "POPAnimationInternal.h" +#import "POPSpringAnimation.h" + +@implementation POPAnimationTracer +{ + __weak POPAnimation *_animation; + POPAnimationState *_animationState; + NSMutableArray *_events; + BOOL _animationHasVelocity; +} +@synthesize shouldLogAndResetOnCompletion = _shouldLogAndResetOnCompletion; + +static POPAnimationEvent *create_event(POPAnimationTracer *self, POPAnimationEventType type, id value = nil, bool recordAnimation = false) +{ + bool useLocalTime = 0 != self->_animationState->startTime; + CFTimeInterval time = useLocalTime + ? self->_animationState->lastTime - self->_animationState->startTime + : self->_animationState->lastTime; + + POPAnimationEvent *event; + + if (!value) { + event = [[POPAnimationEvent alloc] initWithType:type time:time]; + } else { + event = [[POPAnimationValueEvent alloc] initWithType:type time:time value:value]; + if (self->_animationHasVelocity) { + [(POPAnimationValueEvent *)event setVelocity:[(POPSpringAnimation *)self->_animation velocity]]; + } + } + + if (recordAnimation) { + event.animationDescription = [self->_animation description]; + } + + return event; +} + +- (id)initWithAnimation:(POPAnimation *)anAnim +{ + self = [super init]; + if (nil != self) { + _animation = anAnim; + _animationState = POPAnimationGetState(anAnim); + _events = [[NSMutableArray alloc] initWithCapacity:50]; + _animationHasVelocity = [anAnim respondsToSelector:@selector(velocity)]; + } + return self; +} + +- (void)readPropertyValue:(id)aValue +{ + POPAnimationEvent *event = create_event(self, kPOPAnimationEventPropertyRead, aValue); + [_events addObject:event]; +} + +- (void)writePropertyValue:(id)aValue +{ + POPAnimationEvent *event = create_event(self, kPOPAnimationEventPropertyWrite, aValue); + [_events addObject:event]; +} + +- (void)updateToValue:(id)aValue +{ + POPAnimationEvent *event = create_event(self, kPOPAnimationEventToValueUpdate, aValue); + [_events addObject:event]; +} + +- (void)updateFromValue:(id)aValue +{ + POPAnimationEvent *event = create_event(self, kPOPAnimationEventFromValueUpdate, aValue); + [_events addObject:event]; +} + +- (void)updateVelocity:(id)aValue +{ + POPAnimationEvent *event = create_event(self, kPOPAnimationEventVelocityUpdate, aValue); + [_events addObject:event]; +} + +- (void)updateSpeed:(float)aFloat +{ + POPAnimationEvent *event = create_event(self, kPOPAnimationEventSpeedUpdate, @(aFloat)); + [_events addObject:event]; +} + +- (void)updateBounciness:(float)aFloat +{ + POPAnimationEvent *event = create_event(self, kPOPAnimationEventBouncinessUpdate, @(aFloat)); + [_events addObject:event]; +} + +- (void)updateFriction:(float)aFloat +{ + POPAnimationEvent *event = create_event(self, kPOPAnimationEventFrictionUpdate, @(aFloat)); + [_events addObject:event]; +} + +- (void)updateMass:(float)aFloat +{ + POPAnimationEvent *event = create_event(self, kPOPAnimationEventMassUpdate, @(aFloat)); + [_events addObject:event]; +} + +- (void)updateTension:(float)aFloat +{ + POPAnimationEvent *event = create_event(self, kPOPAnimationEventTensionUpdate, @(aFloat)); + [_events addObject:event]; +} + +- (void)didStart +{ + POPAnimationEvent *event = create_event(self, kPOPAnimationEventDidStart, nil, true); + [_events addObject:event]; +} + +- (void)didStop:(BOOL)finished +{ + POPAnimationEvent *event = create_event(self, kPOPAnimationEventDidStop, @(finished), true); + [_events addObject:event]; + + if (_shouldLogAndResetOnCompletion) { + NSLog(@"events:%@", self.allEvents); + [self reset]; + } +} + +- (void)didReachToValue:(id)aValue +{ + POPAnimationEvent *event = create_event(self, kPOPAnimationEventDidReachToValue, aValue); + [_events addObject:event]; +} + +- (void)autoreversed +{ + POPAnimationEvent *event = create_event(self, kPOPAnimationEventAutoreversed); + [_events addObject:event]; +} + +- (void)start +{ + POPAnimationState *s = POPAnimationGetState(_animation); + s->tracing = true; +} + +- (void)stop +{ + POPAnimationState *s = POPAnimationGetState(_animation); + s->tracing = false; +} + +- (void)reset +{ + [_events removeAllObjects]; +} + +- (NSArray *)allEvents +{ + return [_events copy]; +} + +- (NSArray *)writeEvents +{ + return [self eventsWithType:kPOPAnimationEventPropertyWrite]; +} + +- (NSArray *)eventsWithType:(POPAnimationEventType)aType +{ + NSMutableArray *array = [NSMutableArray array]; + for (POPAnimationEvent *event in _events) { + if (aType == event.type) { + [array addObject:event]; + } + } + return array; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/POPAnimationTracerInternal.h b/submodules/LegacyComponents/LegacyComponents/POPAnimationTracerInternal.h new file mode 100644 index 0000000000..6e3dbcae01 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/POPAnimationTracerInternal.h @@ -0,0 +1,96 @@ +/** + Copyright (c) 2014-present, Facebook, Inc. + All rights reserved. + + This source code is licensed under the BSD-style license found in the + LICENSE file in the root directory of this source tree. An additional grant + of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "POPAnimationTracer.h" + +@interface POPAnimationTracer (Internal) + +/** + @abstract Designated initalizer. Pass the animation being traced. + */ +- (instancetype)initWithAnimation:(POPAnimation *)anAnim; + +/** + @abstract Records read value. + */ +- (void)readPropertyValue:(id)aValue; + +/** + @abstract Records write value. + */ +- (void)writePropertyValue:(id)aValue; + +/** + Records to value update. + */ +- (void)updateToValue:(id)aValue; + +/** + @abstract Records from value update. + */ +- (void)updateFromValue:(id)aValue; + +/** + @abstract Records from value update. + */ +- (void)updateVelocity:(id)aValue; + +/** + @abstract Records bounciness update. + */ +- (void)updateBounciness:(float)aFloat; + +/** + @abstract Records speed update. + */ +- (void)updateSpeed:(float)aFloat; + +/** + @abstract Records friction update. + */ +- (void)updateFriction:(float)aFloat; + +/** + @abstract Records mass update. + */ +- (void)updateMass:(float)aFloat; + +/** + @abstract Records tension update. + */ +- (void)updateTension:(float)aFloat; + +/** + @abstract Records did add. + */ +- (void)didAdd; + +/** + @abstract Records did start. + */ +- (void)didStart; + +/** + @abstract Records did stop. + */ +- (void)didStop:(BOOL)finished; + +/** + @abstract Records did reach to value. + */ +- (void)didReachToValue:(id)aValue; + +/** + @abstract Records when an autoreverse animation takes place. + */ +- (void)autoreversed; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/POPAnimator.h b/submodules/LegacyComponents/LegacyComponents/POPAnimator.h new file mode 100644 index 0000000000..7d71d2488c --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/POPAnimator.h @@ -0,0 +1,47 @@ +/** + Copyright (c) 2014-present, Facebook, Inc. + All rights reserved. + + This source code is licensed under the BSD-style license found in the + LICENSE file in the root directory of this source tree. An additional grant + of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +@protocol POPAnimatorDelegate; + +/** + @abstract The animator class renders animations. + */ +@interface POPAnimator : NSObject + +/** + @abstract The shared animator instance. + @discussion Consumers should generally use the shared instance in lieu of creating new instances. + */ ++ (instancetype)sharedAnimator; + +/** + @abstract The optional animator delegate. + */ +@property (weak, nonatomic) id delegate; + +@end + +/** + @abstract The animator delegate. + */ +@protocol POPAnimatorDelegate + +/** + @abstract Called on each frame before animation application. + */ +- (void)animatorWillAnimate:(POPAnimator *)animator; + +/** + @abstract Called on each frame after animation application. + */ +- (void)animatorDidAnimate:(POPAnimator *)animator; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/POPAnimator.mm b/submodules/LegacyComponents/LegacyComponents/POPAnimator.mm new file mode 100644 index 0000000000..c93a8a8cb0 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/POPAnimator.mm @@ -0,0 +1,805 @@ +/** + Copyright (c) 2014-present, Facebook, Inc. + All rights reserved. + + This source code is licensed under the BSD-style license found in the + LICENSE file in the root directory of this source tree. An additional grant + of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "POPAnimator.h" +#import "POPAnimatorPrivate.h" + +#import +#import +#import + +#import + +#import "POPAnimation.h" +#import "POPAnimationExtras.h" +#import "POPBasicAnimationInternal.h" +#import "POPDecayAnimation.h" + +#if !TARGET_OS_IPHONE +#import +#endif + +using namespace std; +using namespace POP; + +#define ENABLE_LOGGING_DEBUG 0 +#define ENABLE_LOGGING_INFO 0 + +#if ENABLE_LOGGING_DEBUG +#define FBLogAnimDebug NSLog +#else +#define FBLogAnimDebug(...) +#endif + +#if ENABLE_LOGGING_INFO +#define FBLogAnimInfo NSLog +#else +#define FBLogAnimInfo(...) +#endif + +class POPAnimatorItem +{ +public: + id __weak object; + NSString *key; + POPAnimation *animation; + NSInteger refCount; + id __unsafe_unretained unretainedObject; + + POPAnimatorItem(id o, NSString *k, POPAnimation *a) POP_NOTHROW + { + object = o; + key = [k copy]; + animation = a; + refCount = 1; + unretainedObject = o; + } + + ~POPAnimatorItem() + { + } + + bool operator==(const POPAnimatorItem& o) const { + return unretainedObject == o.unretainedObject && animation == o.animation && [key isEqualToString:o.key]; + } + +}; + +typedef std::shared_ptr POPAnimatorItemRef; +typedef std::shared_ptr POPAnimatorItemConstRef; + +typedef std::list POPAnimatorItemList; +typedef POPAnimatorItemList::iterator POPAnimatorItemListIterator; +typedef POPAnimatorItemList::const_iterator POPAnimatorItemListConstIterator; + +static BOOL _disableBackgroundThread = YES; + +@interface POPAnimator () +{ +#if TARGET_OS_IPHONE + CADisplayLink *_displayLink; +#else + CVDisplayLinkRef _displayLink; + int32_t _enqueuedRender; +#endif + POPAnimatorItemList _list; + CFMutableDictionaryRef _dict; + NSMutableArray *_observers; + POPAnimatorItemList _pendingList; + CFRunLoopObserverRef _pendingListObserver; + CFTimeInterval _slowMotionStartTime; + CFTimeInterval _slowMotionLastTime; + CFTimeInterval _slowMotionAccumulator; + CFTimeInterval _beginTime; + OSSpinLock _lock; + BOOL _disableDisplayLink; +} +@end + +@implementation POPAnimator +@synthesize delegate = _delegate; +@synthesize disableDisplayLink = _disableDisplayLink; +@synthesize beginTime = _beginTime; + +#if !TARGET_OS_IPHONE +static CVReturn displayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeStamp *now, const CVTimeStamp *outputTime, CVOptionFlags flagsIn, CVOptionFlags *flagsOut, void *context) +{ + if (_disableBackgroundThread) { + __unsafe_unretained POPAnimator *pa = (__bridge POPAnimator *)context; + int32_t* enqueuedRender = &pa->_enqueuedRender; + if (*enqueuedRender == 0) { + OSAtomicIncrement32(enqueuedRender); + dispatch_async(dispatch_get_main_queue(), ^{ + [(__bridge POPAnimator*)context render]; + OSAtomicDecrement32(enqueuedRender); + }); + } + } else { + [(__bridge POPAnimator*)context render]; + } + return kCVReturnSuccess; +} +#endif + +// call while holding lock +static void updateDisplayLink(POPAnimator *self) +{ + BOOL paused = (0 == self->_observers.count && self->_list.empty()) || self->_disableDisplayLink; + +#if TARGET_OS_IPHONE + if (paused != self->_displayLink.paused) { + FBLogAnimInfo(paused ? @"pausing display link" : @"unpausing display link"); + self->_displayLink.paused = paused; + } +#else + if (paused == CVDisplayLinkIsRunning(self->_displayLink)) { + FBLogAnimInfo(paused ? @"pausing display link" : @"unpausing display link"); + if (paused) { + CVDisplayLinkStop(self->_displayLink); + } else { + CVDisplayLinkStart(self->_displayLink); + } + } +#endif +} + +static void updateAnimatable(id obj, POPPropertyAnimationState *anim, bool shouldAvoidExtraneousWrite = false) +{ + // handle user-initiated stop or pause; hault animation + if (!anim->active || anim->paused) + return; + + if (anim->hasValue()) { + pop_animatable_write_block write = anim->property.writeBlock; + if (NULL == write) + return; + + // current animation value + VectorRef currentVec = anim->currentValue(); + + if (!anim->additive) { + + // if avoiding extraneous writes and we have a read block defined + if (shouldAvoidExtraneousWrite) { + + pop_animatable_read_block read = anim->property.readBlock; + if (read) { + // compare current animation value with object value + Vector4r currentValue = currentVec->vector4r(); + Vector4r objectValue = read_values(read, obj, anim->valueCount); + if (objectValue == currentValue) { + return; + } + } + } + + // update previous values; support animation convergence + anim->previous2Vec = anim->previousVec; + anim->previousVec = currentVec; + + // write value + write(obj, currentVec->data()); + if (anim->tracing) { + [anim->tracer writePropertyValue:POPBox(currentVec, anim->valueType, true)]; + } + } else { + pop_animatable_read_block read = anim->property.readBlock; + NSCAssert(read, @"additive requires an animatable property readBlock"); + if (NULL == read) { + return; + } + + // object value + Vector4r objectValue = read_values(read, obj, anim->valueCount); + + // current value + Vector4r currentValue = currentVec->vector4r(); + + // determine animation change + if (anim->previousVec) { + Vector4r previousValue = anim->previousVec->vector4r(); + currentValue -= previousValue; + } + + // avoid writing no change + if (shouldAvoidExtraneousWrite && currentValue == Vector4r::Zero()) { + return; + } + + // add to object value + currentValue += objectValue; + + // update previous values; support animation convergence + anim->previous2Vec = anim->previousVec; + anim->previousVec = currentVec; + + // write value + write(obj, currentValue.data()); + if (anim->tracing) { + [anim->tracer writePropertyValue:POPBox(currentVec, anim->valueType, true)]; + } + } + } +} + +static void applyAnimationTime(id obj, POPAnimationState *state, CFTimeInterval time) +{ + if (!state->advanceTime(time, obj)) { + return; + } + + POPPropertyAnimationState *ps = dynamic_cast(state); + if (NULL != ps) { + updateAnimatable(obj, ps); + } + + state->delegateApply(); +} + +static void applyAnimationToValue(id obj, POPAnimationState *state) +{ + POPPropertyAnimationState *ps = dynamic_cast(state); + + if (NULL != ps) { + + // finalize progress + ps->finalizeProgress(); + + // write to value, updating only if needed + updateAnimatable(obj, ps, true); + } + + state->delegateApply(); +} + +static POPAnimation *deleteDictEntry(POPAnimator *self, id __unsafe_unretained obj, NSString *key, BOOL cleanup = YES) +{ + POPAnimation *anim = nil; + + // lock + OSSpinLockLock(&self->_lock); + + NSMutableDictionary *keyAnimationsDict = (__bridge id)CFDictionaryGetValue(self->_dict, (__bridge void *)obj); + if (keyAnimationsDict) { + + anim = keyAnimationsDict[key]; + if (anim) { + + // remove key + [keyAnimationsDict removeObjectForKey:key]; + + // cleanup empty dictionaries + if (cleanup && 0 == keyAnimationsDict.count) { + CFDictionaryRemoveValue(self->_dict, (__bridge void *)obj); + } + } + } + + // unlock + OSSpinLockUnlock(&self->_lock); + return anim; +} + +static void stopAndCleanup(POPAnimator *self, POPAnimatorItemRef item, bool shouldRemove, bool finished) +{ + // remove + if (shouldRemove) { + deleteDictEntry(self, item->unretainedObject, item->key); + } + + // stop + POPAnimationState *state = POPAnimationGetState(item->animation); + state->stop(shouldRemove, finished); + + if (shouldRemove) { + // lock + OSSpinLockLock(&self->_lock); + + // find item in list + // may have already been removed on animationDidStop: + POPAnimatorItemListIterator find_iter = find(self->_list.begin(), self->_list.end(), item); + BOOL found = find_iter != self->_list.end(); + + if (found) { + self->_list.erase(find_iter); + } + + // unlock + OSSpinLockUnlock(&self->_lock); + } +} + ++ (id)sharedAnimator +{ + static POPAnimator* _animator = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + _animator = [[POPAnimator alloc] init]; + }); + return _animator; +} + ++ (BOOL)disableBackgroundThread +{ + return _disableBackgroundThread; +} + ++ (void)setDisableBackgroundThread:(BOOL)flag +{ + _disableBackgroundThread = flag; +} + +#pragma mark - Lifecycle + +- (id)init +{ + self = [super init]; + if (nil == self) return nil; + +#if TARGET_OS_IPHONE + _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(render)]; + _displayLink.paused = YES; + [_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; +#else + CVDisplayLinkCreateWithActiveCGDisplays(&_displayLink); + CVDisplayLinkSetOutputCallback(_displayLink, displayLinkCallback, (__bridge void *)self); +#endif + + _dict = POPDictionaryCreateMutableWeakPointerToStrongObject(5); + _lock = OS_SPINLOCK_INIT; + + return self; +} + +- (void)dealloc +{ +#if TARGET_OS_IPHONE + [_displayLink invalidate]; +#else + CVDisplayLinkStop(_displayLink); + CVDisplayLinkRelease(_displayLink); +#endif + [self _clearPendingListObserver]; +} + +#pragma mark - Utility + +- (void)_processPendingList +{ + // rendering pending animations + CFTimeInterval time = [self _currentRenderTime]; + [self _renderTime:(0 != _beginTime) ? _beginTime : time items:_pendingList]; + + // lock + OSSpinLockLock(&_lock); + + // clear list and observer + _pendingList.clear(); + [self _clearPendingListObserver]; + + // unlock + OSSpinLockUnlock(&_lock); +} + +- (void)_clearPendingListObserver +{ + if (_pendingListObserver) { + CFRunLoopRemoveObserver(CFRunLoopGetMain(), _pendingListObserver, kCFRunLoopCommonModes); + CFRelease(_pendingListObserver); + _pendingListObserver = NULL; + } +} + +- (void)_scheduleProcessPendingList +{ + // see WebKit for magic numbers, eg http://trac.webkit.org/changeset/166540 + static const CFIndex CATransactionCommitRunLoopOrder = 2000000; + static const CFIndex POPAnimationApplyRunLoopOrder = CATransactionCommitRunLoopOrder - 1; + + // lock + OSSpinLockLock(&_lock); + + if (!_pendingListObserver) { + __weak POPAnimator *weakSelf = self; + + _pendingListObserver = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopBeforeWaiting | kCFRunLoopExit, false, POPAnimationApplyRunLoopOrder, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) { + [weakSelf _processPendingList]; + }); + + if (_pendingListObserver) { + CFRunLoopAddObserver(CFRunLoopGetMain(), _pendingListObserver, kCFRunLoopCommonModes); + } + } + + // unlock + OSSpinLockUnlock(&_lock); +} + +- (void)_renderTime:(CFTimeInterval)time items:(std::list)items +{ + // begin transaction with actions disabled + [CATransaction begin]; + [CATransaction setDisableActions:YES]; + + // notify delegate + __strong __typeof__(_delegate) delegate = _delegate; + [delegate animatorWillAnimate:self]; + + // lock + OSSpinLockLock(&_lock); + + // count active animations + const NSUInteger count = items.size(); + if (0 == count) { + // unlock + OSSpinLockUnlock(&_lock); + } else { + // copy list into vector + std::vector vector{ std::begin(items), std::end(items) }; + + // unlock + OSSpinLockUnlock(&_lock); + + for (auto item : vector) { + [self _renderTime:time item:item]; + } + } + + // notify observers + for (id observer in self.observers) { + [observer animatorDidAnimate:(id)self]; + } + + // lock + OSSpinLockLock(&_lock); + + // update display link + updateDisplayLink(self); + + // unlock + OSSpinLockUnlock(&_lock); + + // notify delegate and commit + [delegate animatorDidAnimate:self]; + [CATransaction commit]; +} + +- (void)_renderTime:(CFTimeInterval)time item:(POPAnimatorItemRef)item +{ + id obj = item->object; + POPAnimation *anim = item->animation; + POPAnimationState *state = POPAnimationGetState(anim); + + if (nil == obj) { + // object exists not; stop animating + NSAssert(item->unretainedObject, @"object should exist"); + stopAndCleanup(self, item, true, false); + } else { + + // start if needed + state->startIfNeeded(obj, time, _slowMotionAccumulator); + + // only run active, not paused animations + if (state->active && !state->paused) { + // object exists; animate + applyAnimationTime(obj, state, time); + + FBLogAnimDebug(@"time:%f running:%@", time, item->animation); + if (state->isDone()) { + // set end value + applyAnimationToValue(obj, state); + + state->repeatCount--; + if (state->repeatForever || state->repeatCount > 0) { + if ([anim isKindOfClass:[POPPropertyAnimation class]]) { + POPPropertyAnimation *propAnim = (POPPropertyAnimation *)anim; + id oldFromValue = propAnim.fromValue; + propAnim.fromValue = propAnim.toValue; + + if (state->autoreverses) { + if (state->tracing) { + [state->tracer autoreversed]; + } + + if (state->type == kPOPAnimationDecay) { + POPDecayAnimation *decayAnimation = (POPDecayAnimation *)propAnim; + decayAnimation.velocity = [decayAnimation reversedVelocity]; + } else { + propAnim.toValue = oldFromValue; + } + } else { + if (state->type == kPOPAnimationDecay) { + POPDecayAnimation *decayAnimation = (POPDecayAnimation *)propAnim; + id originalVelocity = decayAnimation.originalVelocity; + decayAnimation.velocity = originalVelocity; + } else { + propAnim.fromValue = oldFromValue; + } + } + } + + state->stop(NO, NO); + state->reset(true); + + state->startIfNeeded(obj, time, _slowMotionAccumulator); + } else { + stopAndCleanup(self, item, state->removedOnCompletion, YES); + } + } + } + } +} + +#pragma mark - API + +- (NSArray *)observers +{ + // lock + OSSpinLockLock(&_lock); + + // get observers + NSArray *observers = 0 != _observers.count ? [_observers copy] : nil; + + // unlock + OSSpinLockUnlock(&_lock); + return observers; +} + +- (void)addAnimation:(POPAnimation *)anim forObject:(id)obj key:(NSString *)key +{ + if (!anim || !obj) { + return; + } + + // support arbitrarily many nil keys + if (!key) { + key = [[NSUUID UUID] UUIDString]; + } + + // lock + OSSpinLockLock(&_lock); + + // get key, animation dict associated with object + NSMutableDictionary *keyAnimationDict = (__bridge id)CFDictionaryGetValue(_dict, (__bridge void *)obj); + + // update associated animation state + if (nil == keyAnimationDict) { + keyAnimationDict = [NSMutableDictionary dictionary]; + CFDictionarySetValue(_dict, (__bridge void *)obj, (__bridge void *)keyAnimationDict); + } else { + // if the animation instance already exists, avoid cancelling only to restart + POPAnimation *existingAnim = keyAnimationDict[key]; + if (existingAnim) { + // unlock + OSSpinLockUnlock(&_lock); + + if (existingAnim == anim) { + return; + } + [self removeAnimationForObject:obj key:key cleanupDict:NO]; + + // lock + OSSpinLockLock(&_lock); + } + } + keyAnimationDict[key] = anim; + + // create entry after potential removal + POPAnimatorItemRef item(new POPAnimatorItem(obj, key, anim)); + + // add to list and pending list + _list.push_back(item); + _pendingList.push_back(item); + + // support animation re-use, reset all animation state + POPAnimationGetState(anim)->reset(true); + + // update display link + updateDisplayLink(self); + + // unlock + OSSpinLockUnlock(&_lock); + + // schedule runloop processing of pending animations + [self _scheduleProcessPendingList]; +} + +- (void)removeAllAnimationsForObject:(id)obj +{ + // lock + OSSpinLockLock(&_lock); + + NSArray *animations = [(__bridge id)CFDictionaryGetValue(_dict, (__bridge void *)obj) allValues]; + CFDictionaryRemoveValue(_dict, (__bridge void *)obj); + + // unlock + OSSpinLockUnlock(&_lock); + + if (0 == animations.count) { + return; + } + + NSHashTable *animationSet = [[NSHashTable alloc] initWithOptions:NSHashTableObjectPointerPersonality capacity:animations.count]; + for (id animation in animations) { + [animationSet addObject:animation]; + } + + // lock + OSSpinLockLock(&_lock); + + POPAnimatorItemRef item; + for (auto iter = _list.begin(); iter != _list.end();) { + item = *iter; + if(![animationSet containsObject:item->animation]) { + iter++; + } else { + iter = _list.erase(iter); + } + } + + // unlock + OSSpinLockUnlock(&_lock); + + for (POPAnimation *anim in animations) { + POPAnimationState *state = POPAnimationGetState(anim); + state->stop(true, !state->active); + } +} + +- (void)removeAnimationForObject:(id)obj key:(NSString *)key cleanupDict:(BOOL)cleanupDict +{ + POPAnimation *anim = deleteDictEntry(self, obj, key, cleanupDict); + if (nil == anim) { + return; + } + + // lock + OSSpinLockLock(&_lock); + + // remove from list + POPAnimatorItemRef item; + for (auto iter = _list.begin(); iter != _list.end();) { + item = *iter; + if(anim == item->animation) { + _list.erase(iter); + break; + } else { + iter++; + } + } + + // remove from pending list + for (auto iter = _pendingList.begin(); iter != _pendingList.end();) { + item = *iter; + if(anim == item->animation) { + _pendingList.erase(iter); + break; + } else { + iter++; + } + } + + // unlock + OSSpinLockUnlock(&_lock); + + // stop animation and callout + POPAnimationState *state = POPAnimationGetState(anim); + state->stop(true, (!state->active && !state->paused)); +} + +- (void)removeAnimationForObject:(id)obj key:(NSString *)key +{ + [self removeAnimationForObject:obj key:key cleanupDict:YES]; +} + +- (NSArray *)animationKeysForObject:(id)obj +{ + // lock + OSSpinLockLock(&_lock); + + // get keys + NSArray *keys = [(__bridge id)CFDictionaryGetValue(_dict, (__bridge void *)obj) allKeys]; + + // unlock + OSSpinLockUnlock(&_lock); + return keys; +} + +- (id)animationForObject:(id)obj key:(NSString *)key +{ + // lock + OSSpinLockLock(&_lock); + + // lookup animation + NSDictionary *keyAnimationsDict = (__bridge id)CFDictionaryGetValue(_dict, (__bridge void *)obj); + POPAnimation *animation = keyAnimationsDict[key]; + + // unlock + OSSpinLockUnlock(&_lock); + return animation; +} + +- (CFTimeInterval)_currentRenderTime +{ + CFTimeInterval time = CACurrentMediaTime(); + +#if TARGET_IPHONE_SIMULATOR + // support slow-motion animations + time += _slowMotionAccumulator; + float f = POPAnimationDragCoefficient(); + + if (f > 1.0) { + if (!_slowMotionStartTime) { + _slowMotionStartTime = time; + } else { + time = (time - _slowMotionStartTime) / f + _slowMotionStartTime; + _slowMotionLastTime = time; + } + } else if (_slowMotionStartTime) { + CFTimeInterval dt = (_slowMotionLastTime - time); + time += dt; + _slowMotionAccumulator += dt; + _slowMotionStartTime = 0; + } +#endif + + return time; +} + +- (void)render +{ + CFTimeInterval time = [self _currentRenderTime]; + [self renderTime:time]; +} + +- (void)renderTime:(CFTimeInterval)time +{ + [self _renderTime:time items:_list]; +} + +- (void)addObserver:(id)observer +{ + NSAssert(nil != observer, @"attempting to add nil %@ observer", self); + if (nil == observer) { + return; + } + + // lock + OSSpinLockLock(&_lock); + + if (!_observers) { + // use ordered collection for deterministic callout + _observers = [[NSMutableArray alloc] initWithCapacity:1]; + } + + [_observers addObject:observer]; + updateDisplayLink(self); + + // unlock + OSSpinLockUnlock(&_lock); +} + +- (void)removeObserver:(id)observer +{ + NSAssert(nil != observer, @"attempting to remove nil %@ observer", self); + if (nil == observer) { + return; + } + + // lock + OSSpinLockLock(&_lock); + + [_observers removeObject:observer]; + updateDisplayLink(self); + + // unlock + OSSpinLockUnlock(&_lock); +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/POPAnimatorPrivate.h b/submodules/LegacyComponents/LegacyComponents/POPAnimatorPrivate.h new file mode 100644 index 0000000000..57d9ae978f --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/POPAnimatorPrivate.h @@ -0,0 +1,69 @@ +/** + Copyright (c) 2014-present, Facebook, Inc. + All rights reserved. + + This source code is licensed under the BSD-style license found in the + LICENSE file in the root directory of this source tree. An additional grant + of patent rights can be found in the PATENTS file in the same directory. + */ + +#import +#import "POPAnimator.h" + +@class POPAnimation; + +@protocol POPAnimatorObserving +@required + +/** + @abstract Called on each observer after animator has advanced. Core Animation actions are disabled by default. + */ +- (void)animatorDidAnimate:(POPAnimator *)animator; + +@end + +@interface POPAnimator () + +#if !TARGET_OS_IPHONE +/** + Determines whether or not to use a high priority background thread for animation updates. Using a background thread can result in faster, more responsive updates, but may be less compatible. Defaults to YES. + */ ++ (BOOL)disableBackgroundThread; ++ (void)setDisableBackgroundThread:(BOOL)flag; +#endif + +/** + Used for externally driven animator instances. + */ +@property (assign, nonatomic) BOOL disableDisplayLink; + +/** + Time used when starting animations. Defaults to 0 meaning current media time is used. Exposed for unit testing. + */ +@property (assign, nonatomic) CFTimeInterval beginTime; + +/** + Exposed for unit testing. + */ +- (void)renderTime:(CFTimeInterval)time; + +/** + Funnel methods for category additions. + */ +- (void)addAnimation:(POPAnimation *)anim forObject:(id)obj key:(NSString *)key; +- (void)removeAllAnimationsForObject:(id)obj; +- (void)removeAnimationForObject:(id)obj key:(NSString *)key; +- (NSArray *)animationKeysForObject:(id)obj; +- (POPAnimation *)animationForObject:(id)obj key:(NSString *)key; + +/** + @abstract Add an animator observer. Observer will be notified of each subsequent animator advance until removal. + */ +- (void)addObserver:(id)observer; + +/** + @abstract Remove an animator observer. + */ +- (void)removeObserver:(id)observer; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/POPBasicAnimation.h b/submodules/LegacyComponents/LegacyComponents/POPBasicAnimation.h new file mode 100644 index 0000000000..1f95c31b90 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/POPBasicAnimation.h @@ -0,0 +1,71 @@ +/** + Copyright (c) 2014-present, Facebook, Inc. + All rights reserved. + + This source code is licensed under the BSD-style license found in the + LICENSE file in the root directory of this source tree. An additional grant + of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +/** + @abstract A concrete basic animation class. + @discussion Animation is achieved through interpolation. + */ +@interface POPBasicAnimation : POPPropertyAnimation + +/** + @abstract The designated initializer. + @returns An instance of a basic animation. + */ ++ (instancetype)animation; + +/** + @abstract Convenience initializer that returns an animation with animatable property of name. + @param name The name of the animatable property. + @returns An instance of a basic animation configured with specified animatable property. + */ ++ (instancetype)animationWithPropertyNamed:(NSString *)name; + +/** + @abstract Convenience constructor. + @returns Returns a basic animation with kCAMediaTimingFunctionDefault timing function. + */ ++ (instancetype)defaultAnimation; + +/** + @abstract Convenience constructor. + @returns Returns a basic animation with kCAMediaTimingFunctionLinear timing function. + */ ++ (instancetype)linearAnimation; + +/** + @abstract Convenience constructor. + @returns Returns a basic animation with kCAMediaTimingFunctionEaseIn timing function. + */ ++ (instancetype)easeInAnimation; + +/** + @abstract Convenience constructor. + @returns Returns a basic animation with kCAMediaTimingFunctionEaseOut timing function. + */ ++ (instancetype)easeOutAnimation; + +/** + @abstract Convenience constructor. + @returns Returns a basic animation with kCAMediaTimingFunctionEaseInEaseOut timing function. + */ ++ (instancetype)easeInEaseOutAnimation; + +/** + @abstract The duration in seconds. Defaults to 0.4. + */ +@property (assign, nonatomic) CFTimeInterval duration; + +/** + @abstract A timing function defining the pacing of the animation. Defaults to nil indicating pacing according to kCAMediaTimingFunctionDefault. + */ +@property (strong, nonatomic) CAMediaTimingFunction *timingFunction; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/POPBasicAnimation.mm b/submodules/LegacyComponents/LegacyComponents/POPBasicAnimation.mm new file mode 100644 index 0000000000..f53bba1401 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/POPBasicAnimation.mm @@ -0,0 +1,90 @@ +/** + Copyright (c) 2014-present, Facebook, Inc. + All rights reserved. + + This source code is licensed under the BSD-style license found in the + LICENSE file in the root directory of this source tree. An additional grant + of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "POPBasicAnimationInternal.h" + +@implementation POPBasicAnimation + +#undef __state +#define __state ((POPBasicAnimationState *)_state) + +#pragma mark - Lifecycle + ++ (instancetype)animation +{ + return [[self alloc] init]; +} + ++ (instancetype)animationWithPropertyNamed:(NSString *)aName +{ + POPBasicAnimation *anim = [self animation]; + anim.property = [POPAnimatableProperty propertyWithName:aName]; + return anim; +} + +- (void)_initState +{ + _state = new POPBasicAnimationState(self); +} + ++ (instancetype)linearAnimation +{ + POPBasicAnimation *anim = [self animation]; + anim.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear]; + return anim; +} + ++ (instancetype)easeInAnimation +{ + POPBasicAnimation *anim = [self animation]; + anim.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn]; + return anim; +} + ++ (instancetype)easeOutAnimation +{ + POPBasicAnimation *anim = [self animation]; + anim.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]; + return anim; +} + ++ (instancetype)easeInEaseOutAnimation +{ + POPBasicAnimation *anim = [self animation]; + anim.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; + return anim; +} + ++ (instancetype)defaultAnimation +{ + POPBasicAnimation *anim = [self animation]; + anim.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionDefault]; + return anim; +} + +- (id)init +{ + return [self _init]; +} + +#pragma mark - Properties + +DEFINE_RW_PROPERTY(POPBasicAnimationState, duration, setDuration:, CFTimeInterval); +DEFINE_RW_PROPERTY_OBJ(POPBasicAnimationState, timingFunction, setTimingFunction:, CAMediaTimingFunction*, __state->updatedTimingFunction();); + +#pragma mark - Utility + +- (void)_appendDescription:(NSMutableString *)s debug:(BOOL)debug +{ + [super _appendDescription:s debug:debug]; + if (__state->duration) + [s appendFormat:@"; duration = %f", __state->duration]; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/POPBasicAnimationInternal.h b/submodules/LegacyComponents/LegacyComponents/POPBasicAnimationInternal.h new file mode 100644 index 0000000000..e274c3f2f4 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/POPBasicAnimationInternal.h @@ -0,0 +1,96 @@ +/** + Copyright (c) 2014-present, Facebook, Inc. + All rights reserved. + + This source code is licensed under the BSD-style license found in the + LICENSE file in the root directory of this source tree. An additional grant + of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "POPBasicAnimation.h" +#import "POPPropertyAnimationInternal.h" + +// default animation duration +static CGFloat const kPOPAnimationDurationDefault = 0.4; + +// progress threshold for computing done +static CGFloat const kPOPProgressThreshold = 1e-6; + +static void interpolate(POPValueType valueType, NSUInteger count, const CGFloat *fromVec, const CGFloat *toVec, CGFloat *outVec, CGFloat p) +{ + switch (valueType) { + case kPOPValueInteger: + case kPOPValueFloat: + case kPOPValuePoint: + case kPOPValueSize: + case kPOPValueRect: + case kPOPValueEdgeInsets: + case kPOPValueColor: + POPInterpolateVector(count, outVec, fromVec, toVec, p); + break; + default: + NSCAssert(false, @"unhandled type %d", valueType); + break; + } +} + +struct _POPBasicAnimationState : _POPPropertyAnimationState +{ + CAMediaTimingFunction *timingFunction; + double timingControlPoints[4]; + CFTimeInterval duration; + CFTimeInterval timeProgress; + + _POPBasicAnimationState(id __unsafe_unretained anim) : _POPPropertyAnimationState(anim), + timingFunction(nil), + timingControlPoints{0.}, + duration(kPOPAnimationDurationDefault), + timeProgress(0.) + { + type = kPOPAnimationBasic; + } + + bool isDone() { + if (_POPPropertyAnimationState::isDone()) { + return true; + } + return timeProgress + kPOPProgressThreshold >= 1.; + } + + void updatedTimingFunction() + { + float vec[4] = {0.}; + [timingFunction getControlPointAtIndex:1 values:&vec[0]]; + [timingFunction getControlPointAtIndex:2 values:&vec[2]]; + for (NSUInteger idx = 0; idx < POP_ARRAY_COUNT(vec); idx++) { + timingControlPoints[idx] = vec[idx]; + } + } + + bool advance(CFTimeInterval time, CFTimeInterval dt, id obj) { + // default timing function + if (!timingFunction) { + ((POPBasicAnimation *)self).timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionDefault]; + } + + // solve for normalized time, aka progresss [0, 1] + CGFloat p = 1.0f; + if (duration > 0.0f) { + // cap local time to duration + CFTimeInterval t = MIN(time - startTime, duration) / duration; + p = POPTimingFunctionSolve(timingControlPoints, t, SOLVE_EPS(duration)); + timeProgress = t; + } else { + timeProgress = 1.; + } + + // interpolate and advance + interpolate(valueType, valueCount, fromVec->data(), toVec->data(), currentVec->data(), p); + progress = p; + clampCurrentValue(); + + return true; + } +}; + +typedef struct _POPBasicAnimationState POPBasicAnimationState; diff --git a/submodules/LegacyComponents/LegacyComponents/POPCGUtils.h b/submodules/LegacyComponents/LegacyComponents/POPCGUtils.h new file mode 100644 index 0000000000..a170750d07 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/POPCGUtils.h @@ -0,0 +1,140 @@ +/** + Copyright (c) 2014-present, Facebook, Inc. + All rights reserved. + + This source code is licensed under the BSD-style license found in the + LICENSE file in the root directory of this source tree. An additional grant + of patent rights can be found in the PATENTS file in the same directory. + */ + +#import +#if TARGET_OS_IPHONE +#import +#endif +#import "POPDefines.h" + +#if SCENEKIT_SDK_AVAILABLE +#import +#endif + +#if TARGET_OS_IPHONE +@class UIColor; +#endif + +POP_EXTERN_C_BEGIN + +NS_INLINE CGPoint values_to_point(const CGFloat values[]) +{ + return CGPointMake(values[0], values[1]); +} + +NS_INLINE CGSize values_to_size(const CGFloat values[]) +{ + return CGSizeMake(values[0], values[1]); +} + +NS_INLINE CGRect values_to_rect(const CGFloat values[]) +{ + return CGRectMake(values[0], values[1], values[2], values[3]); +} + +#if SCENEKIT_SDK_AVAILABLE +NS_INLINE SCNVector3 values_to_vec3(const CGFloat values[]) +{ + return SCNVector3Make(values[0], values[1], values[2]); +} + +NS_INLINE SCNVector4 values_to_vec4(const CGFloat values[]) +{ + return SCNVector4Make(values[0], values[1], values[2], values[3]); +} +#endif + +#if TARGET_OS_IPHONE + +NS_INLINE UIEdgeInsets values_to_edge_insets(const CGFloat values[]) +{ + return UIEdgeInsetsMake(values[0], values[1], values[2], values[3]); +} + +#endif + +NS_INLINE void values_from_point(CGFloat values[], CGPoint p) +{ + values[0] = p.x; + values[1] = p.y; +} + +NS_INLINE void values_from_size(CGFloat values[], CGSize s) +{ + values[0] = s.width; + values[1] = s.height; +} + +NS_INLINE void values_from_rect(CGFloat values[], CGRect r) +{ + values[0] = r.origin.x; + values[1] = r.origin.y; + values[2] = r.size.width; + values[3] = r.size.height; +} + +#if SCENEKIT_SDK_AVAILABLE +NS_INLINE void values_from_vec3(CGFloat values[], SCNVector3 v) +{ + values[0] = v.x; + values[1] = v.y; + values[2] = v.z; +} + +NS_INLINE void values_from_vec4(CGFloat values[], SCNVector4 v) +{ + values[0] = v.x; + values[1] = v.y; + values[2] = v.z; + values[3] = v.w; +} +#endif + +#if TARGET_OS_IPHONE + +NS_INLINE void values_from_edge_insets(CGFloat values[], UIEdgeInsets i) +{ + values[0] = i.top; + values[1] = i.left; + values[2] = i.bottom; + values[3] = i.right; +} + +#endif + +/** + Takes a CGColorRef and converts it into RGBA components, if necessary. + */ +extern void POPCGColorGetRGBAComponents(CGColorRef color, CGFloat components[]); + +/** + Takes RGBA components and returns a CGColorRef. + */ +extern CGColorRef POPCGColorRGBACreate(const CGFloat components[]) CF_RETURNS_RETAINED; + +/** + Takes a color reference and returns a CGColor. + */ +extern CGColorRef POPCGColorWithColor(id color); + +#if TARGET_OS_IPHONE + +/** + Takes a UIColor and converts it into RGBA components, if necessary. + */ +extern void POPUIColorGetRGBAComponents(UIColor *color, CGFloat components[]); + +/** + Takes RGBA components and returns a UIColor. + */ +extern UIColor *POPUIColorRGBACreate(const CGFloat components[]) NS_RETURNS_RETAINED; + +#endif + +POP_EXTERN_C_END diff --git a/submodules/LegacyComponents/LegacyComponents/POPCGUtils.mm b/submodules/LegacyComponents/LegacyComponents/POPCGUtils.mm new file mode 100644 index 0000000000..c8a866a442 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/POPCGUtils.mm @@ -0,0 +1,96 @@ +/** + Copyright (c) 2014-present, Facebook, Inc. + All rights reserved. + + This source code is licensed under the BSD-style license found in the + LICENSE file in the root directory of this source tree. An additional grant + of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "POPCGUtils.h" + +#if TARGET_OS_IPHONE +#import +#else +#import +#endif + +void POPCGColorGetRGBAComponents(CGColorRef color, CGFloat components[]) +{ + if (!color) { +#if TARGET_OS_IPHONE + color = [UIColor clearColor].CGColor; +#else + color = [NSColor clearColor].CGColor; +#endif + } + + const CGFloat *colors = CGColorGetComponents(color); + size_t count = CGColorGetNumberOfComponents(color); + + if (4 == count) { + // RGB colorspace + components[0] = colors[0]; + components[1] = colors[1]; + components[2] = colors[2]; + components[3] = colors[3]; + } else if (2 == count) { + // Grey colorspace + components[0] = components[1] = components[2] = colors[0]; + components[3] = colors[1]; + } else { + // Use CI to convert + CIColor *ciColor = [CIColor colorWithCGColor:color]; + components[0] = ciColor.red; + components[1] = ciColor.green; + components[2] = ciColor.blue; + components[3] = ciColor.alpha; + } +} + +CGColorRef POPCGColorRGBACreate(const CGFloat components[]) +{ +#if TARGET_OS_IPHONE + CGColorSpaceRef space = CGColorSpaceCreateDeviceRGB(); + CGColorRef color = CGColorCreate(space, components); + CGColorSpaceRelease(space); + return color; +#else + return CGColorCreateGenericRGB(components[0], components[1], components[2], components[3]); +#endif +} + +CGColorRef POPCGColorWithColor(id color) +{ + if (CFGetTypeID((__bridge CFTypeRef)color) == CGColorGetTypeID()) { + return ((__bridge CGColorRef)color); + } +#if TARGET_OS_IPHONE + else if ([color isKindOfClass:[UIColor class]]) { + return [color CGColor]; + } +#else + else if ([color isKindOfClass:[NSColor class]]) { + return [color CGColor]; + } +#endif + return nil; +} + +#if TARGET_OS_IPHONE + +void POPUIColorGetRGBAComponents(UIColor *color, CGFloat components[]) +{ + return POPCGColorGetRGBAComponents(color.CGColor, components); +} + +UIColor *POPUIColorRGBACreate(const CGFloat components[]) +{ + CGColorRef colorRef = POPCGColorRGBACreate(components); + UIColor *color = [[UIColor alloc] initWithCGColor:colorRef]; + CGColorRelease(colorRef); + return color; +} + +#endif + diff --git a/submodules/LegacyComponents/LegacyComponents/POPCustomAnimation.h b/submodules/LegacyComponents/LegacyComponents/POPCustomAnimation.h new file mode 100644 index 0000000000..090eb3c4bb --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/POPCustomAnimation.h @@ -0,0 +1,46 @@ +/** + Copyright (c) 2014-present, Facebook, Inc. + All rights reserved. + + This source code is licensed under the BSD-style license found in the + LICENSE file in the root directory of this source tree. An additional grant + of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +@class POPCustomAnimation; + +/** + @abstract POPCustomAnimationBlock is the callback block of a custom animation. + @discussion This block will be executed for each animation frame and should update the property or properties being animated based on current timing. + @param target The object being animated. Reference the passed in target to help avoid retain loops. + @param animation The custom animation instance. Use to determine the current and elapsed time since last callback. Reference the passed in animation to help avoid retain loops. + @return Flag indicating whether the animation should continue animating. Return NO to indicate animation is done. + */ +typedef BOOL (^POPCustomAnimationBlock)(id target, POPCustomAnimation *animation); + +/** + @abstract POPCustomAnimation is a concrete animation subclass for custom animations. + */ +@interface POPCustomAnimation : POPAnimation + +/** +@abstract Creates and returns an initialized custom animation instance. +@discussion This is the designated initializer. +@param block The custom animation callback block. See {@ref POPCustomAnimationBlock}. +@return The initialized custom animation instance. +*/ ++ (instancetype)animationWithBlock:(POPCustomAnimationBlock)block; + +/** + @abstract The current animation time at time of callback. + */ +@property (readonly, nonatomic) CFTimeInterval currentTime; + +/** + @abstract The elapsed animation time since last callback. + */ +@property (readonly, nonatomic) CFTimeInterval elapsedTime; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/POPCustomAnimation.mm b/submodules/LegacyComponents/LegacyComponents/POPCustomAnimation.mm new file mode 100644 index 0000000000..20e361ac08 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/POPCustomAnimation.mm @@ -0,0 +1,56 @@ +/** + Copyright (c) 2014-present, Facebook, Inc. + All rights reserved. + + This source code is licensed under the BSD-style license found in the + LICENSE file in the root directory of this source tree. An additional grant + of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "POPCustomAnimation.h" +#import "POPAnimationInternal.h" + +@interface POPCustomAnimation () +@property (nonatomic, copy) POPCustomAnimationBlock animate; +@end + +@implementation POPCustomAnimation +@synthesize currentTime = _currentTime; +@synthesize elapsedTime = _elapsedTime; +@synthesize animate = _animate; + ++ (instancetype)animationWithBlock:(BOOL(^)(id target, POPCustomAnimation *))block +{ + POPCustomAnimation *b = [[self alloc] _init]; + b.animate = block; + return b; +} + +- (id)_init +{ + self = [super _init]; + if (nil != self) { + _state->type = kPOPAnimationCustom; + } + return self; +} + +- (CFTimeInterval)beginTime +{ + POPAnimationState *s = POPAnimationGetState(self); + return s->startTime > 0 ? s->startTime : s->beginTime; +} + +- (BOOL)_advance:(id)object currentTime:(CFTimeInterval)currentTime elapsedTime:(CFTimeInterval)elapsedTime +{ + _currentTime = currentTime; + _elapsedTime = elapsedTime; + return _animate(object, self); +} + +- (void)_appendDescription:(NSMutableString *)s debug:(BOOL)debug +{ + [s appendFormat:@"; elapsedTime = %f; currentTime = %f;", _elapsedTime, _currentTime]; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/POPDecayAnimation.h b/submodules/LegacyComponents/LegacyComponents/POPDecayAnimation.h new file mode 100644 index 0000000000..49ae39ef8d --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/POPDecayAnimation.h @@ -0,0 +1,66 @@ +/** + Copyright (c) 2014-present, Facebook, Inc. + All rights reserved. + + This source code is licensed under the BSD-style license found in the + LICENSE file in the root directory of this source tree. An additional grant + of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +/** + @abstract A concrete decay animation class. + @discussion Animation is achieved through gradual decay of animation value. + */ +@interface POPDecayAnimation : POPPropertyAnimation + +/** + @abstract The designated initializer. + @returns An instance of a decay animation. + */ ++ (instancetype)animation; + +/** + @abstract Convenience initializer that returns an animation with animatable property of name. + @param name The name of the animatable property. + @returns An instance of a decay animation configured with specified animatable property. + */ ++ (instancetype)animationWithPropertyNamed:(NSString *)name; + +/** + @abstract The current velocity value. + @discussion Set before animation start to account for initial velocity. Expressed in change of value units per second. The only POPValueTypes supported for velocity are: kPOPValuePoint, kPOPValueInteger, kPOPValueFloat, kPOPValueRect, and kPOPValueSize. + */ +@property (copy, nonatomic) id velocity; + +/** + @abstract The original velocity value. + @discussion Since the velocity property is modified as the animation progresses, this property stores the original, passed in velocity to support autoreverse and repeatCount. + */ +@property (copy, nonatomic, readonly) id originalVelocity; + +/** + @abstract The deceleration factor. + @discussion Values specifies should be in the range [0, 1]. Lower values results in faster deceleration. Defaults to 0.998. + */ +@property (assign, nonatomic) CGFloat deceleration; + +/** + @abstract The expected duration. + @discussion Derived based on input velocity and deceleration values. + */ +@property (readonly, assign, nonatomic) CFTimeInterval duration; + +/** + The to value is derived based on input velocity and deceleration. + */ +- (void)setToValue:(id)toValue NS_UNAVAILABLE; + +/** + @abstract The reversed velocity. + @discussion The reversed velocity based on the originalVelocity when the animation was set up. + */ +- (id)reversedVelocity; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/POPDecayAnimation.mm b/submodules/LegacyComponents/LegacyComponents/POPDecayAnimation.mm new file mode 100644 index 0000000000..405c529076 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/POPDecayAnimation.mm @@ -0,0 +1,181 @@ +/** + Copyright (c) 2014-present, Facebook, Inc. + All rights reserved. + + This source code is licensed under the BSD-style license found in the + LICENSE file in the root directory of this source tree. An additional grant + of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "POPDecayAnimationInternal.h" + +#if TARGET_OS_IPHONE +#import +#endif + +const POPValueType supportedVelocityTypes[6] = { kPOPValuePoint, kPOPValueInteger, kPOPValueFloat, kPOPValueRect, kPOPValueSize, kPOPValueEdgeInsets }; + +@implementation POPDecayAnimation + +#pragma mark - Lifecycle + +#undef __state +#define __state ((POPDecayAnimationState *)_state) + ++ (instancetype)animation +{ + return [[self alloc] init]; +} + ++ (instancetype)animationWithPropertyNamed:(NSString *)aName +{ + POPDecayAnimation *anim = [self animation]; + anim.property = [POPAnimatableProperty propertyWithName:aName]; + return anim; +} + +- (id)init +{ + return [self _init]; +} + +- (void)_initState +{ + _state = new POPDecayAnimationState(self); +} + +#pragma mark - Properties + +DEFINE_RW_PROPERTY(POPDecayAnimationState, deceleration, setDeceleration:, CGFloat, __state->toVec = NULL;); + +@dynamic velocity; + +- (id)toValue +{ + [self _ensureComputedProperties]; + return POPBox(__state->toVec, __state->valueType); +} + +- (CFTimeInterval)duration +{ + [self _ensureComputedProperties]; + return __state->duration; +} + +- (void)setFromValue:(id)fromValue +{ + super.fromValue = fromValue; + [self _invalidateComputedProperties]; +} + +- (void)setToValue:(id)aValue +{ + // no-op + NSLog(@"ignoring to value on decay animation %@", self); +} + +- (id)reversedVelocity +{ + id reversedVelocity = nil; + + POPValueType velocityType = POPSelectValueType(self.originalVelocity, supportedVelocityTypes, POP_ARRAY_COUNT(supportedVelocityTypes)); + if (velocityType == kPOPValueFloat) { + CGFloat originalVelocityFloat = [(NSNumber *)self.originalVelocity floatValue]; + NSNumber *negativeOriginalVelocityNumber = @(-originalVelocityFloat); + reversedVelocity = negativeOriginalVelocityNumber; + } else if (velocityType == kPOPValueInteger) { + NSInteger originalVelocityInteger = [(NSNumber *)self.originalVelocity integerValue]; + NSNumber *negativeOriginalVelocityNumber = @(-originalVelocityInteger); + reversedVelocity = negativeOriginalVelocityNumber; + } else if (velocityType == kPOPValuePoint) { + CGPoint originalVelocityPoint = [self.originalVelocity CGPointValue]; + CGPoint negativeOriginalVelocityPoint = CGPointMake(-originalVelocityPoint.x, -originalVelocityPoint.y); + reversedVelocity = [NSValue valueWithCGPoint:negativeOriginalVelocityPoint]; + } else if (velocityType == kPOPValueRect) { + CGRect originalVelocityRect = [self.originalVelocity CGRectValue]; + CGRect negativeOriginalVelocityRect = CGRectMake(-originalVelocityRect.origin.x, -originalVelocityRect.origin.y, -originalVelocityRect.size.width, -originalVelocityRect.size.height); + reversedVelocity = [NSValue valueWithCGRect:negativeOriginalVelocityRect]; + } else if (velocityType == kPOPValueSize) { + CGSize originalVelocitySize = [self.originalVelocity CGSizeValue]; + CGSize negativeOriginalVelocitySize = CGSizeMake(-originalVelocitySize.width, -originalVelocitySize.height); + reversedVelocity = [NSValue valueWithCGSize:negativeOriginalVelocitySize]; + } else if (velocityType == kPOPValueEdgeInsets) { +#if TARGET_OS_IPHONE + UIEdgeInsets originalVelocityInsets = [self.originalVelocity UIEdgeInsetsValue]; + UIEdgeInsets negativeOriginalVelocityInsets = UIEdgeInsetsMake(-originalVelocityInsets.top, -originalVelocityInsets.left, -originalVelocityInsets.bottom, -originalVelocityInsets.right); + reversedVelocity = [NSValue valueWithUIEdgeInsets:negativeOriginalVelocityInsets]; +#endif + } + + return reversedVelocity; +} + +- (id)originalVelocity +{ + return POPBox(__state->originalVelocityVec, __state->valueType); +} + +- (id)velocity +{ + return POPBox(__state->velocityVec, __state->valueType); +} + +- (void)setVelocity:(id)aValue +{ + POPValueType valueType = POPSelectValueType(aValue, supportedVelocityTypes, POP_ARRAY_COUNT(supportedVelocityTypes)); + if (valueType != kPOPValueUnknown) { + VectorRef vec = POPUnbox(aValue, __state->valueType, __state->valueCount, YES); + VectorRef origVec = POPUnbox(aValue, __state->valueType, __state->valueCount, YES); + + if (!vec_equal(vec, __state->velocityVec)) { + __state->velocityVec = vec; + __state->originalVelocityVec = origVec; + + if (__state->tracing) { + [__state->tracer updateVelocity:aValue]; + } + + [self _invalidateComputedProperties]; + + // automatically unpause active animations + if (__state->active && __state->paused) { + __state->fromVec = NULL; + __state->setPaused(false); + } + } + } else { + __state->velocityVec = NULL; + NSLog(@"Invalid velocity value for the decayAnimation: %@", aValue); + } +} + +#pragma mark - Utility + +- (void)_ensureComputedProperties +{ + if (NULL == __state->toVec) { + __state->computeDuration(); + __state->computeToValue(); + } +} + +- (void)_invalidateComputedProperties +{ + __state->toVec = NULL; + __state->duration = 0; +} + +- (void)_appendDescription:(NSMutableString *)s debug:(BOOL)debug +{ + [super _appendDescription:s debug:debug]; + + if (0 != self.duration) { + [s appendFormat:@"; duration = %f", self.duration]; + } + + if (__state->deceleration) { + [s appendFormat:@"; deceleration = %f", __state->deceleration]; + } +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/POPDecayAnimationInternal.h b/submodules/LegacyComponents/LegacyComponents/POPDecayAnimationInternal.h new file mode 100644 index 0000000000..c0969b5a21 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/POPDecayAnimationInternal.h @@ -0,0 +1,158 @@ +/** + Copyright (c) 2014-present, Facebook, Inc. + All rights reserved. + + This source code is licensed under the BSD-style license found in the + LICENSE file in the root directory of this source tree. An additional grant + of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "POPDecayAnimation.h" +#import "POPPropertyAnimationInternal.h" + +// minimal velocity factor before decay animation is considered complete, in units / s +static CGFloat kPOPAnimationDecayMinimalVelocityFactor = 5.; + +// default decay animation deceleration +static CGFloat kPOPAnimationDecayDecelerationDefault = 0.998; + +static void decay_position(CGFloat *x, CGFloat *v, NSUInteger count, CFTimeInterval dt, CGFloat deceleration) +{ + dt *= 1000; + + // v0 = v / 1000 + // v = v0 * powf(deceleration, dt); + // v = v * 1000; + + // x0 = x; + // x = x0 + v0 * deceleration * (1 - powf(deceleration, dt)) / (1 - deceleration) + float v0[count]; + float kv = powf(deceleration, dt); + float kx = deceleration * (1 - kv) / (1 - deceleration); + + for (NSUInteger idx = 0; idx < count; idx++) { + v0[idx] = v[idx] / 1000.; + v[idx] = v0[idx] * kv * 1000.; + x[idx] = x[idx] + v0[idx] * kx; + } +} + +struct _POPDecayAnimationState : _POPPropertyAnimationState +{ + double deceleration; + CFTimeInterval duration; + + _POPDecayAnimationState(id __unsafe_unretained anim) : + _POPPropertyAnimationState(anim), + deceleration(kPOPAnimationDecayDecelerationDefault), + duration(0) + { + type = kPOPAnimationDecay; + } + + bool isDone() { + if (_POPPropertyAnimationState::isDone()) { + return true; + } + + CGFloat f = dynamicsThreshold * kPOPAnimationDecayMinimalVelocityFactor; + const CGFloat *velocityValues = vec_data(velocityVec); + for (NSUInteger idx = 0; idx < valueCount; idx++) { + if (fabsf((float)velocityValues[idx]) >= f) + return false; + } + return true; + + } + + void computeDuration() { + + // compute duration till threshold velocity + Vector4r scaledVelocity = vector4(velocityVec) / 1000.; + + double k = dynamicsThreshold * kPOPAnimationDecayMinimalVelocityFactor / 1000.; + double vx = k / scaledVelocity.x; + double vy = k / scaledVelocity.y; + double vz = k / scaledVelocity.z; + double vw = k / scaledVelocity.w; + double d = log(deceleration) * 1000.; + duration = MAX(MAX(MAX(log(fabs(vx)) / d, log(fabs(vy)) / d), log(fabs(vz)) / d), log(fabs(vw)) / d); + + // ensure velocity threshold is exceeded + if (isnan(duration) || duration < 0) { + duration = 0; + } + } + + void computeToValue() { + // to value assuming final velocity as a factor of dynamics threshold + // derived from v' = v * d^dt used in decay_position + // to compute the to value with maximal dt, p' = p + (v * d) / (1 - d) + VectorRef fromValue = NULL != currentVec ? currentVec : fromVec; + if (!fromValue) { + return; + } + + // ensure duration is computed + if (0 == duration) { + computeDuration(); + } + + // compute to value + VectorRef toValue(Vector::new_vector(fromValue.get())); + Vector4r velocity = velocityVec->vector4r(); + decay_position(toValue->data(), velocity.data(), valueCount, duration, deceleration); + toVec = toValue; + } + + void computeDestinationValues() { + // to value assuming final velocity as a factor of dynamics threshold + // derived from v' = v * d^dt used in decay_position + // to compute the to value with maximal dt, p' = p + (v * d) / (1 - d) + VectorRef fromValue = NULL != currentVec ? currentVec : fromVec; + if (!fromValue) { + return; + } + + VectorRef toValue(Vector::new_vector(fromValue.get())); + + // compute duration till threshold velocity + Vector4r scaledVelocity = vector4(velocityVec) / 1000.; + + double k = dynamicsThreshold * kPOPAnimationDecayMinimalVelocityFactor / 1000.; + double vx = k / scaledVelocity.x; + double vy = k / scaledVelocity.y; + double vz = k / scaledVelocity.z; + double vw = k / scaledVelocity.w; + double d = log(deceleration) * 1000.; + duration = MAX(MAX(MAX(log(fabs(vx)) / d, log(fabs(vy)) / d), log(fabs(vz)) / d), log(fabs(vw)) / d); + + // ensure velocity threshold is exceeded + if (isnan(duration) || duration < 0) { + duration = 0; + } else { + // compute to value + Vector4r velocity = velocityVec->vector4r(); + decay_position(toValue->data(), velocity.data(), valueCount, duration, deceleration); + } + + toVec = toValue; + } + + bool advance(CFTimeInterval time, CFTimeInterval dt, id obj) { + // advance past not yet initialized animations + if (NULL == currentVec) { + return false; + } + + decay_position(currentVec->data(), velocityVec->data(), valueCount, dt, deceleration); + + // clamp to compute end value; avoid possibility of decaying past + clampCurrentValue(kPOPAnimationClampEnd | clampMode); + + return true; + } + +}; + +typedef struct _POPDecayAnimationState POPDecayAnimationState; diff --git a/submodules/LegacyComponents/LegacyComponents/POPDefines.h b/submodules/LegacyComponents/LegacyComponents/POPDefines.h new file mode 100644 index 0000000000..eb2878141d --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/POPDefines.h @@ -0,0 +1,37 @@ +/** + Copyright (c) 2014-present, Facebook, Inc. + All rights reserved. + + This source code is licensed under the BSD-style license found in the + LICENSE file in the root directory of this source tree. An additional grant + of patent rights can be found in the PATENTS file in the same directory. + */ + +#ifndef POP_POPDefines_h +#define POP_POPDefines_h + +#import + +#ifdef __cplusplus +# define POP_EXTERN_C_BEGIN extern "C" { +# define POP_EXTERN_C_END } +#else +# define POP_EXTERN_C_BEGIN +# define POP_EXTERN_C_END +#endif + +#define POP_ARRAY_COUNT(x) sizeof(x) / sizeof(x[0]) + +#if defined (__cplusplus) && defined (__GNUC__) +# define POP_NOTHROW __attribute__ ((nothrow)) +#else +# define POP_NOTHROW +#endif + +#if TARGET_OS_MAC + #define SCENEKIT_SDK_AVAILABLE defined(POP_USE_SCENEKIT) +#elif TARGET_OS_IPHONE + #define SCENEKIT_SDK_AVAILABLE defined(POP_USE_SCENEKIT) +#endif + +#endif diff --git a/submodules/LegacyComponents/LegacyComponents/POPGeometry.h b/submodules/LegacyComponents/LegacyComponents/POPGeometry.h new file mode 100644 index 0000000000..8ba07e3322 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/POPGeometry.h @@ -0,0 +1,73 @@ +/** + Copyright (c) 2014-present, Facebook, Inc. + All rights reserved. + + This source code is licensed under the BSD-style license found in the + LICENSE file in the root directory of this source tree. An additional grant + of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#if TARGET_OS_IPHONE +#import +#endif + +#if !TARGET_OS_IPHONE + +/** NSValue extensions to support animatable types. */ +@interface NSValue (POP) + +/** + @abstract Creates an NSValue given a CGPoint. + */ ++ (NSValue *)valueWithCGPoint:(CGPoint)point; + +/** + @abstract Creates an NSValue given a CGSize. + */ ++ (NSValue *)valueWithCGSize:(CGSize)size; + +/** + @abstract Creates an NSValue given a CGRect. + */ ++ (NSValue *)valueWithCGRect:(CGRect)rect; + +/** + @abstract Creates an NSValue given a CFRange. + */ ++ (NSValue *)valueWithCFRange:(CFRange)range; + +/** + @abstract Creates an NSValue given a CGAffineTransform. + */ ++ (NSValue *)valueWithCGAffineTransform:(CGAffineTransform)transform; + +/** + @abstract Returns the underlying CGPoint value. + */ +- (CGPoint)CGPointValue; + +/** + @abstract Returns the underlying CGSize value. + */ +- (CGSize)CGSizeValue; + +/** + @abstract Returns the underlying CGRect value. + */ +- (CGRect)CGRectValue; + +/** + @abstract Returns the underlying CFRange value. + */ +- (CFRange)CFRangeValue; + +/** + @abstract Returns the underlying CGAffineTransform value. + */ +- (CGAffineTransform)CGAffineTransformValue; + +@end + +#endif diff --git a/submodules/LegacyComponents/LegacyComponents/POPGeometry.mm b/submodules/LegacyComponents/LegacyComponents/POPGeometry.mm new file mode 100644 index 0000000000..41998b10b2 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/POPGeometry.mm @@ -0,0 +1,94 @@ +/** + Copyright (c) 2014-present, Facebook, Inc. + All rights reserved. + + This source code is licensed under the BSD-style license found in the + LICENSE file in the root directory of this source tree. An additional grant + of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "POPGeometry.h" + +#if !TARGET_OS_IPHONE +@implementation NSValue (POP) + ++ (NSValue *)valueWithCGPoint:(CGPoint)point { + return [NSValue valueWithBytes:&point objCType:@encode(CGPoint)]; +} + ++ (NSValue *)valueWithCGSize:(CGSize)size { + return [NSValue valueWithBytes:&size objCType:@encode(CGSize)]; +} + ++ (NSValue *)valueWithCGRect:(CGRect)rect { + return [NSValue valueWithBytes:&rect objCType:@encode(CGRect)]; +} + ++ (NSValue *)valueWithCFRange:(CFRange)range { + return [NSValue valueWithBytes:&range objCType:@encode(CFRange)]; +} + ++ (NSValue *)valueWithCGAffineTransform:(CGAffineTransform)transform +{ + return [NSValue valueWithBytes:&transform objCType:@encode(CGAffineTransform)]; +} + +- (CGPoint)CGPointValue { + CGPoint result; + [self getValue:&result]; + return result; +} + +- (CGSize)CGSizeValue { + CGSize result; + [self getValue:&result]; + return result; +} + +- (CGRect)CGRectValue { + CGRect result; + [self getValue:&result]; + return result; +} + +- (CFRange)CFRangeValue { + CFRange result; + [self getValue:&result]; + return result; +} + +- (CGAffineTransform)CGAffineTransformValue { + CGAffineTransform result; + [self getValue:&result]; + return result; +} +@end + +#endif + +#if TARGET_OS_IPHONE +#import "POPDefines.h" + +#if SCENEKIT_SDK_AVAILABLE +#import + +/** + Dirty hacks because iOS is weird and decided to define both SCNVector3's and SCNVector4's objCType as "t". However @encode(SCNVector3) and @encode(SCNVector4) both return the proper definition ("{SCNVector3=fff}" and "{SCNVector4=ffff}" respectively) + + [[NSValue valueWithSCNVector3:SCNVector3Make(0.0, 0.0, 0.0)] objcType] returns "t", whereas it should return "{SCNVector3=fff}". + + *flips table* + */ +@implementation NSValue (SceneKitFixes) + ++ (NSValue *)valueWithSCNVector3:(SCNVector3)vec3 { + return [NSValue valueWithBytes:&vec3 objCType:@encode(SCNVector3)]; +} + ++ (NSValue *)valueWithSCNVector4:(SCNVector4)vec4 { + return [NSValue valueWithBytes:&vec4 objCType:@encode(SCNVector4)]; +} + +@end +#endif +#endif diff --git a/submodules/LegacyComponents/LegacyComponents/POPLayerExtras.h b/submodules/LegacyComponents/LegacyComponents/POPLayerExtras.h new file mode 100644 index 0000000000..ff30e01731 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/POPLayerExtras.h @@ -0,0 +1,196 @@ +/** + Copyright (c) 2014-present, Facebook, Inc. + All rights reserved. + + This source code is licensed under the BSD-style license found in the + LICENSE file in the root directory of this source tree. An additional grant + of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "POPDefines.h" + +POP_EXTERN_C_BEGIN + +#pragma mark - Scale + +/** + @abstract Returns layer scale factor for the x axis. + */ +extern CGFloat POPLayerGetScaleX(CALayer *l); + +/** + @abstract Set layer scale factor for the x axis. + */ +extern void POPLayerSetScaleX(CALayer *l, CGFloat f); + +/** + @abstract Returns layer scale factor for the y axis. + */ +extern CGFloat POPLayerGetScaleY(CALayer *l); + +/** + @abstract Set layer scale factor for the y axis. + */ +extern void POPLayerSetScaleY(CALayer *l, CGFloat f); + +/** + @abstract Returns layer scale factor for the z axis. + */ +extern CGFloat POPLayerGetScaleZ(CALayer *l); + +/** + @abstract Set layer scale factor for the z axis. + */ +extern void POPLayerSetScaleZ(CALayer *l, CGFloat f); + +/** + @abstract Returns layer scale factors for x and y access as point. + */ +extern CGPoint POPLayerGetScaleXY(CALayer *l); + +/** + @abstract Sets layer x and y scale factors given point. + */ +extern void POPLayerSetScaleXY(CALayer *l, CGPoint p); + +#pragma mark - Translation + +/** + @abstract Returns layer translation factor for the x axis. + */ +extern CGFloat POPLayerGetTranslationX(CALayer *l); + +/** + @abstract Set layer translation factor for the x axis. + */ +extern void POPLayerSetTranslationX(CALayer *l, CGFloat f); + +/** + @abstract Returns layer translation factor for the y axis. + */ +extern CGFloat POPLayerGetTranslationY(CALayer *l); + +/** + @abstract Set layer translation factor for the y axis. + */ +extern void POPLayerSetTranslationY(CALayer *l, CGFloat f); + +/** + @abstract Returns layer translation factor for the z axis. + */ +extern CGFloat POPLayerGetTranslationZ(CALayer *l); + +/** + @abstract Set layer translation factor for the z axis. + */ +extern void POPLayerSetTranslationZ(CALayer *l, CGFloat f); + +/** + @abstract Returns layer translation factors for x and y access as point. + */ +extern CGPoint POPLayerGetTranslationXY(CALayer *l); + +/** + @abstract Sets layer x and y translation factors given point. + */ +extern void POPLayerSetTranslationXY(CALayer *l, CGPoint p); + +#pragma mark - Rotation + +/** + @abstract Returns layer rotation, in radians, in the X axis. + */ +extern CGFloat POPLayerGetRotationX(CALayer *l); + +/** + @abstract Sets layer rotation, in radians, in the X axis. + */ +extern void POPLayerSetRotationX(CALayer *l, CGFloat f); + +/** + @abstract Returns layer rotation, in radians, in the Y axis. + */ +extern CGFloat POPLayerGetRotationY(CALayer *l); + +/** + @abstract Sets layer rotation, in radians, in the Y axis. + */ +extern void POPLayerSetRotationY(CALayer *l, CGFloat f); + +/** + @abstract Returns layer rotation, in radians, in the Z axis. + */ +extern CGFloat POPLayerGetRotationZ(CALayer *l); + +/** + @abstract Sets layer rotation, in radians, in the Z axis. + */ +extern void POPLayerSetRotationZ(CALayer *l, CGFloat f); + +/** + @abstract Returns layer rotation, in radians, in the Z axis. + */ +extern CGFloat POPLayerGetRotation(CALayer *l); + +/** + @abstract Sets layer rotation, in radians, in the Z axis. + */ +extern void POPLayerSetRotation(CALayer *l, CGFloat f); + +#pragma mark - Sublayer Scale + +/** + @abstract Returns sublayer scale factors for x and y access as point. + */ +extern CGPoint POPLayerGetSubScaleXY(CALayer *l); + +/** + @abstract Sets sublayer x and y scale factors given point. + */ +extern void POPLayerSetSubScaleXY(CALayer *l, CGPoint p); + +#pragma mark - Sublayer Translation + +/** + @abstract Returns sublayer translation factor for the x axis. + */ +extern CGFloat POPLayerGetSubTranslationX(CALayer *l); + +/** + @abstract Set sublayer translation factor for the x axis. + */ +extern void POPLayerSetSubTranslationX(CALayer *l, CGFloat f); + +/** + @abstract Returns sublayer translation factor for the y axis. + */ +extern CGFloat POPLayerGetSubTranslationY(CALayer *l); + +/** + @abstract Set sublayer translation factor for the y axis. + */ +extern void POPLayerSetSubTranslationY(CALayer *l, CGFloat f); + +/** + @abstract Returns sublayer translation factor for the z axis. + */ +extern CGFloat POPLayerGetSubTranslationZ(CALayer *l); + +/** + @abstract Set sublayer translation factor for the z axis. + */ +extern void POPLayerSetSubTranslationZ(CALayer *l, CGFloat f); + +/** + @abstract Returns sublayer translation factors for x and y access as point. + */ +extern CGPoint POPLayerGetSubTranslationXY(CALayer *l); + +/** + @abstract Sets sublayer x and y translation factors given point. + */ +extern void POPLayerSetSubTranslationXY(CALayer *l, CGPoint p); + +POP_EXTERN_C_END diff --git a/submodules/LegacyComponents/LegacyComponents/POPLayerExtras.mm b/submodules/LegacyComponents/LegacyComponents/POPLayerExtras.mm new file mode 100644 index 0000000000..c8ad7f907b --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/POPLayerExtras.mm @@ -0,0 +1,288 @@ +/** + Copyright (c) 2014-present, Facebook, Inc. + All rights reserved. + + This source code is licensed under the BSD-style license found in the + LICENSE file in the root directory of this source tree. An additional grant + of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "POPLayerExtras.h" + +#include "TransformationMatrix.h" + +using namespace WebCore; + +#define DECOMPOSE_TRANSFORM(L) \ + TransformationMatrix _m(L.transform); \ + TransformationMatrix::DecomposedType _d; \ + _m.decompose(_d); + +#define RECOMPOSE_TRANSFORM(L) \ + _m.recompose(_d); \ + L.transform = _m.transform3d(); + +#define RECOMPOSE_ROT_TRANSFORM(L) \ + _m.recompose(_d, true); \ + L.transform = _m.transform3d(); + +#define DECOMPOSE_SUBLAYER_TRANSFORM(L) \ + TransformationMatrix _m(L.sublayerTransform); \ + TransformationMatrix::DecomposedType _d; \ + _m.decompose(_d); + +#define RECOMPOSE_SUBLAYER_TRANSFORM(L) \ + _m.recompose(_d); \ + L.sublayerTransform = _m.transform3d(); + +#pragma mark - Scale + +NS_INLINE void ensureNonZeroValue(CGFloat &f) +{ + if (f == 0) { + f = 1e-6; + } +} + +NS_INLINE void ensureNonZeroValue(CGPoint &p) +{ + if (p.x == 0 && p.y == 0) { + p.x = 1e-6; + p.y = 1e-6; + } +} + +CGFloat POPLayerGetScaleX(CALayer *l) +{ + DECOMPOSE_TRANSFORM(l); + return _d.scaleX; +} + +void POPLayerSetScaleX(CALayer *l, CGFloat f) +{ + ensureNonZeroValue(f); + DECOMPOSE_TRANSFORM(l); + _d.scaleX = f; + RECOMPOSE_TRANSFORM(l); +} + +CGFloat POPLayerGetScaleY(CALayer *l) +{ + DECOMPOSE_TRANSFORM(l); + return _d.scaleY; +} + +void POPLayerSetScaleY(CALayer *l, CGFloat f) +{ + ensureNonZeroValue(f); + DECOMPOSE_TRANSFORM(l); + _d.scaleY = f; + RECOMPOSE_TRANSFORM(l); +} + +CGFloat POPLayerGetScaleZ(CALayer *l) +{ + DECOMPOSE_TRANSFORM(l); + return _d.scaleZ; +} + +void POPLayerSetScaleZ(CALayer *l, CGFloat f) +{ + ensureNonZeroValue(f); + DECOMPOSE_TRANSFORM(l); + _d.scaleZ = f; + RECOMPOSE_TRANSFORM(l); +} + +CGPoint POPLayerGetScaleXY(CALayer *l) +{ + DECOMPOSE_TRANSFORM(l); + return CGPointMake(_d.scaleX, _d.scaleY); +} + +void POPLayerSetScaleXY(CALayer *l, CGPoint p) +{ + ensureNonZeroValue(p); + DECOMPOSE_TRANSFORM(l); + _d.scaleX = p.x; + _d.scaleY = p.y; + RECOMPOSE_TRANSFORM(l); +} + +#pragma mark - Translation + +CGFloat POPLayerGetTranslationX(CALayer *l) +{ + DECOMPOSE_TRANSFORM(l); + return _d.translateX; +} + +void POPLayerSetTranslationX(CALayer *l, CGFloat f) +{ + DECOMPOSE_TRANSFORM(l); + _d.translateX = f; + RECOMPOSE_TRANSFORM(l); +} + +CGFloat POPLayerGetTranslationY(CALayer *l) +{ + DECOMPOSE_TRANSFORM(l); + return _d.translateY; +} + +void POPLayerSetTranslationY(CALayer *l, CGFloat f) +{ + DECOMPOSE_TRANSFORM(l); + _d.translateY = f; + RECOMPOSE_TRANSFORM(l); +} + +CGFloat POPLayerGetTranslationZ(CALayer *l) +{ + DECOMPOSE_TRANSFORM(l); + return _d.translateZ; +} + +void POPLayerSetTranslationZ(CALayer *l, CGFloat f) +{ + DECOMPOSE_TRANSFORM(l); + _d.translateZ = f; + RECOMPOSE_TRANSFORM(l); +} + +CGPoint POPLayerGetTranslationXY(CALayer *l) +{ + DECOMPOSE_TRANSFORM(l); + return CGPointMake(_d.translateX, _d.translateY); +} + +void POPLayerSetTranslationXY(CALayer *l, CGPoint p) +{ + DECOMPOSE_TRANSFORM(l); + _d.translateX = p.x; + _d.translateY = p.y; + RECOMPOSE_TRANSFORM(l); +} + +#pragma mark - Rotation + +CGFloat POPLayerGetRotationX(CALayer *l) +{ + DECOMPOSE_TRANSFORM(l); + return _d.rotateX; +} + +void POPLayerSetRotationX(CALayer *l, CGFloat f) +{ + DECOMPOSE_TRANSFORM(l); + _d.rotateX = f; + RECOMPOSE_ROT_TRANSFORM(l); +} + +CGFloat POPLayerGetRotationY(CALayer *l) +{ + DECOMPOSE_TRANSFORM(l); + return _d.rotateY; +} + +void POPLayerSetRotationY(CALayer *l, CGFloat f) +{ + DECOMPOSE_TRANSFORM(l); + _d.rotateY = f; + RECOMPOSE_ROT_TRANSFORM(l); +} + +CGFloat POPLayerGetRotationZ(CALayer *l) +{ + DECOMPOSE_TRANSFORM(l); + return _d.rotateZ; +} + +void POPLayerSetRotationZ(CALayer *l, CGFloat f) +{ + DECOMPOSE_TRANSFORM(l); + _d.rotateZ = f; + RECOMPOSE_ROT_TRANSFORM(l); +} + +CGFloat POPLayerGetRotation(CALayer *l) +{ + return POPLayerGetRotationZ(l); +} + +void POPLayerSetRotation(CALayer *l, CGFloat f) +{ + POPLayerSetRotationZ(l, f); +} + +#pragma mark - Sublayer Scale + +CGPoint POPLayerGetSubScaleXY(CALayer *l) +{ + DECOMPOSE_SUBLAYER_TRANSFORM(l); + return CGPointMake(_d.scaleX, _d.scaleY); +} + +void POPLayerSetSubScaleXY(CALayer *l, CGPoint p) +{ + ensureNonZeroValue(p); + DECOMPOSE_SUBLAYER_TRANSFORM(l); + _d.scaleX = p.x; + _d.scaleY = p.y; + RECOMPOSE_SUBLAYER_TRANSFORM(l); +} + +#pragma mark - Sublayer Translation + +extern CGFloat POPLayerGetSubTranslationX(CALayer *l) +{ + DECOMPOSE_SUBLAYER_TRANSFORM(l); + return _d.translateX; +} + +extern void POPLayerSetSubTranslationX(CALayer *l, CGFloat f) +{ + DECOMPOSE_SUBLAYER_TRANSFORM(l); + _d.translateX = f; + RECOMPOSE_SUBLAYER_TRANSFORM(l); +} + +extern CGFloat POPLayerGetSubTranslationY(CALayer *l) +{ + DECOMPOSE_SUBLAYER_TRANSFORM(l); + return _d.translateY; +} + +extern void POPLayerSetSubTranslationY(CALayer *l, CGFloat f) +{ + DECOMPOSE_SUBLAYER_TRANSFORM(l); + _d.translateY = f; + RECOMPOSE_SUBLAYER_TRANSFORM(l); +} + +extern CGFloat POPLayerGetSubTranslationZ(CALayer *l) +{ + DECOMPOSE_SUBLAYER_TRANSFORM(l); + return _d.translateZ; +} + +extern void POPLayerSetSubTranslationZ(CALayer *l, CGFloat f) +{ + DECOMPOSE_SUBLAYER_TRANSFORM(l); + _d.translateZ = f; + RECOMPOSE_SUBLAYER_TRANSFORM(l); +} + +extern CGPoint POPLayerGetSubTranslationXY(CALayer *l) +{ + DECOMPOSE_SUBLAYER_TRANSFORM(l); + return CGPointMake(_d.translateX, _d.translateY); +} + +extern void POPLayerSetSubTranslationXY(CALayer *l, CGPoint p) +{ + DECOMPOSE_SUBLAYER_TRANSFORM(l); + _d.translateX = p.x; + _d.translateY = p.y; + RECOMPOSE_SUBLAYER_TRANSFORM(l); +} diff --git a/submodules/LegacyComponents/LegacyComponents/POPMath.h b/submodules/LegacyComponents/LegacyComponents/POPMath.h new file mode 100644 index 0000000000..baba374e8f --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/POPMath.h @@ -0,0 +1,55 @@ +/** + Copyright (c) 2014-present, Facebook, Inc. + All rights reserved. + + This source code is licensed under the BSD-style license found in the + LICENSE file in the root directory of this source tree. An additional grant + of patent rights can be found in the PATENTS file in the same directory. + */ + +#import +#import + +#import "POPDefines.h" +#import "POPVector.h" + +NS_INLINE CGFloat sqrtr(CGFloat f) +{ +#if CGFLOAT_IS_DOUBLE + return sqrt(f); +#else + return sqrtf(f); +#endif +} + +// round to nearest sub; pass 2.0 to round to every 0.5 (eg: retina pixels) +NS_INLINE CGFloat POPSubRound(CGFloat f, CGFloat sub) +{ + return round(f * sub) / sub; +} + +#define MIX(a, b, f) ((a) + (f) * ((b) - (a))) + +// the longer the duration, the higher the necessary precision +#define SOLVE_EPS(dur) (1. / (1000. * (dur))) + +#define _EQLF_(x, y, epsilon) (fabsf ((x) - (y)) < epsilon) + +extern void POPInterpolateVector(NSUInteger count, CGFloat *dst, const CGFloat *from, const CGFloat *to, CGFloat f); + +extern double POPTimingFunctionSolve(const double vec[4], double t, double eps); + +// quadratic mapping of t [0, 1] to [start, end] +extern double POPQuadraticOutInterpolation(double t, double start, double end); + +// normalize value to [0, 1] based on its range [startValue, endValue] +extern double POPNormalize(double value, double startValue, double endValue); + +// project a normalized value [0, 1] to a given range [start, end] +extern double POPProjectNormal(double n, double start, double end); + +// solve a quadratic equation of the form a * x^2 + b * x + c = 0 +extern void POPQuadraticSolve(CGFloat a, CGFloat b, CGFloat c, CGFloat &x1, CGFloat &x2); + +// for a given tension return the bouncy 3 friction that produces no bounce +extern double POPBouncy3NoBounce(double tension); diff --git a/submodules/LegacyComponents/LegacyComponents/POPMath.mm b/submodules/LegacyComponents/LegacyComponents/POPMath.mm new file mode 100644 index 0000000000..5dbfd7de3c --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/POPMath.mm @@ -0,0 +1,82 @@ +/** + Copyright (c) 2014-present, Facebook, Inc. + All rights reserved. + + This source code is licensed under the BSD-style license found in the + LICENSE file in the root directory of this source tree. An additional grant + of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "POPMath.h" +#import "UnitBezier.h" +#import "POPAnimationPrivate.h" + +void POPInterpolateVector(NSUInteger count, CGFloat *dst, const CGFloat *from, const CGFloat *to, CGFloat f) +{ + for (NSUInteger idx = 0; idx < count; idx++) { + dst[idx] = MIX(from[idx], to[idx], f); + } +} + +double POPTimingFunctionSolve(const double vec[4], double t, double eps) +{ + WebCore::UnitBezier bezier(vec[0], vec[1], vec[2], vec[3]); + return bezier.solve(t, eps); +} + +double POPNormalize(double value, double startValue, double endValue) +{ + return (value - startValue) / (endValue - startValue); +} + +double POPProjectNormal(double n, double start, double end) +{ + return start + (n * (end - start)); +} + +static double linear_interpolation(double t, double start, double end) +{ + return t * end + (1.f - t) * start; +} + +double POPQuadraticOutInterpolation(double t, double start, double end) +{ + return linear_interpolation(2*t - t*t, start, end); +} + +static double b3_friction1(double x) +{ + return (0.0007 * pow(x, 3)) - (0.031 * pow(x, 2)) + 0.64 * x + 1.28; +} + +static double b3_friction2(double x) +{ + return (0.000044 * pow(x, 3)) - (0.006 * pow(x, 2)) + 0.36 * x + 2.; +} + +static double b3_friction3(double x) +{ + return (0.00000045 * pow(x, 3)) - (0.000332 * pow(x, 2)) + 0.1078 * x + 5.84; +} + +double POPBouncy3NoBounce(double tension) +{ + double friction = 0; + if (tension <= 18.) { + friction = b3_friction1(tension); + } else if (tension > 18 && tension <= 44) { + friction = b3_friction2(tension); + } else if (tension > 44) { + friction = b3_friction3(tension); + } else { + assert(false); + } + return friction; +} + +void POPQuadraticSolve(CGFloat a, CGFloat b, CGFloat c, CGFloat &x1, CGFloat &x2) +{ + CGFloat discriminant = sqrt(b * b - 4 * a * c); + x1 = (-b + discriminant) / (2 * a); + x2 = (-b - discriminant) / (2 * a); +} diff --git a/submodules/LegacyComponents/LegacyComponents/POPPropertyAnimation.h b/submodules/LegacyComponents/LegacyComponents/POPPropertyAnimation.h new file mode 100644 index 0000000000..56d4b276f3 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/POPPropertyAnimation.h @@ -0,0 +1,65 @@ +/** + Copyright (c) 2014-present, Facebook, Inc. + All rights reserved. + + This source code is licensed under the BSD-style license found in the + LICENSE file in the root directory of this source tree. An additional grant + of patent rights can be found in the PATENTS file in the same directory. + */ + +#import +#import + +/** + @abstract Flags for clamping animation values. + @discussion Animation values can optionally be clamped to avoid overshoot. kPOPAnimationClampStart ensures values are more than fromValue and kPOPAnimationClampEnd ensures values are less than toValue. + */ +typedef NS_OPTIONS(NSUInteger, POPAnimationClampFlags) +{ + kPOPAnimationClampNone = 0, + kPOPAnimationClampStart = 1UL << 0, + kPOPAnimationClampEnd = 1UL << 1, + kPOPAnimationClampBoth = kPOPAnimationClampStart | kPOPAnimationClampEnd, +}; + +/** + @abstract The semi-concrete property animation subclass. + */ +@interface POPPropertyAnimation : POPAnimation + +/** + @abstract The property to animate. + */ +@property (strong, nonatomic) POPAnimatableProperty *property; + +/** + @abstract The value to animate from. + @discussion The value type should match the property. If unspecified, the value is initialized to the object's current value on animation start. + */ +@property (copy, nonatomic) id fromValue; + +/** + @abstract The value to animate to. + @discussion The value type should match the property. If unspecified, the value is initialized to the object's current value on animation start. + */ +@property (copy, nonatomic) id toValue; + +/** + @abstract The rounding factor applied to the current animated value. + @discussion Specify 1.0 to animate between integral values. Defaults to 0 meaning no rounding. + */ +@property (assign, nonatomic) CGFloat roundingFactor; + +/** + @abstract The clamp mode applied to the current animated value. + @discussion See {@ref POPAnimationClampFlags} for possible values. Defaults to kPOPAnimationClampNone. + */ +@property (assign, nonatomic) NSUInteger clampMode; + +/** + @abstract The flag indicating whether values should be "added" each frame, rather than set. + @discussion Addition may be type dependent. Defaults to NO. + */ +@property (assign, nonatomic, getter = isAdditive) BOOL additive; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/POPPropertyAnimation.mm b/submodules/LegacyComponents/LegacyComponents/POPPropertyAnimation.mm new file mode 100644 index 0000000000..118e6897d8 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/POPPropertyAnimation.mm @@ -0,0 +1,105 @@ +/** + Copyright (c) 2014-present, Facebook, Inc. + All rights reserved. + + This source code is licensed under the BSD-style license found in the + LICENSE file in the root directory of this source tree. An additional grant + of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "POPPropertyAnimationInternal.h" + +@implementation POPPropertyAnimation + +#pragma mark - Lifecycle + +#undef __state +#define __state ((POPPropertyAnimationState *)_state) + +- (void)_initState +{ + _state = new POPPropertyAnimationState(self); +} + +#pragma mark - Properties + +DEFINE_RW_FLAG(POPPropertyAnimationState, additive, isAdditive, setAdditive:); +DEFINE_RW_PROPERTY(POPPropertyAnimationState, roundingFactor, setRoundingFactor:, CGFloat); +DEFINE_RW_PROPERTY(POPPropertyAnimationState, clampMode, setClampMode:, NSUInteger); +DEFINE_RW_PROPERTY_OBJ(POPPropertyAnimationState, property, setProperty:, POPAnimatableProperty*, ((POPPropertyAnimationState*)_state)->updatedDynamicsThreshold();); +DEFINE_RW_PROPERTY_OBJ_COPY(POPPropertyAnimationState, progressMarkers, setProgressMarkers:, NSArray*, ((POPPropertyAnimationState*)_state)->updatedProgressMarkers();); + +- (id)fromValue +{ + return POPBox(__state->fromVec, __state->valueType); +} + +- (void)setFromValue:(id)aValue +{ + POPPropertyAnimationState *s = __state; + VectorRef vec = POPUnbox(aValue, s->valueType, s->valueCount, YES); + if (!vec_equal(vec, s->fromVec)) { + s->fromVec = vec; + + if (s->tracing) { + [s->tracer updateFromValue:aValue]; + } + } +} + +- (id)toValue +{ + return POPBox(__state->toVec, __state->valueType); +} + +- (void)setToValue:(id)aValue +{ + POPPropertyAnimationState *s = __state; + VectorRef vec = POPUnbox(aValue, s->valueType, s->valueCount, YES); + + if (!vec_equal(vec, s->toVec)) { + s->toVec = vec; + + // invalidate to dependent state + s->didReachToValue = false; + s->distanceVec = NULL; + + if (s->tracing) { + [s->tracer updateToValue:aValue]; + } + + // automatically unpause active animations + if (s->active && s->paused) { + s->setPaused(false); + } + } +} + +- (id)currentValue +{ + return POPBox(__state->currentValue(), __state->valueType); +} + +#pragma mark - Utility + +- (void)_appendDescription:(NSMutableString *)s debug:(BOOL)debug +{ + [s appendFormat:@"; from = %@; to = %@", describe(__state->fromVec), describe(__state->toVec)]; + + if (_state->active) + [s appendFormat:@"; currentValue = %@", describe(__state->currentValue())]; + + if (__state->velocityVec && 0 != __state->velocityVec->norm()) + [s appendFormat:@"; velocity = %@", describe(__state->velocityVec)]; + + if (!self.removedOnCompletion) + [s appendFormat:@"; removedOnCompletion = %@", POPStringFromBOOL(self.removedOnCompletion)]; + + if (__state->progressMarkers) + [s appendFormat:@"; progressMarkers = [%@]", [__state->progressMarkers componentsJoinedByString:@", "]]; + + if (_state->active) + [s appendFormat:@"; progress = %f", __state->progress]; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/POPPropertyAnimationInternal.h b/submodules/LegacyComponents/LegacyComponents/POPPropertyAnimationInternal.h new file mode 100644 index 0000000000..9f2aee4447 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/POPPropertyAnimationInternal.h @@ -0,0 +1,359 @@ +/** + Copyright (c) 2014-present, Facebook, Inc. + All rights reserved. + + This source code is licensed under the BSD-style license found in the + LICENSE file in the root directory of this source tree. An additional grant + of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "POPAnimationInternal.h" +#import "POPPropertyAnimation.h" + +static void clampValue(CGFloat &value, CGFloat fromValue, CGFloat toValue, NSUInteger clamp) +{ + BOOL increasing = (toValue > fromValue); + + // Clamp start of animation. + if ((kPOPAnimationClampStart & clamp) && + ((increasing && (value < fromValue)) || (!increasing && (value > fromValue)))) { + value = fromValue; + } + + // Clamp end of animation. + if ((kPOPAnimationClampEnd & clamp) && + ((increasing && (value > toValue)) || (!increasing && (value < toValue)))) { + value = toValue; + } +} + +struct _POPPropertyAnimationState : _POPAnimationState +{ + POPAnimatableProperty *property; + POPValueType valueType; + NSUInteger valueCount; + VectorRef fromVec; + VectorRef toVec; + VectorRef currentVec; + VectorRef previousVec; + VectorRef previous2Vec; + VectorRef velocityVec; + VectorRef originalVelocityVec; + VectorRef distanceVec; + CGFloat roundingFactor; + NSUInteger clampMode; + NSArray *progressMarkers; + POPProgressMarker *progressMarkerState; + NSUInteger progressMarkerCount; + NSUInteger nextProgressMarkerIdx; + CGFloat dynamicsThreshold; + + _POPPropertyAnimationState(id __unsafe_unretained anim) : _POPAnimationState(anim), + property(nil), + valueType((POPValueType)0), + valueCount(0), + fromVec(nullptr), + toVec(nullptr), + currentVec(nullptr), + previousVec(nullptr), + previous2Vec(nullptr), + velocityVec(nullptr), + originalVelocityVec(nullptr), + distanceVec(nullptr), + roundingFactor(0), + clampMode(0), + progressMarkers(nil), + progressMarkerState(nil), + progressMarkerCount(0), + nextProgressMarkerIdx(0), + dynamicsThreshold(0) + { + type = kPOPAnimationBasic; + } + + ~_POPPropertyAnimationState() + { + if (progressMarkerState) { + free(progressMarkerState); + progressMarkerState = NULL; + } + } + + bool canProgress() { + return hasValue(); + } + + bool shouldRound() { + return 0 != roundingFactor; + } + + bool hasValue() { + return 0 != valueCount; + } + + bool isDone() { + // inherit done + if (_POPAnimationState::isDone()) { + return true; + } + + // consider an animation with no values done + if (!hasValue() && !isCustom()) { + return true; + } + + return false; + } + + // returns a copy of the currentVec, rounding if needed + VectorRef currentValue() { + VectorRef vec = VectorRef(Vector::new_vector(currentVec.get())); + if (shouldRound()) { + vec->subRound(1 / roundingFactor); + } + return vec; + } + + void resetProgressMarkerState() + { + for (NSUInteger idx = 0; idx < progressMarkerCount; idx++) + progressMarkerState[idx].reached = false; + + nextProgressMarkerIdx = 0; + } + + void updatedProgressMarkers() + { + if (progressMarkerState) { + free(progressMarkerState); + progressMarkerState = NULL; + } + + progressMarkerCount = progressMarkers.count; + + if (0 != progressMarkerCount) { + progressMarkerState = (POPProgressMarker *)malloc(progressMarkerCount * sizeof(POPProgressMarker)); + [progressMarkers enumerateObjectsUsingBlock:^(NSNumber *progressMarker, NSUInteger idx, BOOL *stop) { + progressMarkerState[idx].reached = false; + progressMarkerState[idx].progress = [progressMarker floatValue]; + }]; + } + + nextProgressMarkerIdx = 0; + } + + virtual void updatedDynamicsThreshold() + { + dynamicsThreshold = property.threshold; + } + + void finalizeProgress() + { + progress = 1.0; + NSUInteger count = valueCount; + VectorRef outVec(Vector::new_vector(count, NULL)); + + if (outVec && toVec) { + *outVec = *toVec; + } + + currentVec = outVec; + clampCurrentValue(); + delegateProgress(); + } + + void computeProgress() { + if (!canProgress()) { + return; + } + + static ComputeProgressFunctor func; + Vector4r v = vector4(currentVec); + Vector4r f = vector4(fromVec); + Vector4r t = vector4(toVec); + progress = func(v, f, t); + } + + void delegateProgress() { + if (!canProgress()) { + return; + } + + if (delegateDidProgress && progressMarkerState) { + + while (nextProgressMarkerIdx < progressMarkerCount) { + if (progress < progressMarkerState[nextProgressMarkerIdx].progress) + break; + + if (!progressMarkerState[nextProgressMarkerIdx].reached) { + ActionEnabler enabler; + [delegate pop_animation:self didReachProgress:progressMarkerState[nextProgressMarkerIdx].progress]; + progressMarkerState[nextProgressMarkerIdx].reached = true; + } + + nextProgressMarkerIdx++; + } + } + + if (!didReachToValue) { + bool didReachToValue = false; + if (0 == valueCount) { + didReachToValue = true; + } else { + Vector4r distance = toVec->vector4r(); + distance -= currentVec->vector4r(); + + if (0 == distance.squaredNorm()) { + didReachToValue = true; + } else { + // components + if (distanceVec) { + didReachToValue = true; + const CGFloat *distanceValues = distanceVec->data(); + for (NSUInteger idx = 0; idx < valueCount; idx++) { + didReachToValue &= (signbit(distance[idx]) != signbit(distanceValues[idx])); + } + } + } + } + + if (didReachToValue) { + handleDidReachToValue(); + } + } + } + + void handleDidReachToValue() { + didReachToValue = true; + + if (delegateDidReachToValue) { + ActionEnabler enabler; + [delegate pop_animationDidReachToValue:self]; + } + + POPAnimationDidReachToValueBlock block = animationDidReachToValueBlock; + if (block != NULL) { + ActionEnabler enabler; + block(self); + } + + if (tracing) { + [tracer didReachToValue:POPBox(currentValue(), valueType, true)]; + } + } + + void readObjectValue(VectorRef *ptrVec, id obj) + { + // use current object value as from value + pop_animatable_read_block read = property.readBlock; + if (NULL != read) { + + Vector4r vec = read_values(read, obj, valueCount); + *ptrVec = VectorRef(Vector::new_vector(valueCount, vec)); + + if (tracing) { + [tracer readPropertyValue:POPBox(*ptrVec, valueType, true)]; + } + } + } + + virtual void willRun(bool started, id obj) { + // ensure from value initialized + if (NULL == fromVec) { + readObjectValue(&fromVec, obj); + } + + // ensure to value initialized + if (NULL == toVec) { + // compute decay to value + if (kPOPAnimationDecay == type) { + [self toValue]; + } else { + // read to value + readObjectValue(&toVec, obj); + } + } + + // handle one time value initialization on start + if (started) { + + // initialize current vec + if (!currentVec) { + currentVec = VectorRef(Vector::new_vector(valueCount, NULL)); + + // initialize current value with from value + // only do this on initial creation to avoid overwriting current value + // on paused animation continuation + if (currentVec && fromVec) { + *currentVec = *fromVec; + } + } + + // ensure velocity values + if (!velocityVec) { + velocityVec = VectorRef(Vector::new_vector(valueCount, NULL)); + } + if (!originalVelocityVec) { + originalVelocityVec = VectorRef(Vector::new_vector(valueCount, NULL)); + } + } + + // ensure distance value initialized + // depends on current value set on one time start + if (NULL == distanceVec) { + + // not yet started animations may not have current value + VectorRef fromVec2 = NULL != currentVec ? currentVec : fromVec; + + if (fromVec2 && toVec) { + Vector4r distance = toVec->vector4r(); + distance -= fromVec2->vector4r(); + + if (0 != distance.squaredNorm()) { + distanceVec = VectorRef(Vector::new_vector(valueCount, distance)); + } + } + } + } + + virtual void reset(bool all) { + _POPAnimationState::reset(all); + + if (all) { + currentVec = NULL; + previousVec = NULL; + previous2Vec = NULL; + } + progress = 0; + resetProgressMarkerState(); + didReachToValue = false; + distanceVec = NULL; + } + + void clampCurrentValue(NSUInteger clamp) + { + if (kPOPAnimationClampNone == clamp) + return; + + // Clamp all vector values + CGFloat *currentValues = currentVec->data(); + const CGFloat *fromValues = fromVec->data(); + const CGFloat *toValues = toVec->data(); + + for (NSUInteger idx = 0; idx < valueCount; idx++) { + clampValue(currentValues[idx], fromValues[idx], toValues[idx], clamp); + } + } + + void clampCurrentValue() + { + clampCurrentValue(clampMode); + } +}; + +typedef struct _POPPropertyAnimationState POPPropertyAnimationState; + +@interface POPPropertyAnimation () + +@end + diff --git a/submodules/LegacyComponents/LegacyComponents/POPSpringAnimation.h b/submodules/LegacyComponents/LegacyComponents/POPSpringAnimation.h new file mode 100644 index 0000000000..08e0fab644 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/POPSpringAnimation.h @@ -0,0 +1,67 @@ +/** + Copyright (c) 2014-present, Facebook, Inc. + All rights reserved. + + This source code is licensed under the BSD-style license found in the + LICENSE file in the root directory of this source tree. An additional grant + of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +/** + @abstract A concrete spring animation class. + @discussion Animation is achieved through modeling spring dynamics. + */ +@interface POPSpringAnimation : POPPropertyAnimation + +/** + @abstract The designated initializer. + @returns An instance of a spring animation. + */ ++ (instancetype)animation; + +/** + @abstract Convenience initializer that returns an animation with animatable property of name. + @param name The name of the animatable property. + @returns An instance of a spring animation configured with specified animatable property. + */ ++ (instancetype)animationWithPropertyNamed:(NSString *)name; + +/** + @abstract The current velocity value. + @discussion Set before animation start to account for initial velocity. Expressed in change of value units per second. + */ +@property (copy, nonatomic) id velocity; + +/** + @abstract The effective bounciness. + @discussion Use in conjunction with 'springSpeed' to change animation effect. Values are converted into corresponding dynamics constants. Defined as a value in the range [0, 20]. Defaults to 4. + */ +@property (assign, nonatomic) CGFloat springBounciness; + +/** + @abstract The effective speed. + @discussion Use in conjunction with 'springBounciness' to change animation effect. Values are converted into corresponding dynamics constants. Defined as a value in the range [0, 20]. Defaults to 12. + */ +@property (assign, nonatomic) CGFloat springSpeed; + +/** + @abstract The tension used in the dynamics simulation. + @discussion Can be used over bounciness and speed for finer grain tweaking of animation effect. + */ +@property (assign, nonatomic) CGFloat dynamicsTension; + +/** + @abstract The friction used in the dynamics simulation. + @discussion Can be used over bounciness and speed for finer grain tweaking of animation effect. + */ +@property (assign, nonatomic) CGFloat dynamicsFriction; + +/** + @abstract The mass used in the dynamics simulation. + @discussion Can be used over bounciness and speed for finer grain tweaking of animation effect. + */ +@property (assign, nonatomic) CGFloat dynamicsMass; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/POPSpringAnimation.mm b/submodules/LegacyComponents/LegacyComponents/POPSpringAnimation.mm new file mode 100644 index 0000000000..10cb510a63 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/POPSpringAnimation.mm @@ -0,0 +1,164 @@ +/** + Copyright (c) 2014-present, Facebook, Inc. + All rights reserved. + + This source code is licensed under the BSD-style license found in the + LICENSE file in the root directory of this source tree. An additional grant + of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "POPSpringAnimationInternal.h" + +@implementation POPSpringAnimation + +#pragma mark - Lifecycle + +#undef __state +#define __state ((POPSpringAnimationState *)_state) + ++ (instancetype)animation +{ + return [[self alloc] init]; +} + ++ (instancetype)animationWithPropertyNamed:(NSString *)aName +{ + POPSpringAnimation *anim = [self animation]; + anim.property = [POPAnimatableProperty propertyWithName:aName]; + return anim; +} + +- (void)_initState +{ + _state = new POPSpringAnimationState(self); +} + +- (id)init +{ + self = [super _init]; + if (nil != self) { + __state->solver = new SpringSolver4d(1, 1, 1); + __state->updatedDynamicsThreshold(); + __state->updatedBouncinessAndSpeed(); + } + return self; +} + +- (void)dealloc +{ + if (__state) { + delete __state->solver; + __state->solver = NULL; + } +} + +#pragma mark - Properties + +- (id)velocity +{ + return POPBox(__state->velocityVec, __state->valueType); +} + +- (void)setVelocity:(id)aValue +{ + POPPropertyAnimationState *s = __state; + VectorRef vec = POPUnbox(aValue, s->valueType, s->valueCount, YES); + if (!vec_equal(vec, s->velocityVec)) { + s->velocityVec = vec; + + if (s->tracing) { + [s->tracer updateVelocity:aValue]; + } + } +} + +DEFINE_RW_PROPERTY(POPSpringAnimationState, dynamicsTension, setDynamicsTension:, CGFloat, [self _updatedDynamicsTension];); +DEFINE_RW_PROPERTY(POPSpringAnimationState, dynamicsFriction, setDynamicsFriction:, CGFloat, [self _updatedDynamicsFriction];); +DEFINE_RW_PROPERTY(POPSpringAnimationState, dynamicsMass, setDynamicsMass:, CGFloat, [self _updatedDynamicsMass];); + +FB_PROPERTY_GET(POPSpringAnimationState, springSpeed, CGFloat); +- (void)setSpringSpeed:(CGFloat)aFloat +{ + POPSpringAnimationState *s = __state; + if (s->userSpecifiedDynamics || aFloat != s->springSpeed) { + s->springSpeed = aFloat; + s->userSpecifiedDynamics = false; + s->updatedBouncinessAndSpeed(); + if (s->tracing) { + [s->tracer updateSpeed:aFloat]; + } + } +} + +FB_PROPERTY_GET(POPSpringAnimationState, springBounciness, CGFloat); +- (void)setSpringBounciness:(CGFloat)aFloat +{ + POPSpringAnimationState *s = __state; + if (s->userSpecifiedDynamics || aFloat != s->springBounciness) { + s->springBounciness = aFloat; + s->userSpecifiedDynamics = false; + s->updatedBouncinessAndSpeed(); + if (s->tracing) { + [s->tracer updateBounciness:aFloat]; + } + } +} + +- (SpringSolver4d *)solver +{ + return __state->solver; +} + +- (void)setSolver:(SpringSolver4d *)aSolver +{ + if (aSolver != __state->solver) { + if (__state->solver) { + delete(__state->solver); + } + __state->solver = aSolver; + } +} + +#pragma mark - Utility + +- (void)_updatedDynamicsTension +{ + __state->userSpecifiedDynamics = true; + if(__state->tracing) { + [__state->tracer updateTension:__state->dynamicsTension]; + } + __state->updatedDynamics(); +} + +- (void)_updatedDynamicsFriction +{ + __state->userSpecifiedDynamics = true; + if(__state->tracing) { + [__state->tracer updateFriction:__state->dynamicsFriction]; + } + __state->updatedDynamics(); +} + +- (void)_updatedDynamicsMass +{ + __state->userSpecifiedDynamics = true; + if(__state->tracing) { + [__state->tracer updateMass:__state->dynamicsMass]; + } + __state->updatedDynamics(); +} + +- (void)_appendDescription:(NSMutableString *)s debug:(BOOL)debug +{ + [super _appendDescription:s debug:debug]; + + if (debug) { + if (_state->userSpecifiedDynamics) { + [s appendFormat:@"; dynamics = (tension:%f, friction:%f, mass:%f)", __state->dynamicsTension, __state->dynamicsFriction, __state->dynamicsMass]; + } else { + [s appendFormat:@"; bounciness = %f; speed = %f", __state->springBounciness, __state->springSpeed]; + } + } +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/POPSpringAnimationInternal.h b/submodules/LegacyComponents/LegacyComponents/POPSpringAnimationInternal.h new file mode 100644 index 0000000000..f5664ddbfe --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/POPSpringAnimationInternal.h @@ -0,0 +1,130 @@ +/** + Copyright (c) 2014-present, Facebook, Inc. + All rights reserved. + + This source code is licensed under the BSD-style license found in the + LICENSE file in the root directory of this source tree. An additional grant + of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "POPAnimationExtras.h" +#import "POPPropertyAnimationInternal.h" + +struct _POPSpringAnimationState : _POPPropertyAnimationState +{ + SpringSolver4d *solver; + CGFloat springSpeed; + CGFloat springBounciness; // normalized springiness + CGFloat dynamicsTension; // tension + CGFloat dynamicsFriction; // friction + CGFloat dynamicsMass; // mass + + _POPSpringAnimationState(id __unsafe_unretained anim) : _POPPropertyAnimationState(anim), + solver(nullptr), + springSpeed(12.), + springBounciness(4.), + dynamicsTension(0), + dynamicsFriction(0), + dynamicsMass(0) + { + type = kPOPAnimationSpring; + } + + bool hasConverged() + { + NSUInteger count = valueCount; + if (shouldRound()) { + return vec_equal(previous2Vec, previousVec) && vec_equal(previousVec, toVec); + } else { + if (!previousVec || !previous2Vec) + return false; + + CGFloat t = dynamicsThreshold / 5; + + const CGFloat *toValues = toVec->data(); + const CGFloat *previousValues = previousVec->data(); + const CGFloat *previous2Values = previous2Vec->data(); + + for (NSUInteger idx = 0; idx < count; idx++) { + if ((fabsf((float)(toValues[idx] - previousValues[idx])) >= t) || (fabsf((float)(previous2Values[idx] - previousValues[idx])) >= t)) { + return false; + } + } + return true; + } + } + + bool isDone() { + if (_POPPropertyAnimationState::isDone()) { + return true; + } + return solver->started() && (hasConverged() || solver->hasConverged()); + } + + void updatedDynamics() + { + if (NULL != solver) { + solver->setConstants(dynamicsTension, dynamicsFriction, dynamicsMass); + } + } + + void updatedDynamicsThreshold() + { + _POPPropertyAnimationState::updatedDynamicsThreshold(); + if (NULL != solver) { + solver->setThreshold(dynamicsThreshold); + } + } + + void updatedBouncinessAndSpeed() { + [POPSpringAnimation convertBounciness:springBounciness speed:springSpeed toTension:&dynamicsTension friction:&dynamicsFriction mass:&dynamicsMass]; + updatedDynamics(); + } + + bool advance(CFTimeInterval time, CFTimeInterval dt, id obj) { + // advance past not yet initialized animations + if (NULL == currentVec) { + return false; + } + + CFTimeInterval localTime = time - startTime; + + Vector4d value = vector4d(currentVec); + Vector4d toValue = vector4d(toVec); + Vector4d velocity = vector4d(velocityVec); + + SSState4d state; + state.p = toValue - value; + + // the solver assumes a spring of size zero + // flip the velocity from user perspective to solver perspective + state.v = velocity * -1; + + solver->advance(state, localTime, dt); + value = toValue - state.p; + + // flip velocity back to user perspective + velocity = state.v * -1; + + *currentVec = value; + + if (velocityVec) { + *velocityVec = velocity; + } + + clampCurrentValue(); + + return true; + } + + virtual void reset(bool all) { + _POPPropertyAnimationState::reset(all); + + if (solver) { + solver->setConstants(dynamicsTension, dynamicsFriction, dynamicsMass); + solver->reset(); + } + } +}; + +typedef struct _POPSpringAnimationState POPSpringAnimationState; diff --git a/submodules/LegacyComponents/LegacyComponents/POPSpringSolver.h b/submodules/LegacyComponents/LegacyComponents/POPSpringSolver.h new file mode 100644 index 0000000000..df485bf8f1 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/POPSpringSolver.h @@ -0,0 +1,190 @@ +/** + Copyright (c) 2014-present, Facebook, Inc. + All rights reserved. + + This source code is licensed under the BSD-style license found in the + LICENSE file in the root directory of this source tree. An additional grant + of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "POPVector.h" + +namespace POP { + + template + struct SSState + { + T p; + T v; + }; + + template + struct SSDerivative + { + T dp; + T dv; + }; + + typedef SSState SSState4d; + typedef SSDerivative SSDerivative4d; + + const CFTimeInterval solverDt = 0.001f; + const CFTimeInterval maxSolverDt = 30.0f; + + /** + Templated spring solver class. + */ + template + class SpringSolver + { + double _k; // stiffness + double _b; // dampening + double _m; // mass + + double _tp; // threshold + double _tv; // threshold velocity + double _ta; // threshold acceleration + + CFTimeInterval _accumulatedTime; + SSState _lastState; + T _lastDv; + bool _started; + + public: + SpringSolver(double k, double b, double m = 1) : _k(k), _b(b), _m(m), _started(false) + { + _accumulatedTime = 0; + _lastState.p = T::Zero(); + _lastState.v = T::Zero(); + _lastDv = T::Zero(); + setThreshold(1.); + } + + ~SpringSolver() + { + } + + bool started() + { + return _started; + } + + void setConstants(double k, double b, double m) + { + _k = k; + _b = b; + _m = m; + } + + void setThreshold(double t) + { + _tp = t / 2; // half a unit + _tv = 25.0 * t; // 5 units per second, squared for comparison + _ta = 625.0 * t * t; // 5 units per second squared, squared for comparison + } + + T acceleration(const SSState &state, double t) + { + return state.p*(-_k/_m) - state.v*(_b/_m); + } + + SSDerivative evaluate(const SSState &initial, double t) + { + SSDerivative output; + output.dp = initial.v; + output.dv = acceleration(initial, t); + return output; + } + + SSDerivative evaluate(const SSState &initial, double t, double dt, const SSDerivative &d) + { + SSState state; + state.p = initial.p + d.dp*dt; + state.v = initial.v + d.dv*dt; + SSDerivative output; + output.dp = state.v; + output.dv = acceleration(state, t+dt); + return output; + } + + void integrate(SSState &state, double t, double dt) + { + SSDerivative a = evaluate(state, t); + SSDerivative b = evaluate(state, t, dt*0.5, a); + SSDerivative c = evaluate(state, t, dt*0.5, b); + SSDerivative d = evaluate(state, t, dt, c); + + T dpdt = (a.dp + (b.dp + c.dp)*2.0 + d.dp) * (1.0/6.0); + T dvdt = (a.dv + (b.dv + c.dv)*2.0 + d.dv) * (1.0/6.0); + + state.p = state.p + dpdt*dt; + state.v = state.v + dvdt*dt; + + _lastDv = dvdt; + } + + SSState interpolate(const SSState &previous, const SSState ¤t, double alpha) + { + SSState state; + state.p = current.p*alpha + previous.p*(1-alpha); + state.v = current.v*alpha + previous.v*(1-alpha); + return state; + } + + void advance(SSState &state, double t, double dt) + { + _started = true; + + if (dt > maxSolverDt) { + // excessive time step, force shut down + _lastDv = _lastState.v = _lastState.p = T::Zero(); + } else { + _accumulatedTime += dt; + + SSState previousState = state, currentState = state; + while (_accumulatedTime >= solverDt) { + previousState = currentState; + this->integrate(currentState, t, solverDt); + t += solverDt; + _accumulatedTime -= solverDt; + } + CFTimeInterval alpha = _accumulatedTime / solverDt; + _lastState = state = this->interpolate(previousState, currentState, alpha); + } + } + + bool hasConverged() + { + if (!_started) { + return false; + } + + for (size_t idx = 0; idx < _lastState.p.size(); idx++) { + if (fabs(_lastState.p(idx)) >= _tp) { + return false; + } + } + + return (_lastState.v.squaredNorm() < _tv) && (_lastDv.squaredNorm() < _ta); + } + + void reset() + { + _accumulatedTime = 0; + _lastState.p = T::Zero(); + _lastState.v = T::Zero(); + _lastDv = T::Zero(); + _started = false; + } + }; + + /** + Convenience spring solver type definitions. + */ + typedef SpringSolver SpringSolver2d; + typedef SpringSolver SpringSolver3d; + typedef SpringSolver SpringSolver4d; +} + diff --git a/submodules/LegacyComponents/LegacyComponents/POPVector.h b/submodules/LegacyComponents/LegacyComponents/POPVector.h new file mode 100644 index 0000000000..630f17307b --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/POPVector.h @@ -0,0 +1,391 @@ +/** + Copyright (c) 2014-present, Facebook, Inc. + All rights reserved. + + This source code is licensed under the BSD-style license found in the + LICENSE file in the root directory of this source tree. An additional grant + of patent rights can be found in the PATENTS file in the same directory. + */ + +#ifndef __POP__FBVector__ +#define __POP__FBVector__ + +#include +#include + +#import +#import +#if TARGET_OS_IPHONE +#import +#endif + +#import "POPMath.h" +#import "POPDefines.h" + +#if SCENEKIT_SDK_AVAILABLE +#import +#endif + +namespace POP { + + /** Fixed two-size vector class */ + template + struct Vector2 + { + private: + typedef T Vector2::* const _data[2]; + static const _data _v; + + public: + T x; + T y; + + // Zero vector + static const Vector2 Zero() { return Vector2(0); } + + // Constructors + Vector2() {} + explicit Vector2(T v) { x = v; y = v; }; + explicit Vector2(T x0, T y0) : x(x0), y(y0) {}; + explicit Vector2(const CGPoint &p) : x(p.x), y (p.y) {} + explicit Vector2(const CGSize &s) : x(s.width), y (s.height) {} + + // Copy constructor + template explicit Vector2(const Vector2 &v) : x(v.x), y(v.y) {} + + // Index operators + const T& operator[](size_t i) const { return this->*_v[i]; } + T& operator[](size_t i) { return this->*_v[i]; } + const T& operator()(size_t i) const { return this->*_v[i]; } + T& operator()(size_t i) { return this->*_v[i]; } + + // Backing data + T * data() { return &(this->*_v[0]); } + const T * data() const { return &(this->*_v[0]); } + + // Size + inline size_t size() const { return 2; } + + // Assignment + Vector2 &operator= (T v) { x = v; y = v; return *this;} + template Vector2 &operator= (const Vector2 &v) { x = v.x; y = v.y; return *this;} + + // Negation + Vector2 operator- (void) const { return Vector2(-x, -y); } + + // Equality + bool operator== (T v) const { return (x == v && y == v); } + bool operator== (const Vector2 &v) const { return (x == v.x && y == v.y); } + + // Inequality + bool operator!= (T v) const {return (x != v || y != v); } + bool operator!= (const Vector2 &v) const { return (x != v.x || y != v.y); } + + // Scalar Math + Vector2 operator+ (T v) const { return Vector2(x + v, y + v); } + Vector2 operator- (T v) const { return Vector2(x - v, y - v); } + Vector2 operator* (T v) const { return Vector2(x * v, y * v); } + Vector2 operator/ (T v) const { return Vector2(x / v, y / v); } + Vector2 &operator+= (T v) { x += v; y += v; return *this; }; + Vector2 &operator-= (T v) { x -= v; y -= v; return *this; }; + Vector2 &operator*= (T v) { x *= v; y *= v; return *this; }; + Vector2 &operator/= (T v) { x /= v; y /= v; return *this; }; + + // Vector Math + Vector2 operator+ (const Vector2 &v) const { return Vector2(x + v.x, y + v.y); } + Vector2 operator- (const Vector2 &v) const { return Vector2(x - v.x, y - v.y); } + Vector2 &operator+= (const Vector2 &v) { x += v.x; y += v.y; return *this; }; + Vector2 &operator-= (const Vector2 &v) { x -= v.x; y -= v.y; return *this; }; + + // Norms + CGFloat norm() const { return sqrtr(squaredNorm()); } + CGFloat squaredNorm() const { return x * x + y * y; } + + // Cast + template Vector2 cast() const { return Vector2(x, y); } + CGPoint cg_point() const { return CGPointMake(x, y); }; + }; + + template + const typename Vector2::_data Vector2::_v = { &Vector2::x, &Vector2::y }; + + /** Fixed three-size vector class */ + template + struct Vector3 + { + private: + typedef T Vector3::* const _data[3]; + static const _data _v; + + public: + T x; + T y; + T z; + + // Zero vector + static const Vector3 Zero() { return Vector3(0); }; + + // Constructors + Vector3() {} + explicit Vector3(T v) : x(v), y(v), z(v) {}; + explicit Vector3(T x0, T y0, T z0) : x(x0), y(y0), z(z0) {}; + + // Copy constructor + template explicit Vector3(const Vector3 &v) : x(v.x), y(v.y), z(v.z) {} + + // Index operators + const T& operator[](size_t i) const { return this->*_v[i]; } + T& operator[](size_t i) { return this->*_v[i]; } + const T& operator()(size_t i) const { return this->*_v[i]; } + T& operator()(size_t i) { return this->*_v[i]; } + + // Backing data + T * data() { return &(this->*_v[0]); } + const T * data() const { return &(this->*_v[0]); } + + // Size + inline size_t size() const { return 3; } + + // Assignment + Vector3 &operator= (T v) { x = v; y = v; z = v; return *this;} + template Vector3 &operator= (const Vector3 &v) { x = v.x; y = v.y; z = v.z; return *this;} + + // Negation + Vector3 operator- (void) const { return Vector3(-x, -y, -z); } + + // Equality + bool operator== (T v) const { return (x == v && y == v && z = v); } + bool operator== (const Vector3 &v) const { return (x == v.x && y == v.y && z == v.z); } + + // Inequality + bool operator!= (T v) const {return (x != v || y != v || z != v); } + bool operator!= (const Vector3 &v) const { return (x != v.x || y != v.y || z != v.z); } + + // Scalar Math + Vector3 operator+ (T v) const { return Vector3(x + v, y + v, z + v); } + Vector3 operator- (T v) const { return Vector3(x - v, y - v, z - v); } + Vector3 operator* (T v) const { return Vector3(x * v, y * v, z * v); } + Vector3 operator/ (T v) const { return Vector3(x / v, y / v, z / v); } + Vector3 &operator+= (T v) { x += v; y += v; z += v; return *this; }; + Vector3 &operator-= (T v) { x -= v; y -= v; z -= v; return *this; }; + Vector3 &operator*= (T v) { x *= v; y *= v; z *= v; return *this; }; + Vector3 &operator/= (T v) { x /= v; y /= v; z /= v; return *this; }; + + // Vector Math + Vector3 operator+ (const Vector3 &v) const { return Vector3(x + v.x, y + v.y, z + v.z); } + Vector3 operator- (const Vector3 &v) const { return Vector3(x - v.x, y - v.y, z - v.z); } + Vector3 &operator+= (const Vector3 &v) { x += v.x; y += v.y; z += v.z; return *this; }; + Vector3 &operator-= (const Vector3 &v) { x -= v.x; y -= v.y; z -= v.z; return *this; }; + + // Norms + CGFloat norm() const { return sqrtr(squaredNorm()); } + CGFloat squaredNorm() const { return x * x + y * y + z * z; } + + // Cast + template Vector3 cast() const { return Vector3(x, y, z); } + }; + + template + const typename Vector3::_data Vector3::_v = { &Vector3::x, &Vector3::y, &Vector3::z }; + + /** Fixed four-size vector class */ + template + struct Vector4 + { + private: + typedef T Vector4::* const _data[4]; + static const _data _v; + + public: + T x; + T y; + T z; + T w; + + // Zero vector + static const Vector4 Zero() { return Vector4(0); }; + + // Constructors + Vector4() {} + explicit Vector4(T v) : x(v), y(v), z(v), w(v) {}; + explicit Vector4(T x0, T y0, T z0, T w0) : x(x0), y(y0), z(z0), w(w0) {}; + + // Copy constructor + template explicit Vector4(const Vector4 &v) : x(v.x), y(v.y), z(v.z), w(v.w) {} + + // Index operators + const T& operator[](size_t i) const { return this->*_v[i]; } + T& operator[](size_t i) { return this->*_v[i]; } + const T& operator()(size_t i) const { return this->*_v[i]; } + T& operator()(size_t i) { return this->*_v[i]; } + + // Backing data + T * data() { return &(this->*_v[0]); } + const T * data() const { return &(this->*_v[0]); } + + // Size + inline size_t size() const { return 4; } + + // Assignment + Vector4 &operator= (T v) { x = v; y = v; z = v; w = v; return *this;} + template Vector4 &operator= (const Vector4 &v) { x = v.x; y = v.y; z = v.z; w = v.w; return *this;} + + // Negation + Vector4 operator- (void) const { return Vector4(-x, -y, -z, -w); } + + // Equality + bool operator== (T v) const { return (x == v && y == v && z = v, w = v); } + bool operator== (const Vector4 &v) const { return (x == v.x && y == v.y && z == v.z && w == v.w); } + + // Inequality + bool operator!= (T v) const {return (x != v || y != v || z != v || w != v); } + bool operator!= (const Vector4 &v) const { return (x != v.x || y != v.y || z != v.z || w != v.w); } + + // Scalar Math + Vector4 operator+ (T v) const { return Vector4(x + v, y + v, z + v, w + v); } + Vector4 operator- (T v) const { return Vector4(x - v, y - v, z - v, w - v); } + Vector4 operator* (T v) const { return Vector4(x * v, y * v, z * v, w * v); } + Vector4 operator/ (T v) const { return Vector4(x / v, y / v, z / v, w / v); } + Vector4 &operator+= (T v) { x += v; y += v; z += v; w += v; return *this; }; + Vector4 &operator-= (T v) { x -= v; y -= v; z -= v; w -= v; return *this; }; + Vector4 &operator*= (T v) { x *= v; y *= v; z *= v; w *= v; return *this; }; + Vector4 &operator/= (T v) { x /= v; y /= v; z /= v; w /= v; return *this; }; + + // Vector Math + Vector4 operator+ (const Vector4 &v) const { return Vector4(x + v.x, y + v.y, z + v.z, w + v.w); } + Vector4 operator- (const Vector4 &v) const { return Vector4(x - v.x, y - v.y, z - v.z, w - v.w); } + Vector4 &operator+= (const Vector4 &v) { x += v.x; y += v.y; z += v.z; w += v.w; return *this; }; + Vector4 &operator-= (const Vector4 &v) { x -= v.x; y -= v.y; z -= v.z; w -= v.w; return *this; }; + + // Norms + CGFloat norm() const { return sqrtr(squaredNorm()); } + CGFloat squaredNorm() const { return x * x + y * y + z * z + w * w; } + + // Cast + template Vector4 cast() const { return Vector4(x, y, z, w); } + }; + + template + const typename Vector4::_data Vector4::_v = { &Vector4::x, &Vector4::y, &Vector4::z, &Vector4::w }; + + /** Convenience typedefs */ + typedef Vector2 Vector2f; + typedef Vector2 Vector2d; + typedef Vector2 Vector2r; + typedef Vector3 Vector3f; + typedef Vector3 Vector3d; + typedef Vector3 Vector3r; + typedef Vector4 Vector4f; + typedef Vector4 Vector4d; + typedef Vector4 Vector4r; + + /** Variable-sized vector class */ + class Vector + { + size_t _count; + CGFloat *_values; + + private: + Vector(size_t); + Vector(const Vector& other); + + public: + ~Vector(); + + // Creates a new vector instance of count with values. Initializing a vector of size 0 returns NULL. + static Vector *new_vector(NSUInteger count, const CGFloat *values); + + // Creates a new vector given a pointer to another. Can return NULL. + static Vector *new_vector(const Vector * const other); + + // Creates a variable size vector given a static vector and count. + static Vector *new_vector(NSUInteger count, Vector4r vec); + + // Size of vector + NSUInteger size() const { return _count; } + + // Returns array of values + CGFloat *data () { return _values; } + const CGFloat *data () const { return _values; }; + + // Vector2r support + Vector2r vector2r() const; + + // Vector4r support + Vector4r vector4r() const; + + // CGFloat support + static Vector *new_cg_float(CGFloat f); + + // CGPoint support + CGPoint cg_point() const; + static Vector *new_cg_point(const CGPoint &p); + + // CGSize support + CGSize cg_size() const; + static Vector *new_cg_size(const CGSize &s); + + // CGRect support + CGRect cg_rect() const; + static Vector *new_cg_rect(const CGRect &r); + +#if TARGET_OS_IPHONE + // UIEdgeInsets support + UIEdgeInsets ui_edge_insets() const; + static Vector *new_ui_edge_insets(const UIEdgeInsets &i); +#endif + + // CGAffineTransform support + CGAffineTransform cg_affine_transform() const; + static Vector *new_cg_affine_transform(const CGAffineTransform &t); + + // CGColorRef support + CGColorRef cg_color() const CF_RETURNS_RETAINED; + static Vector *new_cg_color(CGColorRef color); + +#if SCENEKIT_SDK_AVAILABLE + // SCNVector3 support + SCNVector3 scn_vector3() const; + static Vector *new_scn_vector3(const SCNVector3 &vec3); + + // SCNVector4 support + SCNVector4 scn_vector4() const; + static Vector *new_scn_vector4(const SCNVector4 &vec4); +#endif + + // operator overloads + CGFloat &operator[](size_t i) const { + NSCAssert(size() > i, @"unexpected vector size:%lu", (unsigned long)size()); + return _values[i]; + } + + // Returns the mathematical length + CGFloat norm() const; + CGFloat squaredNorm() const; + + // Round to nearest sub + void subRound(CGFloat sub); + + // Returns string description + NSString * toString() const; + + // Operator overloads + template Vector& operator= (const Vector4& other) { + size_t count = MIN(_count, other.size()); + for (size_t i = 0; i < count; i++) { + _values[i] = other[i]; + } + return *this; + } + Vector& operator= (const Vector& other); + void swap(Vector &first, Vector &second); + bool operator==(const Vector &other) const; + bool operator!=(const Vector &other) const; + }; + + /** Convenience typedefs */ + typedef std::shared_ptr VectorRef; + typedef std::shared_ptr VectorConstRef; + +} +#endif /* defined(__POP__FBVector__) */ diff --git a/submodules/LegacyComponents/LegacyComponents/POPVector.mm b/submodules/LegacyComponents/LegacyComponents/POPVector.mm new file mode 100644 index 0000000000..98007ef172 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/POPVector.mm @@ -0,0 +1,332 @@ +/** + Copyright (c) 2014-present, Facebook, Inc. + All rights reserved. + + This source code is licensed under the BSD-style license found in the + LICENSE file in the root directory of this source tree. An additional grant + of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "POPVector.h" +#import "POPCGUtils.h" + +namespace POP +{ + + Vector::Vector(const size_t count) + { + _count = count; + _values = 0 != count ? (CGFloat *)calloc(count, sizeof(CGFloat)) : NULL; + } + + Vector::Vector(const Vector& other) + { + _count = other.size(); + _values = 0 != _count ? (CGFloat *)calloc(_count, sizeof(CGFloat)) : NULL; + if (0 != _count) { + memcpy(_values, other.data(), _count * sizeof(CGFloat)); + } + } + + Vector::~Vector() + { + if (NULL != _values) { + free(_values); + _values = NULL; + } + _count = 0; + } + + void Vector::swap(Vector &first, Vector &second) + { + using std::swap; + swap(first._count, second._count); + swap(first._values, second._values); + } + + Vector& Vector::operator=(const Vector& other) + { + Vector temp(other); + swap(*this, temp); + return *this; + } + + bool Vector::operator==(const Vector &other) const { + if (_count != other.size()) { + return false; + } + + const CGFloat * const values = other.data(); + + for (NSUInteger idx = 0; idx < _count; idx++) { + if (_values[idx] != values[idx]) { + return false; + } + } + + return true; + } + + bool Vector::operator!=(const Vector &other) const { + if (_count == other.size()) { + return false; + } + + const CGFloat * const values = other.data(); + + for (NSUInteger idx = 0; idx < _count; idx++) { + if (_values[idx] != values[idx]) { + return false; + } + } + + return true; + } + + Vector *Vector::new_vector(NSUInteger count, const CGFloat *values) + { + if (0 == count) { + return NULL; + } + + Vector *v = new Vector(count); + if (NULL != values) { + memcpy(v->_values, values, count * sizeof(CGFloat)); + } + return v; + } + + Vector *Vector::new_vector(const Vector * const other) + { + if (NULL == other) { + return NULL; + } + + return Vector::new_vector(other->size(), other->data()); + } + + Vector *Vector::new_vector(NSUInteger count, Vector4r vec) + { + if (0 == count) { + return NULL; + } + + Vector *v = new Vector(count); + + NSCAssert(count <= 4, @"unexpected count %lu", (unsigned long)count); + for (NSUInteger i = 0; i < MIN(count, (NSUInteger)4); i++) { + v->_values[i] = vec[i]; + } + + return v; + } + + Vector4r Vector::vector4r() const + { + Vector4r v = Vector4r::Zero(); + for (size_t i = 0; i < _count; i++) { + v(i) = _values[i]; + } + return v; + } + + Vector2r Vector::vector2r() const + { + Vector2r v = Vector2r::Zero(); + if (_count > 0) v(0) = _values[0]; + if (_count > 1) v(1) = _values[1]; + return v; + } + + Vector *Vector::new_cg_float(CGFloat f) + { + Vector *v = new Vector(1); + v->_values[0] = f; + return v; + } + + CGPoint Vector::cg_point () const + { + Vector2r v = vector2r(); + return CGPointMake(v(0), v(1)); + } + + Vector *Vector::new_cg_point(const CGPoint &p) + { + Vector *v = new Vector(2); + v->_values[0] = p.x; + v->_values[1] = p.y; + return v; + } + + CGSize Vector::cg_size () const + { + Vector2r v = vector2r(); + return CGSizeMake(v(0), v(1)); + } + + Vector *Vector::new_cg_size(const CGSize &s) + { + Vector *v = new Vector(2); + v->_values[0] = s.width; + v->_values[1] = s.height; + return v; + } + + CGRect Vector::cg_rect() const + { + return _count < 4 ? CGRectZero : CGRectMake(_values[0], _values[1], _values[2], _values[3]); + } + + Vector *Vector::new_cg_rect(const CGRect &r) + { + Vector *v = new Vector(4); + v->_values[0] = r.origin.x; + v->_values[1] = r.origin.y; + v->_values[2] = r.size.width; + v->_values[3] = r.size.height; + return v; + } + +#if TARGET_OS_IPHONE + + UIEdgeInsets Vector::ui_edge_insets() const + { + return _count < 4 ? UIEdgeInsetsZero : UIEdgeInsetsMake(_values[0], _values[1], _values[2], _values[3]); + } + + Vector *Vector::new_ui_edge_insets(const UIEdgeInsets &i) + { + Vector *v = new Vector(4); + v->_values[0] = i.top; + v->_values[1] = i.left; + v->_values[2] = i.bottom; + v->_values[3] = i.right; + return v; + } + +#endif + + CGAffineTransform Vector::cg_affine_transform() const + { + if (_count < 6) { + return CGAffineTransformIdentity; + } + + NSCAssert(size() >= 6, @"unexpected vector size:%lu", (unsigned long)size()); + CGAffineTransform t; + t.a = _values[0]; + t.b = _values[1]; + t.c = _values[2]; + t.d = _values[3]; + t.tx = _values[4]; + t.ty = _values[5]; + return t; + } + + Vector *Vector::new_cg_affine_transform(const CGAffineTransform &t) + { + Vector *v = new Vector(6); + v->_values[0] = t.a; + v->_values[1] = t.b; + v->_values[2] = t.c; + v->_values[3] = t.d; + v->_values[4] = t.tx; + v->_values[5] = t.ty; + return v; + } + + CGColorRef Vector::cg_color() const + { + if (_count < 4) { + return NULL; + } + return POPCGColorRGBACreate(_values); + } + + Vector *Vector::new_cg_color(CGColorRef color) + { + CGFloat rgba[4]; + POPCGColorGetRGBAComponents(color, rgba); + return new_vector(4, rgba); + } + +#if SCENEKIT_SDK_AVAILABLE + SCNVector3 Vector::scn_vector3() const + { + return _count < 3 ? SCNVector3Make(0.0, 0.0, 0.0) : SCNVector3Make(_values[0], _values[1], _values[2]); + } + + Vector *Vector::new_scn_vector3(const SCNVector3 &vec3) + { + Vector *v = new Vector(3); + v->_values[0] = vec3.x; + v->_values[1] = vec3.y; + v->_values[2] = vec3.z; + return v; + } + + SCNVector4 Vector::scn_vector4() const + { + return _count < 4 ? SCNVector4Make(0.0, 0.0, 0.0, 0.0) : SCNVector4Make(_values[0], _values[1], _values[2], _values[3]); + } + + Vector *Vector::new_scn_vector4(const SCNVector4 &vec4) + { + Vector *v = new Vector(4); + v->_values[0] = vec4.x; + v->_values[1] = vec4.y; + v->_values[2] = vec4.z; + v->_values[3] = vec4.w; + return v; + } +#endif + + void Vector::subRound(CGFloat sub) + { + for (NSUInteger idx = 0; idx < _count; idx++) { + _values[idx] = POPSubRound(_values[idx], sub); + } + } + + CGFloat Vector::norm() const + { + return sqrtr(squaredNorm()); + } + + CGFloat Vector::squaredNorm() const + { + CGFloat d = 0; + for (NSUInteger idx = 0; idx < _count; idx++) { + d += (_values[idx] * _values[idx]); + } + return d; + } + + NSString * Vector::toString() const + { + if (0 == _count) + return @"()"; + + if (1 == _count) + return [NSString stringWithFormat:@"%f", _values[0]]; + + if (2 == _count) + return [NSString stringWithFormat:@"(%.3f, %.3f)", _values[0], _values[1]]; + + NSMutableString *s = [NSMutableString stringWithCapacity:10]; + + for (NSUInteger idx = 0; idx < _count; idx++) { + if (0 == idx) { + [s appendFormat:@"[%.3f", _values[idx]]; + } else if (idx == _count - 1) { + [s appendFormat:@", %.3f]", _values[idx]]; + } else { + [s appendFormat:@", %.3f", _values[idx]]; + } + } + + return s; + + } +} diff --git a/submodules/LegacyComponents/LegacyComponents/PSCoding.h b/submodules/LegacyComponents/LegacyComponents/PSCoding.h new file mode 100644 index 0000000000..615783b235 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PSCoding.h @@ -0,0 +1,10 @@ +#import + +@class PSKeyValueCoder; + +@protocol PSCoding + +- (instancetype)initWithKeyValueCoder:(PSKeyValueCoder *)coder; +- (void)encodeWithKeyValueCoder:(PSKeyValueCoder *)coder; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PSData.h b/submodules/LegacyComponents/LegacyComponents/PSData.h new file mode 100644 index 0000000000..f0e333c994 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PSData.h @@ -0,0 +1,14 @@ +#ifndef __PSData_h +#define __PSData_h + +typedef struct +{ + uint8_t *data; + NSUInteger length; +} PSData; + +typedef PSData PSConstData; + +#define PSDataOffset(d, offset) {d.data += offset; d.length -= offset;} + +#endif diff --git a/submodules/LegacyComponents/LegacyComponents/PSKeyValueCoder.h b/submodules/LegacyComponents/LegacyComponents/PSKeyValueCoder.h new file mode 100644 index 0000000000..907bb72c8f --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PSKeyValueCoder.h @@ -0,0 +1,54 @@ +#import + +#import + +typedef enum { + PSKeyValueCoderFieldTypeString = 1, + PSKeyValueCoderFieldTypeInt32 = 2, + PSKeyValueCoderFieldTypeInt64 = 3, + PSKeyValueCoderFieldTypeCustomClass = 4, + PSKeyValueCoderFieldTypeArray = 5, + PSKeyValueCoderFieldTypeData = 6, + PSKeyValueCoderFieldTypeInt32Array = 7, + PSKeyValueCoderFieldTypeInt32Dictionary = 8, + PSKeyValueCoderFieldTypeDouble = 9 +} PSKeyValueCoderFieldType; + +@interface PSKeyValueCoder : NSObject + +- (void)encodeString:(NSString *)string forKey:(NSString *)key; +- (void)encodeString:(NSString *)string forCKey:(const char *)key; +- (void)encodeInt32:(int32_t)number forKey:(NSString *)key; +- (void)encodeInt32:(int32_t)number forCKey:(const char *)key; +- (void)encodeInt64:(int64_t)number forKey:(NSString *)key; +- (void)encodeInt64:(int64_t)number forCKey:(const char *)key; +- (void)encodeObject:(id)object forKey:(NSString *)key; +- (void)encodeObject:(id)object forCKey:(const char *)key; +- (void)encodeArray:(NSArray *)array forKey:(NSString *)key; +- (void)encodeArray:(NSArray *)array forCKey:(const char *)key; +- (void)encodeData:(NSData *)data forCKey:(const char *)key; +- (void)encodeBytes:(uint8_t const *)value length:(NSUInteger)length forCKey:(const char *)key; +- (void)encodeInt32Array:(NSArray *)value forCKey:(const char *)key; +- (void)encodeInt32Dictionary:(NSDictionary *)value forCKey:(const char *)key; +- (void)encodeDouble:(double)value forCKey:(const char *)key; + +- (NSString *)decodeStringForKey:(NSString *)key; +- (NSString *)decodeStringForCKey:(const char *)key; +- (int32_t)decodeInt32ForKey:(NSString *)key; +- (int32_t)decodeInt32ForCKey:(const char *)key; +- (int64_t)decodeInt64ForKey:(NSString *)key; +- (int64_t)decodeInt64ForCKey:(const char *)key; +- (id)decodeObjectForKey:(NSString *)key; +- (id)decodeObjectForCKey:(const char *)key; +- (NSArray *)decodeArrayForKey:(NSString *)key; +- (NSArray *)decodeArrayForCKey:(const char *)key; +- (NSData *)decodeDataCorCKey:(const char *)key; +- (void)decodeBytesForCKey:(const char *)key value:(uint8_t *)value length:(NSUInteger)length; +- (NSDictionary *)decodeObjectsByKeys; +- (NSArray *)decodeInt32ArrayForCKey:(const char *)key; +- (NSDictionary *)decodeInt32DictionaryForCKey:(const char *)key; +- (double)decodeDoubleForCKey:(const char *)key; + ++ (Class)classForName:(NSString *)name; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PSKeyValueCoder.m b/submodules/LegacyComponents/LegacyComponents/PSKeyValueCoder.m new file mode 100644 index 0000000000..20bd9382c0 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PSKeyValueCoder.m @@ -0,0 +1,180 @@ +#import "PSKeyValueCoder.h" + +#import + +pthread_rwlock_t classNameCacheLock = PTHREAD_RWLOCK_INITIALIZER; + +NSMutableDictionary *classNameCache() +{ + static NSMutableDictionary *dict = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^ + { + dict = [[NSMutableDictionary alloc] init]; + }); + + return dict; +} + +@implementation PSKeyValueCoder + ++ (Class)classForName:(NSString *)name +{ + if (name == nil) + return nil; + + Class result = nil; + + pthread_rwlock_rdlock(&classNameCacheLock); + result = [classNameCache() objectForKey:name]; + pthread_rwlock_unlock(&classNameCacheLock); + + if (result == nil) + { + result = NSClassFromString(name); + if (result != nil) + { + pthread_rwlock_wrlock(&classNameCacheLock); + classNameCache()[name] = result; + pthread_rwlock_unlock(&classNameCacheLock); + } + } + + return result; +} + +- (void)encodeString:(NSString *)__unused string forKey:(NSString *)__unused key +{ +} + +- (void)encodeInt32:(int32_t)__unused number forKey:(NSString *)__unused key +{ +} + +- (void)encodeInt64:(int64_t)__unused number forKey:(NSString *)__unused key +{ +} + +- (void)encodeObject:(id)__unused object forKey:(NSString *)__unused key +{ +} + +- (NSString *)decodeStringForKey:(NSString *)__unused key +{ + return nil; +} + +- (int32_t)decodeInt32ForKey:(NSString *)__unused key +{ + return 0; +} + +- (int64_t)decodeInt64ForKey:(NSString *)__unused key +{ + return 0; +} + +- (id)decodeObjectForKey:(NSString *)__unused key +{ + return nil; +} + +- (void)encodeString:(NSString *)__unused string forCKey:(const char *)__unused key +{ +} + +- (void)encodeInt32:(int32_t)__unused number forCKey:(const char *)__unused key +{ +} + +- (void)encodeInt64:(int64_t)__unused number forCKey:(const char *)__unused key +{ +} + +- (void)encodeObject:(id)__unused object forCKey:(const char *)__unused key +{ +} + +- (void)encodeArray:(NSArray *)__unused array forKey:(NSString *)__unused key +{ +} + +- (void)encodeArray:(NSArray *)__unused array forCKey:(const char *)__unused key +{ +} + +- (void)encodeData:(NSData *)__unused data forCKey:(const char *)__unused key +{ +} + +- (void)encodeBytes:(uint8_t const *)__unused value length:(NSUInteger)__unused length forCKey:(const char *)__unused key +{ +} + +- (void)encodeInt32Array:(NSArray *)__unused value forCKey:(const char *)__unused key +{ +} + +- (void)encodeInt32Dictionary:(NSDictionary *)__unused value forCKey:(const char *)__unused key { +} + +- (void)encodeDouble:(double)__unused value forCKey:(const char *)__unused key { +} + +- (NSString *)decodeStringForCKey:(const char *)__unused key +{ + return nil; +} + +- (int32_t)decodeInt32ForCKey:(const char *)__unused key +{ + return 0; +} + +- (int64_t)decodeInt64ForCKey:(const char *)__unused key +{ + return 0; +} + +- (id)decodeObjectForCKey:(const char *)__unused key +{ + return nil; +} + +- (NSArray *)decodeArrayForKey:(NSString *)__unused key +{ + return nil; +} + +- (NSArray *)decodeArrayForCKey:(const char *)__unused key +{ + return nil; +} + +- (NSData *)decodeDataCorCKey:(const char *)__unused key +{ + return nil; +} + +- (void)decodeBytesForCKey:(const char *)__unused key value:(uint8_t *)__unused value length:(NSUInteger)__unused length +{ +} + +- (NSDictionary *)decodeObjectsByKeys +{ + return nil; +} + +- (NSArray *)decodeInt32ArrayForCKey:(const char *)__unused key { + return nil; +} + +- (NSDictionary *)decodeInt32DictionaryForCKey:(const char *)__unused key { + return nil; +} + +- (double)decodeDoubleForCKey:(const char *)__unused key { + return 0.0f; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PSKeyValueDecoder.h b/submodules/LegacyComponents/LegacyComponents/PSKeyValueDecoder.h new file mode 100644 index 0000000000..1b031f2309 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PSKeyValueDecoder.h @@ -0,0 +1,12 @@ +#import + +@interface PSKeyValueDecoder : PSKeyValueCoder + +- (instancetype)init; +- (instancetype)initWithData:(NSData *)data; + +- (void)resetData:(NSData *)data; +- (void)resetBytes:(uint8_t const *)bytes length:(NSUInteger)length; +- (void)rewind; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PSKeyValueDecoder.m b/submodules/LegacyComponents/LegacyComponents/PSKeyValueDecoder.m new file mode 100644 index 0000000000..b9e695ce5f --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PSKeyValueDecoder.m @@ -0,0 +1,698 @@ +#import "PSKeyValueDecoder.h" + +#import + +@interface PSKeyValueDecoder () +{ + NSData *_data; + +@public + uint8_t const *_currentPtr; + uint8_t const *_begin; + uint8_t const *_end; + + PSKeyValueDecoder *_tempCoder; +} + +@end + +static uint32_t readLength(uint8_t const **currentPtr) +{ + uint32_t result = 0; + + result |= (*(*currentPtr)) & 127; + + if ((*(*currentPtr)) & 128) + { + (*currentPtr)++; + result |= ((*(*currentPtr)) & 127) << (7 * 1); + + if ((*(*currentPtr)) & 128) + { + (*currentPtr)++; + result |= ((*(*currentPtr)) & 127) << (7 * 2); + + if ((*(*currentPtr)) & 128) + { + (*currentPtr)++; + result |= ((*(*currentPtr)) & 127) << (7 * 3); + + if ((*(*currentPtr)) & 128) + { + (*currentPtr)++; + result |= ((*(*currentPtr)) & 127) << (7 * 4); + } + } + } + } + + (*currentPtr)++; + + return result; +} + +static NSString *readString(uint8_t const **currentPtr) +{ + uint32_t stringLength = readLength(currentPtr); + + NSString *string = [[NSString alloc] initWithBytes:*currentPtr length:stringLength encoding:NSUTF8StringEncoding]; + (*currentPtr) += stringLength; + return string; +} + +static void skipString(uint8_t const **currentPtr) +{ + uint32_t stringLength = readLength(currentPtr); + (*currentPtr) += stringLength; +} + +static int32_t readInt32(uint8_t const **currentPtr) +{ + int32_t number = *((int32_t *)(*currentPtr)); + (*currentPtr) += 4; + return number; +} + +static void skipInt32(uint8_t const **currentPtr) +{ + (*currentPtr) += 4; +} + +static int64_t readInt64(uint8_t const **currentPtr) +{ + int64_t number; + memcpy(&number, *currentPtr, 8); + + (*currentPtr) += 8; + return number; +} + +static double readDouble(uint8_t const **currentPtr) +{ + double number; + memcpy(&number, *currentPtr, 8); + + (*currentPtr) += 8; + return number; +} + +static void skipInt64(uint8_t const **currentPtr) +{ + (*currentPtr) += 8; +} + +static id readObject(uint8_t const **currentPtr, PSKeyValueDecoder *tempCoder) +{ + uint32_t objectLength = *((uint32_t *)(*currentPtr)); + (*currentPtr) += 4; + + uint8_t const *objectEnd = (*currentPtr) + objectLength; + + + const char *className = (const char *)(*currentPtr); + NSUInteger classNameLength = strlen(className) + 1; + (*currentPtr) += classNameLength; + + id object = nil; + + Class objectClass = objc_getClass(className); + if (objectClass != nil) + { + tempCoder->_begin = *currentPtr; + tempCoder->_end = objectEnd; + tempCoder->_currentPtr = tempCoder->_begin; + + object = [(id)[(id)objectClass alloc] initWithKeyValueCoder:tempCoder]; + } + + *currentPtr = objectEnd; + + return object; +} + +static void skipObject(uint8_t const **currentPtr) +{ + uint32_t objectLength = *((uint32_t *)(*currentPtr)); + (*currentPtr) += 4 + objectLength; +} + +static NSArray *readArray(uint8_t const **currentPtr, PSKeyValueDecoder *tempCoder) +{ + uint32_t objectLength = *((uint32_t *)(*currentPtr)); + (*currentPtr) += 4; + + uint8_t const *objectEnd = (*currentPtr) + objectLength; + + uint32_t count = readLength(currentPtr); + + NSMutableArray *array = [[NSMutableArray alloc] initWithCapacity:count]; + + for (uint32_t i = 0; i < count; i++) + { + id object = readObject(currentPtr, tempCoder); + if (object != nil) + [array addObject:object]; + } + + *currentPtr = objectEnd; + + return array; +} + +static void skipArray(uint8_t const **currentPtr) +{ + uint32_t objectLength = ((uint32_t *)*currentPtr)[0]; + (*currentPtr) += 4 + objectLength; +} + +static NSDictionary *readInt32Dictionary(uint8_t const **currentPtr, PSKeyValueDecoder *tempCoder) +{ + uint32_t objectLength = *((uint32_t *)(*currentPtr)); + (*currentPtr) += 4; + + uint8_t const *objectEnd = (*currentPtr) + objectLength; + + uint32_t count = readLength(currentPtr); + + NSMutableDictionary *dict = [[NSMutableDictionary alloc] initWithCapacity:count]; + + for (uint32_t i = 0; i < count; i++) + { + int32_t key = *((int32_t *)(*currentPtr)); + (*currentPtr) += 4; + + id object = readObject(currentPtr, tempCoder); + if (object != nil) { + dict[@(key)] = object; + } + } + + *currentPtr = objectEnd; + + return dict; +} + +static void skipInt32Dictionary(uint8_t const **currentPtr) +{ + uint32_t objectLength = ((uint32_t *)*currentPtr)[0]; + (*currentPtr) += 4 + objectLength; +} + +static NSData *readData(uint8_t const **currentPtr) +{ + uint32_t length = readLength(currentPtr); + + NSData *data = [[NSData alloc] initWithBytes:*currentPtr length:length]; + + *currentPtr += length; + + return data; +} + +static void readBytes(uint8_t const **currentPtr, uint8_t *value, NSUInteger maxLength) +{ + uint32_t length = readLength(currentPtr); + + memcpy(value, *currentPtr, MIN((uint32_t)maxLength, length)); + + *currentPtr += length; +} + +static void skipData(uint8_t const **currentPtr) +{ + uint32_t length = readLength(currentPtr); + (*currentPtr) += length; +} + +static void skipField(uint8_t const **currentPtr) +{ + uint8_t fieldType = *(*currentPtr); + (*currentPtr)++; + + switch (fieldType) + { + case PSKeyValueCoderFieldTypeString: + { + skipString(currentPtr); + break; + } + case PSKeyValueCoderFieldTypeInt32: + { + skipInt32(currentPtr); + break; + } + case PSKeyValueCoderFieldTypeInt64: + { + skipInt64(currentPtr); + break; + } + case PSKeyValueCoderFieldTypeCustomClass: + { + skipObject(currentPtr); + break; + } + case PSKeyValueCoderFieldTypeArray: + { + skipArray(currentPtr); + break; + } + case PSKeyValueCoderFieldTypeData: + { + skipData(currentPtr); + break; + } + case PSKeyValueCoderFieldTypeInt32Dictionary: + { + skipInt32Dictionary(currentPtr); + break; + } + case PSKeyValueCoderFieldTypeDouble: + { + skipInt64(currentPtr); + break; + } + default: + break; + } +} + +@implementation PSKeyValueDecoder + +- (instancetype)init +{ + self = [super init]; + if (self != nil) + { + + } + return self; +} + +- (instancetype)initWithData:(NSData *)data +{ + self = [super init]; + if (self != nil) + { + _data = data; + + _begin = (uint8_t const *)[_data bytes]; + _end = _begin + [_data length]; + _currentPtr = _begin; + } + return self; +} + +- (void)resetData:(NSData *)data +{ + _data = data; + + _begin = (uint8_t const *)[_data bytes]; + _end = _begin + [_data length]; + _currentPtr = _begin; +} + +- (void)resetBytes:(uint8_t const *)bytes length:(NSUInteger)length +{ + _data = nil; + + _begin = bytes; + _end = _begin + length; + _currentPtr = _begin; +} + +- (void)rewind { + _begin = (uint8_t const *)[_data bytes]; + _end = _begin + [_data length]; + _currentPtr = _begin; +} + +static bool skipToValueForRawKey(PSKeyValueDecoder *self, uint8_t const *key, NSUInteger keyLength) +{ + uint8_t const *middlePtr = self->_currentPtr; + + for (int i = 0; i < 2; i++) + { + uint8_t const *scanEnd = self->_end; + + if (i == 1) + { + self->_currentPtr = self->_begin; + scanEnd = middlePtr; + } + + while (self->_currentPtr < scanEnd) + { + uint32_t compareKeyLength = readLength(&self->_currentPtr); + + if (compareKeyLength != keyLength || memcmp(key, self->_currentPtr, keyLength)) + { + if (compareKeyLength > 1000) { + return false; + } + + self->_currentPtr += compareKeyLength; + skipField(&self->_currentPtr); + + continue; + } + + self->_currentPtr += compareKeyLength; + + return true; + } + } + + return false; +} + +static NSString *decodeStringForRawKey(PSKeyValueDecoder *self, uint8_t const *key, NSUInteger keyLength) +{ + if (skipToValueForRawKey(self, key, keyLength)) + { + uint8_t fieldType = *self->_currentPtr; + self->_currentPtr++; + + if (fieldType == PSKeyValueCoderFieldTypeString) + return readString(&self->_currentPtr); + else if (fieldType == PSKeyValueCoderFieldTypeInt32) + return [[NSString alloc] initWithFormat:@"%" PRId32 "", readInt32(&self->_currentPtr)]; + else if (fieldType == PSKeyValueCoderFieldTypeInt64) + return [[NSString alloc] initWithFormat:@"%" PRId64 "", readInt64(&self->_currentPtr)]; + else + { + skipField(&self->_currentPtr); + + return nil; + } + } + + return nil; +} + +- (NSString *)decodeStringForKey:(NSString *)key +{ + NSData *keyData = [key dataUsingEncoding:NSUTF8StringEncoding]; + return decodeStringForRawKey(self, (uint8_t const *)[keyData bytes], [keyData length]); +} + +- (NSString *)decodeStringForCKey:(const char *)key +{ + return decodeStringForRawKey(self, (uint8_t const *)key, (NSUInteger)strlen(key)); +} + +static int32_t decodeInt32ForRawKey(PSKeyValueDecoder *self, uint8_t const *key, NSUInteger keyLength) +{ + if (skipToValueForRawKey(self, key, keyLength)) + { + uint8_t fieldType = *self->_currentPtr; + self->_currentPtr++; + + if (fieldType == PSKeyValueCoderFieldTypeString) + return (int32_t)[readString(&self->_currentPtr) intValue]; + else if (fieldType == PSKeyValueCoderFieldTypeInt32) + return readInt32(&self->_currentPtr); + else if (fieldType == PSKeyValueCoderFieldTypeInt64) + return (int32_t)readInt64(&self->_currentPtr); + else + { + skipField(&self->_currentPtr); + + return 0; + } + } + + return 0; +} + +- (int32_t)decodeInt32ForKey:(NSString *)key +{ + NSData *keyData = [key dataUsingEncoding:NSUTF8StringEncoding]; + return decodeInt32ForRawKey(self, (uint8_t const *)[keyData bytes], [keyData length]); +} + +- (int32_t)decodeInt32ForCKey:(const char *)key +{ + return decodeInt32ForRawKey(self, (uint8_t const *)key, strlen(key)); +} + +static int64_t decodeInt64ForRawKey(PSKeyValueDecoder *self, uint8_t const *key, NSUInteger keyLength) +{ + if (skipToValueForRawKey(self, key, keyLength)) + { + uint8_t fieldType = *self->_currentPtr; + self->_currentPtr++; + + if (fieldType == PSKeyValueCoderFieldTypeString) + return (int64_t)[readString(&self->_currentPtr) longLongValue]; + else if (fieldType == PSKeyValueCoderFieldTypeInt32) + return readInt32(&self->_currentPtr); + else if (fieldType == PSKeyValueCoderFieldTypeInt64) + return readInt64(&self->_currentPtr); + else + { + skipField(&self->_currentPtr); + return 0; + } + } + + return 0; +} + +- (int64_t)decodeInt64ForKey:(NSString *)key +{ + NSData *keyData = [key dataUsingEncoding:NSUTF8StringEncoding]; + return decodeInt64ForRawKey(self, (uint8_t const *)[keyData bytes], [keyData length]); +} + +- (int64_t)decodeInt64ForCKey:(const char *)key +{ + return decodeInt64ForRawKey(self, (uint8_t const *)key, strlen(key)); +} + +static id decodeObjectForRawKey(PSKeyValueDecoder *self, uint8_t const *key, NSUInteger keyLength) +{ + if (skipToValueForRawKey(self, key, keyLength)) + { + uint8_t fieldType = *self->_currentPtr; + self->_currentPtr++; + + if (fieldType == PSKeyValueCoderFieldTypeCustomClass) + { + if (self->_tempCoder == nil) + self->_tempCoder = [[PSKeyValueDecoder alloc] init]; + return readObject(&self->_currentPtr, self->_tempCoder); + } + else + { + skipField(&self->_currentPtr); + + return nil; + } + } + + return nil; +} + +- (id)decodeObjectForKey:(NSString *)key +{ + NSData *keyData = [key dataUsingEncoding:NSUTF8StringEncoding]; + return decodeObjectForRawKey(self, (uint8_t const *)[keyData bytes], [keyData length]); +} + +- (id)decodeObjectForCKey:(const char *)key +{ + return decodeObjectForRawKey(self, (uint8_t const *)key, strlen(key)); +} + +static NSArray *decodeArrayForRawKey(PSKeyValueDecoder *self, uint8_t const *key, NSUInteger keyLength) +{ + if (skipToValueForRawKey(self, key, keyLength)) + { + uint8_t fieldType = *self->_currentPtr; + self->_currentPtr++; + + if (fieldType == PSKeyValueCoderFieldTypeArray) + { + if (self->_tempCoder == nil) + self->_tempCoder = [[PSKeyValueDecoder alloc] init]; + return readArray(&self->_currentPtr, self->_tempCoder); + } + else + { + skipField(&self->_currentPtr); + + return nil; + } + } + + return nil; +} + +- (NSArray *)decodeArrayForKey:(NSString *)key +{ + NSData *keyData = [key dataUsingEncoding:NSUTF8StringEncoding]; + return decodeArrayForRawKey(self, (uint8_t const *)[keyData bytes], [keyData length]); +} + +- (NSArray *)decodeArrayForCKey:(const char *)key +{ + return decodeArrayForRawKey(self, (uint8_t const *)key, strlen(key)); +} + +static NSData *decodeDataForRawKey(PSKeyValueDecoder *self, uint8_t const *key, NSUInteger keyLength) +{ + if (skipToValueForRawKey(self, key, keyLength)) + { + uint8_t fieldType = *self->_currentPtr; + self->_currentPtr++; + + if (fieldType == PSKeyValueCoderFieldTypeData) + { + return readData(&self->_currentPtr); + } + else + { + skipField(&self->_currentPtr); + + return nil; + } + } + + return nil; +} + +static void decodeBytesForRawKey(PSKeyValueDecoder *self, uint8_t const *key, NSUInteger keyLength, uint8_t *value, NSUInteger maxLength) +{ + if (skipToValueForRawKey(self, key, keyLength)) + { + uint8_t fieldType = *self->_currentPtr; + self->_currentPtr++; + + if (fieldType == PSKeyValueCoderFieldTypeData) + { + readBytes(&self->_currentPtr, value, maxLength); + } + else + { + skipField(&self->_currentPtr); + } + } +} + +- (NSData *)decodeDataCorCKey:(const char *)key +{ + return decodeDataForRawKey(self, (uint8_t const *)key, strlen(key)); +} + +- (void)decodeBytesForCKey:(const char *)key value:(uint8_t *)value length:(NSUInteger)length +{ + decodeBytesForRawKey(self, (uint8_t const *)key, strlen(key), value, length); +} + +- (NSDictionary *)decodeObjectsByKeys +{ + NSMutableDictionary *dict = [[NSMutableDictionary alloc] init]; + + if (self->_tempCoder == nil) + self->_tempCoder = [[PSKeyValueDecoder alloc] init]; + + self->_currentPtr = self->_begin; + while (self->_currentPtr < self->_end) + { + uint32_t keyLength = readLength(&self->_currentPtr); + NSString *key = [[NSString alloc] initWithBytes:self->_currentPtr length:keyLength encoding:NSUTF8StringEncoding]; + self->_currentPtr += keyLength; + + uint8_t fieldType = *self->_currentPtr; + self->_currentPtr++; + + if (fieldType == PSKeyValueCoderFieldTypeCustomClass) + { + id value = readObject(&self->_currentPtr, self->_tempCoder); + if (value == nil) + continue; + + dict[key] = value; + } + else + break; + } + + self->_currentPtr = self->_begin; + + return dict; +} + +- (NSArray *)decodeInt32ArrayForCKey:(const char *)key { + if (skipToValueForRawKey(self, (void *)key, strlen(key))) { + uint8_t fieldType = *self->_currentPtr; + self->_currentPtr++; + + if (fieldType == PSKeyValueCoderFieldTypeInt32Array) { + int32_t count = 0; + memcpy(&count, self->_currentPtr, 4); + self->_currentPtr += 4; + NSMutableArray *array = [[NSMutableArray alloc] init]; + + for (int32_t i = 0; i < count; i++) { + int32_t number = 0; + memcpy(&number, self->_currentPtr, 4); + self->_currentPtr += 4; + [array addObject:@(number)]; + } + + return array; + } else { + self->_currentPtr--; + skipField(&self->_currentPtr); + return nil; + } + } else { + return nil; + } +} + +- (NSDictionary *)decodeInt32DictionaryForCKey:(const char *)key { + if (skipToValueForRawKey(self, (uint8_t const *)key, strlen(key))) { + uint8_t fieldType = *self->_currentPtr; + self->_currentPtr++; + + if (fieldType == PSKeyValueCoderFieldTypeInt32Dictionary) { + if (self->_tempCoder == nil) + self->_tempCoder = [[PSKeyValueDecoder alloc] init]; + return readInt32Dictionary(&self->_currentPtr, self->_tempCoder); + } + else { + skipField(&self->_currentPtr); + } + } + + return nil; +} + +- (double)decodeDoubleForCKey:(const char *)key { + if (skipToValueForRawKey(self, (uint8_t const *)key, strlen(key))) + { + uint8_t fieldType = *self->_currentPtr; + self->_currentPtr++; + + if (fieldType == PSKeyValueCoderFieldTypeString) + return (int32_t)[readString(&self->_currentPtr) doubleValue]; + else if (fieldType == PSKeyValueCoderFieldTypeInt32) + return readInt32(&self->_currentPtr); + else if (fieldType == PSKeyValueCoderFieldTypeInt64) + return (double)readInt64(&self->_currentPtr); + else if (fieldType == PSKeyValueCoderFieldTypeDouble) + return readDouble(&self->_currentPtr); + else + { + skipField(&self->_currentPtr); + + return 0; + } + } + + return 0.0; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PSKeyValueEncoder.h b/submodules/LegacyComponents/LegacyComponents/PSKeyValueEncoder.h new file mode 100644 index 0000000000..c3d12f1b91 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PSKeyValueEncoder.h @@ -0,0 +1,8 @@ +#import + +@interface PSKeyValueEncoder : PSKeyValueCoder + +- (void)reset; +- (NSData *)data; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PSKeyValueEncoder.m b/submodules/LegacyComponents/LegacyComponents/PSKeyValueEncoder.m new file mode 100644 index 0000000000..23bb690b58 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PSKeyValueEncoder.m @@ -0,0 +1,373 @@ +#import "PSKeyValueEncoder.h" + +#import + +@interface PSKeyValueEncoder () +{ +@public + NSMutableData *_data; +} + +@end + +@implementation PSKeyValueEncoder + +- (instancetype)init +{ + self = [super init]; + if (self != nil) + { + _data = [[NSMutableData alloc] init]; + } + return self; +} + +static inline void writeLength(PSKeyValueEncoder *self, uint32_t value) +{ + uint8_t bytes[5]; + int length = 0; + + if (value > 127) + { + bytes[length++] = ((uint8_t)(value & 127)) | 128; + value >>= 7; + } + + if (value > 127) + { + bytes[length++] = ((uint8_t)(value & 127)) | 128; + value >>= 7; + } + + if (value > 127) + { + bytes[length++] = ((uint8_t)(value & 127)) | 128; + value >>= 7; + } + + if (value > 127) + { + bytes[length++] = ((uint8_t)(value & 127)) | 128; + value >>= 7; + } + + bytes[length++] = ((uint8_t)(value & 127)); + + [self->_data appendBytes:bytes length:length]; +} + +- (void)encodeString:(NSString *)string forKey:(NSString *)key +{ + if (key == nil || string == nil) + return; + + NSData *keyData = [key dataUsingEncoding:NSUTF8StringEncoding]; + writeLength(self, (uint32_t)keyData.length); + [_data appendData:keyData]; + + uint8_t fieldType = PSKeyValueCoderFieldTypeString; + [_data appendBytes:&fieldType length:1]; + + NSData *stringData = [string dataUsingEncoding:NSUTF8StringEncoding]; + writeLength(self, (uint32_t)stringData.length); + [_data appendData:stringData]; +} + +- (void)encodeString:(NSString *)string forCKey:(const char *)key +{ + if (key == nil || string == nil) + return; + + uint32_t keyLength = (uint32_t)strlen(key); + writeLength(self, keyLength); + [_data appendBytes:key length:keyLength]; + + uint8_t fieldType = PSKeyValueCoderFieldTypeString; + [_data appendBytes:&fieldType length:1]; + + NSData *stringData = [string dataUsingEncoding:NSUTF8StringEncoding]; + writeLength(self, (uint32_t)stringData.length); + [_data appendData:stringData]; +} + +- (void)encodeInt32:(int32_t)number forKey:(NSString *)key +{ + if (key == nil) + return; + + NSData *keyData = [key dataUsingEncoding:NSUTF8StringEncoding]; + writeLength(self, (uint32_t)keyData.length); + [_data appendData:keyData]; + + uint8_t fieldType = PSKeyValueCoderFieldTypeInt32; + [_data appendBytes:&fieldType length:1]; + + [_data appendBytes:&number length:4]; +} + +- (void)encodeInt32:(int32_t)number forCKey:(const char *)key +{ + if (key == nil) + return; + + uint32_t keyLength = (uint32_t)strlen(key); + writeLength(self, keyLength); + [_data appendBytes:key length:keyLength]; + + uint8_t fieldType = PSKeyValueCoderFieldTypeInt32; + [_data appendBytes:&fieldType length:1]; + + [_data appendBytes:&number length:4]; +} + +- (void)encodeInt64:(int64_t)number forKey:(NSString *)key +{ + if (key == nil) + return; + + NSData *keyData = [key dataUsingEncoding:NSUTF8StringEncoding]; + writeLength(self, (uint32_t)keyData.length); + [_data appendData:keyData]; + + uint8_t fieldType = PSKeyValueCoderFieldTypeInt64; + [_data appendBytes:&fieldType length:1]; + + [_data appendBytes:&number length:8]; +} + +- (void)encodeInt64:(int64_t)number forCKey:(const char *)key +{ + if (key == nil) + return; + + uint32_t keyLength = (uint32_t)strlen(key); + writeLength(self, keyLength); + [_data appendBytes:key length:keyLength]; + + uint8_t fieldType = PSKeyValueCoderFieldTypeInt64; + [_data appendBytes:&fieldType length:1]; + + [_data appendBytes:&number length:8]; +} + +- (void)encodeObject:(id)object forKey:(NSString *)key +{ + if (key == nil || object == nil) + return; + + NSData *keyData = [key dataUsingEncoding:NSUTF8StringEncoding]; + writeLength(self, (uint32_t)keyData.length); + [_data appendData:keyData]; + + uint8_t fieldType = PSKeyValueCoderFieldTypeCustomClass; + [_data appendBytes:&fieldType length:1]; + + uint32_t objectLength = 0; + NSUInteger objectLengthPosition = [_data length]; + [_data appendBytes:&objectLength length:4]; + + NSString *className = NSStringFromClass([object class]); + NSData *classNameData = [className dataUsingEncoding:NSUTF8StringEncoding]; + [_data appendData:classNameData]; + uint8_t zero = 0; + [_data appendBytes:&zero length:1]; + + [object encodeWithKeyValueCoder:self]; + + objectLength = (int)([_data length] - objectLengthPosition - 4); + [_data replaceBytesInRange:NSMakeRange(objectLengthPosition, 4) withBytes:&objectLength]; +} + +static void encodeObjectValue(PSKeyValueEncoder *self, id object) +{ + uint32_t objectLength = 0; + NSUInteger objectLengthPosition = [self->_data length]; + [self->_data appendBytes:&objectLength length:4]; + + Class objectClass = object_getClass(object); + const char *className = class_getName(objectClass); + [self->_data appendBytes:className length:strlen(className) + 1]; + + [object encodeWithKeyValueCoder:self]; + + objectLength = (int)([self->_data length] - objectLengthPosition - 4); + [self->_data replaceBytesInRange:NSMakeRange(objectLengthPosition, 4) withBytes:&objectLength]; +} + +- (void)encodeObject:(id)object forCKey:(const char *)key +{ + if (key == nil || object == nil) + return; + + uint32_t keyLength = (uint32_t)strlen(key); + writeLength(self, keyLength); + [_data appendBytes:key length:keyLength]; + + uint8_t fieldType = PSKeyValueCoderFieldTypeCustomClass; + [_data appendBytes:&fieldType length:1]; + + encodeObjectValue(self, object); +} + +- (void)encodeArray:(NSArray *)array forKey:(NSString *)key +{ + if (key == nil || array == nil) + return; + + NSData *keyData = [key dataUsingEncoding:NSUTF8StringEncoding]; + writeLength(self, (uint32_t)keyData.length); + [_data appendData:keyData]; + + uint8_t fieldType = PSKeyValueCoderFieldTypeArray; + [_data appendBytes:&fieldType length:1]; + + uint32_t objectLength = 0; + NSUInteger objectLengthPosition = [self->_data length]; + [self->_data appendBytes:&objectLength length:4]; + + uint32_t count = (uint32_t)[array count]; + writeLength(self, count); + + for (uint32_t i = 0; i < count; i++) + { + encodeObjectValue(self, array[i]); + } + + objectLength = (int)([_data length] - objectLengthPosition - 4); + [_data replaceBytesInRange:NSMakeRange(objectLengthPosition, 4) withBytes:&objectLength]; +} + +- (void)encodeArray:(NSArray *)array forCKey:(const char *)key +{ + if (key == nil || array == nil) + return; + + uint32_t keyLength = (uint32_t)strlen(key); + writeLength(self, keyLength); + [_data appendBytes:key length:keyLength]; + + uint8_t fieldType = PSKeyValueCoderFieldTypeArray; + [_data appendBytes:&fieldType length:1]; + + uint32_t objectLength = 0; + NSUInteger objectLengthPosition = [self->_data length]; + [self->_data appendBytes:&objectLength length:4]; + + uint32_t count = (uint32_t)[array count]; + writeLength(self, count); + + for (uint32_t i = 0; i < count; i++) + { + encodeObjectValue(self, array[i]); + } + + objectLength = (int)([_data length] - objectLengthPosition - 4); + [_data replaceBytesInRange:NSMakeRange(objectLengthPosition, 4) withBytes:&objectLength]; +} + +- (void)encodeData:(NSData *)data forCKey:(const char *)key +{ + if (key == nil || data == nil) + return; + + uint32_t keyLength = (uint32_t)strlen(key); + writeLength(self, keyLength); + [_data appendBytes:key length:keyLength]; + + uint8_t fieldType = PSKeyValueCoderFieldTypeData; + [_data appendBytes:&fieldType length:1]; + + writeLength(self, (uint32_t)data.length); + [_data appendData:data]; +} + +- (void)encodeBytes:(uint8_t const *)value length:(NSUInteger)length forCKey:(const char *)key +{ + if (key == nil) + return; + + uint32_t keyLength = (uint32_t)strlen(key); + writeLength(self, keyLength); + [_data appendBytes:key length:keyLength]; + + uint8_t fieldType = PSKeyValueCoderFieldTypeData; + [_data appendBytes:&fieldType length:1]; + + writeLength(self, (uint32_t)length); + [_data appendBytes:value length:length]; +} + +- (void)encodeInt32Array:(NSArray *)value forCKey:(const char *)key { + if (key == NULL) { + return; + } + + uint32_t keyLength = (uint32_t)strlen(key); + writeLength(self, keyLength); + [_data appendBytes:key length:keyLength]; + + uint8_t fieldType = PSKeyValueCoderFieldTypeInt32Array; + [_data appendBytes:&fieldType length:1]; + + int32_t count = (int32_t)value.count; + [_data appendBytes:&count length:4]; + + for (NSNumber *nNumber in value) { + int32_t number = [nNumber intValue]; + [_data appendBytes:&number length:4]; + } +} + +- (void)encodeInt32Dictionary:(NSDictionary *)value forCKey:(const char *)key { + if (key == NULL) { + return; + } + + uint32_t keyLength = (uint32_t)strlen(key); + writeLength(self, keyLength); + [_data appendBytes:key length:keyLength]; + + uint8_t fieldType = PSKeyValueCoderFieldTypeInt32Dictionary; + [_data appendBytes:&fieldType length:1]; + + uint32_t objectLength = 0; + NSUInteger objectLengthPosition = [self->_data length]; + [self->_data appendBytes:&objectLength length:4]; + + NSArray *allKeys = [value allKeys]; + uint32_t count = (uint32_t)[allKeys count]; + writeLength(self, count); + + for (NSNumber *nKey in allKeys) { + id object = value[nKey]; + int32_t objectKey = [nKey intValue]; + [self->_data appendBytes:&objectKey length:4]; + encodeObjectValue(self, object); + } + + objectLength = (int)([_data length] - objectLengthPosition - 4); + [_data replaceBytesInRange:NSMakeRange(objectLengthPosition, 4) withBytes:&objectLength]; +} + +- (void)encodeDouble:(double)value forCKey:(const char *)key { + uint32_t keyLength = (uint32_t)strlen(key); + writeLength(self, keyLength); + [_data appendBytes:key length:keyLength]; + + uint8_t fieldType = PSKeyValueCoderFieldTypeDouble; + [_data appendBytes:&fieldType length:1]; + + [_data appendBytes:&value length:8]; +} + +- (void)reset +{ + [_data setLength:0]; +} + +- (NSData *)data +{ + return _data; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PSKeyValueReader.h b/submodules/LegacyComponents/LegacyComponents/PSKeyValueReader.h new file mode 100644 index 0000000000..be958a4d1c --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PSKeyValueReader.h @@ -0,0 +1,24 @@ +#import + +#import + +typedef enum { + PSKeyValueReaderSelectLowerKey = 0, + PSKeyValueReaderSelectHigherKey = 1 +} PSKeyValueReaderSelectKey; + +typedef enum { + PSKeyValueReaderEnumerationReverse = 1, + PSKeyValueReaderEnumerationLowerBoundExclusive = 2, + PSKeyValueReaderEnumerationUpperBoundExclusive = 4 +} PSKeyValueReaderEnumerationOptions; + +@protocol PSKeyValueReader + +- (bool)readValueForRawKey:(PSConstData *)key value:(PSConstData *)value; + +- (bool)readValueBetweenLowerBoundKey:(PSConstData *)lowerBoundKey upperBoundKey:(PSConstData *)upperBoundKey selectKey:(PSKeyValueReaderSelectKey)selectKey selectedKey:(PSConstData *)selectedKey selectedValue:(PSConstData *)selectedValue; + +- (void)enumerateKeysAndValuesBetweenLowerBoundKey:(PSConstData *)lowerBoundKey upperBoundKey:(PSConstData *)upperBoundKey options:(NSInteger)options withBlock:(void (^)(PSConstData *key, PSConstData *value, bool *stop))block; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PSKeyValueStore.h b/submodules/LegacyComponents/LegacyComponents/PSKeyValueStore.h new file mode 100644 index 0000000000..a9dd0d2289 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PSKeyValueStore.h @@ -0,0 +1,11 @@ +#import +#import + +@protocol PSKeyValueStore + +- (void)readInTransaction:(void (^)(id))transaction; +- (void)readWriteInTransaction:(void (^)(id))transaction; + +- (void)sync; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PSKeyValueWriter.h b/submodules/LegacyComponents/LegacyComponents/PSKeyValueWriter.h new file mode 100644 index 0000000000..6e668f5ba1 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PSKeyValueWriter.h @@ -0,0 +1,11 @@ +#import + +#import + +@protocol PSKeyValueWriter + +- (void)writeValueForRawKey:(const uint8_t *)key keyLength:(NSUInteger)keyLength value:(const uint8_t *)value valueLength:(NSUInteger)valueLength; +- (bool)deleteValueForRawKey:(PSData *)key; +- (void)deleteAllValues; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PSLMDBKeyValueCursor.h b/submodules/LegacyComponents/LegacyComponents/PSLMDBKeyValueCursor.h new file mode 100644 index 0000000000..6f86e133a0 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PSLMDBKeyValueCursor.h @@ -0,0 +1,20 @@ +#import + +#import + +@class PSLMDBTable; + +typedef enum { + PSKeyValueCursorDirectionForward = 0, + PSKeyValueCursorDirectionBack = 1 +} PSKeyValueCursorDirection; + +@interface PSLMDBKeyValueCursor : NSObject + +- (instancetype)initWithTable:(PSLMDBTable *)table transaction:(MDB_txn *)transaction cursor:(MDB_cursor *)cursor; + +- (bool)positionAt:(uint8_t const **)key keyLength:(NSUInteger *)keyLength value:(uint8_t const **)value valueLength:(NSUInteger *)valueLength directionIfNotFound:(PSKeyValueCursorDirection)directionIfNotFound; +- (bool)previous:(uint8_t const **)key keyLength:(NSUInteger *)keyLength value:(uint8_t const **)value valueLength:(NSUInteger *)valueLength; +- (bool)next:(uint8_t const **)key keyLength:(NSUInteger *)keyLength value:(uint8_t const **)value valueLength:(NSUInteger *)valueLength; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PSLMDBKeyValueCursor.m b/submodules/LegacyComponents/LegacyComponents/PSLMDBKeyValueCursor.m new file mode 100644 index 0000000000..acb42e2250 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PSLMDBKeyValueCursor.m @@ -0,0 +1,139 @@ +#import "PSLMDBKeyValueCursor.h" + +#import "LegacyComponentsInternal.h" + +#import "PSLMDBTable.h" + +@interface PSLMDBKeyValueCursor () +{ + MDB_dbi _dbi; + MDB_txn *_txn; + MDB_cursor *_cursor; +} + +@end + +@implementation PSLMDBKeyValueCursor + +- (instancetype)initWithTable:(PSLMDBTable *)table transaction:(MDB_txn *)transaction cursor:(MDB_cursor *)cursor +{ + self = [super init]; + if (self != nil) + { + _dbi = table.dbi; + _txn = transaction; + _cursor = cursor; + } + return self; +} + +- (bool)positionAt:(uint8_t const **)key keyLength:(NSUInteger *)keyLength value:(uint8_t const **)value valueLength:(NSUInteger *)valueLength directionIfNotFound:(PSKeyValueCursorDirection)directionIfNotFound +{ + if (key == NULL || keyLength == 0) + return false; + + MDB_val mdbKey = {.mv_data = (void *)*key, .mv_size = *keyLength}; + MDB_val mdbData = {.mv_data = NULL, .mv_size = 0}; + + int rc = 0; + rc = mdb_cursor_get(_cursor, &mdbKey, &mdbData, directionIfNotFound == PSKeyValueCursorDirectionForward ? MDB_SET_RANGE : MDB_SET_KEY); + + if (rc == MDB_SUCCESS) + { + *key = mdbKey.mv_data; + *keyLength = mdbKey.mv_size; + + if (value != NULL) + *value = mdbData.mv_data; + + if (valueLength != NULL) + *valueLength = mdbData.mv_size; + + return true; + } + else if (rc == MDB_NOTFOUND) + { + if (directionIfNotFound == PSKeyValueCursorDirectionBack) + return [self previous:key keyLength:keyLength value:value valueLength:valueLength]; + } + else + { + TGLegacyLog(@"[PSLMDBKeyValueReader mdb_cursor_get error %d]", rc); + + return false; + } + + return false; +} + +- (bool)previous:(uint8_t const **)key keyLength:(NSUInteger *)keyLength value:(uint8_t const **)value valueLength:(NSUInteger *)valueLength +{ + if (key == NULL || keyLength == 0) + return false; + + MDB_val mdbKey = {.mv_data = NULL, .mv_size = 0}; + MDB_val mdbData = {.mv_data = NULL, .mv_size = 0}; + + int rc = 0; + rc = mdb_cursor_get(_cursor, &mdbKey, &mdbData, MDB_PREV); + + if (rc == MDB_SUCCESS) + { + *key = mdbKey.mv_data; + *keyLength = mdbKey.mv_size; + + if (value != NULL) + *value = mdbData.mv_data; + + if (valueLength != NULL) + *valueLength = mdbData.mv_size; + + return true; + } + else + { + if (rc != MDB_NOTFOUND) + TGLegacyLog(@"[PSLMDBKeyValueReader mdb_cursor_get error %d]", rc); + + return false; + } + + return false; +} + +- (bool)next:(uint8_t const **)key keyLength:(NSUInteger *)keyLength value:(uint8_t const **)value valueLength:(NSUInteger *)valueLength +{ + if (key == NULL || keyLength == 0) + return false; + + MDB_val mdbKey = {.mv_data = NULL, .mv_size = 0}; + MDB_val mdbData = {.mv_data = NULL, .mv_size = 0}; + + int rc = 0; + rc = mdb_cursor_get(_cursor, &mdbKey, &mdbData, MDB_NEXT); + + if (rc == MDB_SUCCESS) + { + *key = mdbKey.mv_data; + *keyLength = mdbKey.mv_size; + + if (value != NULL) + *value = mdbData.mv_data; + + if (valueLength != NULL) + *valueLength = mdbData.mv_size; + + return true; + } + else + { + if (rc != MDB_NOTFOUND) + TGLegacyLog(@"[PSLMDBKeyValueReader mdb_cursor_get error %d]", rc); + + return false; + } + + return false; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PSLMDBKeyValueReaderWriter.h b/submodules/LegacyComponents/LegacyComponents/PSLMDBKeyValueReaderWriter.h new file mode 100644 index 0000000000..2d2e8097cb --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PSLMDBKeyValueReaderWriter.h @@ -0,0 +1,11 @@ +#import +#import +#import + +@class PSLMDBTable; + +@interface PSLMDBKeyValueReaderWriter : NSObject + +- (instancetype)initWithTable:(PSLMDBTable *)table transaction:(MDB_txn *)transaction; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PSLMDBKeyValueReaderWriter.m b/submodules/LegacyComponents/LegacyComponents/PSLMDBKeyValueReaderWriter.m new file mode 100644 index 0000000000..0d3462fcf6 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PSLMDBKeyValueReaderWriter.m @@ -0,0 +1,270 @@ +#import "PSLMDBKeyValueReaderWriter.h" + +#import "LegacyComponentsInternal.h" + +#import "PSLMDBTable.h" +#import "PSLMDBKeyValueCursor.h" + +@interface PSLMDBKeyValueReaderWriter () +{ + PSLMDBTable *_table; + MDB_dbi _dbi; + MDB_txn *_txn; +} + +@end + +@implementation PSLMDBKeyValueReaderWriter + +- (instancetype)initWithTable:(PSLMDBTable *)table transaction:(MDB_txn *)transaction +{ + self = [super init]; + if (self != nil) + { + _table = table; + _dbi = table.dbi; + _txn = transaction; + } + return self; +} + +- (bool)readValueForRawKey:(PSConstData *)key value:(PSConstData *)value +{ + if (key == NULL) + return false; + + MDB_val mdbKey; + MDB_val mdbData; + + mdbKey.mv_data = (uint8_t *)key->data; + mdbKey.mv_size = (size_t)key->length; + + int rc = 0; + rc = mdb_get(_txn, _dbi, &mdbKey, &mdbData); + + if (rc == MDB_SUCCESS) + { + if (value != NULL) + { + value->data = mdbData.mv_data; + value->length = (NSUInteger)mdbData.mv_size; + } + + return true; + } + else + { + if (rc != MDB_NOTFOUND) + TGLegacyLog(@"[PSLMDBKeyValueReader mdb_get error %d]", rc); + + return false; + } +} + +- (void)writeValueForRawKey:(const uint8_t *)key keyLength:(NSUInteger)keyLength value:(const uint8_t *)value valueLength:(NSUInteger)valueLength +{ + if (key == NULL || keyLength == 0) + return; + + MDB_val mdbKey; + MDB_val mdbData; + + mdbKey.mv_data = (uint8_t *)key; + mdbKey.mv_size = keyLength; + + mdbData.mv_data = (uint8_t *)value; + mdbData.mv_size = valueLength; + + int rc = 0; + rc = mdb_put(_txn, _dbi, &mdbKey, &mdbData, 0); + + if (rc != MDB_SUCCESS) + TGLegacyLog(@"[PSLMDBKeyValueWriter mdb_put error %d]", rc); +} + +- (void)readWithCursor:(void (^)(PSLMDBKeyValueCursor *))readWithCursorBlock +{ + if (!readWithCursorBlock) + return; + + MDB_cursor *cursor = NULL; + int rc = 0; + rc = mdb_cursor_open(_txn, _dbi, &cursor); + if (rc == MDB_SUCCESS) + { + readWithCursorBlock([[PSLMDBKeyValueCursor alloc] initWithTable:_table transaction:_txn cursor:cursor]); + + mdb_cursor_close(cursor); + } + else + TGLegacyLog(@"[PSLMDBKeyValueWriter mdb_cursor_open error %d]", rc); +} + +- (bool)readValueBetweenLowerBoundKey:(PSConstData *)lowerBoundKey upperBoundKey:(PSConstData *)upperBoundKey selectKey:(PSKeyValueReaderSelectKey)selectKey selectedKey:(PSConstData *)selectedKey selectedValue:(PSConstData *)selectedValue +{ + __block bool result = false; + + [self enumerateKeysAndValuesBetweenLowerBoundKey:lowerBoundKey upperBoundKey:upperBoundKey options:selectKey == PSKeyValueReaderSelectHigherKey ? PSKeyValueReaderEnumerationReverse : 0 withBlock:^(PSData *key, PSData *value, bool *stop) + { + if (selectedKey) + *selectedKey = *key; + if (selectedValue) + *selectedValue = *value; + + if (stop) + *stop = true; + + result = true; + }]; + + return result; +} + +- (void)enumerateKeysAndValuesBetweenLowerBoundKey:(PSConstData *)lowerBoundKey upperBoundKey:(PSConstData *)upperBoundKey options:(NSInteger)options withBlock:(void (^)(PSConstData *key, PSConstData *value, bool *stop))block +{ + if (!block || upperBoundKey == NULL || lowerBoundKey == NULL) + return; + + [self readWithCursor:^(PSLMDBKeyValueCursor *cursor) + { + MDB_val upperBoundKeyVal = {.mv_data = (uint8_t *)upperBoundKey->data, .mv_size = upperBoundKey->length}; + MDB_val lowerBoundKeyVal = {.mv_data = (uint8_t *)lowerBoundKey->data, .mv_size = lowerBoundKey->length}; + + if (options & PSKeyValueReaderEnumerationReverse) + { + uint8_t const *positionedKey = upperBoundKey->data; + NSUInteger positionedKeyLength = upperBoundKey->length; + + uint8_t const *positionedValue = NULL; + NSUInteger positionedValueLength = 0; + + if ([cursor positionAt:&positionedKey keyLength:&positionedKeyLength value:&positionedValue valueLength:&positionedValueLength directionIfNotFound:PSKeyValueCursorDirectionBack]) + { + MDB_val positionedKeyVal = {.mv_data = (uint8_t *)positionedKey, .mv_size = positionedKeyLength}; + + bool continueSearch = true; + + if ((options & PSKeyValueReaderEnumerationUpperBoundExclusive) && mdb_cmp(_txn, _dbi, &upperBoundKeyVal, &positionedKeyVal) == 0) + { + continueSearch = [cursor previous:&positionedKey keyLength:&positionedKeyLength value:&positionedValue valueLength:&positionedValueLength]; + } + + if (continueSearch) + { + int cmpResult = mdb_cmp(_txn, _dbi, &positionedKeyVal, &lowerBoundKeyVal); + if ((options & PSKeyValueReaderEnumerationLowerBoundExclusive) ? (cmpResult > 0) : (cmpResult >= 0)) + { + bool stop = false; + PSData positionedKeyData = {.data = (uint8_t *)positionedKey, .length = positionedKeyLength}; + PSData positionedValueData = {.data = (uint8_t *)positionedValue, .length = positionedValueLength}; + block(&positionedKeyData, &positionedValueData, &stop); + + while (!stop) + { + if (![cursor previous:&positionedKey keyLength:&positionedKeyLength value:&positionedValue valueLength:&positionedValueLength]) + break; + + MDB_val positionedKeyVal = {.mv_data = (uint8_t *)positionedKey, .mv_size = positionedKeyLength}; + + int cmpResult = mdb_cmp(_txn, _dbi, &positionedKeyVal, &lowerBoundKeyVal); + if ((options & PSKeyValueReaderEnumerationLowerBoundExclusive) ? (cmpResult > 0) : (cmpResult >= 0)) + { + PSData positionedKeyData = {.data = (uint8_t *)positionedKey, .length = positionedKeyLength}; + PSData positionedValueData = {.data = (uint8_t *)positionedValue, .length = positionedValueLength}; + block(&positionedKeyData, &positionedValueData, &stop); + } + else + break; + } + } + } + } + } + else + { + uint8_t const *positionedKey = lowerBoundKey->data; + NSUInteger positionedKeyLength = lowerBoundKey->length; + + uint8_t const *positionedValue = NULL; + NSUInteger positionedValueLength = 0; + + if ([cursor positionAt:&positionedKey keyLength:&positionedKeyLength value:&positionedValue valueLength:&positionedValueLength directionIfNotFound:PSKeyValueCursorDirectionForward]) + { + MDB_val positionedKeyVal = {.mv_data = (uint8_t *)positionedKey, .mv_size = positionedKeyLength}; + + bool continueSearch = true; + if ((options & PSKeyValueReaderEnumerationLowerBoundExclusive) && mdb_cmp(_txn, _dbi, &lowerBoundKeyVal, &positionedKeyVal) == 0) + { + continueSearch = [cursor next:&positionedKey keyLength:&positionedKeyLength value:&positionedValue valueLength:&positionedValueLength]; + } + + if (continueSearch) + { + int cmpResult = mdb_cmp(_txn, _dbi, &positionedKeyVal, &upperBoundKeyVal); + if ((options & PSKeyValueReaderEnumerationUpperBoundExclusive) ? (cmpResult < 0) : (cmpResult <= 0)) + { + bool stop = false; + PSData positionedKeyData = {.data = (uint8_t *)positionedKey, .length = positionedKeyLength}; + PSData positionedValueData = {.data = (uint8_t *)positionedValue, .length = positionedValueLength}; + block(&positionedKeyData, &positionedValueData, &stop); + + while (!stop) + { + if (![cursor next:&positionedKey keyLength:&positionedKeyLength value:&positionedValue valueLength:&positionedValueLength]) + break; + + MDB_val positionedKeyVal = {.mv_data = (uint8_t *)positionedKey, .mv_size = positionedKeyLength}; + + int cmpResult = mdb_cmp(_txn, _dbi, &positionedKeyVal, &upperBoundKeyVal); + if ((options & PSKeyValueReaderEnumerationUpperBoundExclusive) ? (cmpResult < 0) : (cmpResult <= 0)) + { + PSData positionedKeyData = {.data = (uint8_t *)positionedKey, .length = positionedKeyLength}; + PSData positionedValueData = {.data = (uint8_t *)positionedValue, .length = positionedValueLength}; + block(&positionedKeyData, &positionedValueData, &stop); + } + else + break; + } + } + } + } + } + }]; +} + +- (bool)deleteValueForRawKey:(PSData *)key +{ + if (key == NULL || key->data == NULL || key->length == 0) + return false; + + MDB_val mdbKey; + mdbKey.mv_data = (uint8_t *)key->data; + mdbKey.mv_size = (size_t)key->length; + + int rc = 0; + rc = mdb_del(_txn, _dbi, &mdbKey, NULL); + + if (rc != MDB_SUCCESS && rc != MDB_NOTFOUND) + TGLegacyLog(@"[PSLMDBKeyValueWriter mdb_del error %d]", rc); + + return rc == MDB_SUCCESS; +} + +- (void)deleteAllValues +{ + MDB_cursor *cursor = NULL; + int rc = 0; + rc = mdb_cursor_open(_txn, _dbi, &cursor); + if (rc == MDB_SUCCESS) + { + rc = mdb_cursor_get(cursor, NULL, NULL, MDB_FIRST); + while (rc == MDB_SUCCESS) + { + rc = mdb_cursor_del(cursor, 0); + rc = mdb_cursor_get(cursor, NULL, NULL, MDB_NEXT); + } + mdb_cursor_close(cursor); + } +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PSLMDBKeyValueStore.h b/submodules/LegacyComponents/LegacyComponents/PSLMDBKeyValueStore.h new file mode 100644 index 0000000000..122143c91e --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PSLMDBKeyValueStore.h @@ -0,0 +1,9 @@ +#import + +@interface PSLMDBKeyValueStore : NSObject + ++ (instancetype)storeWithPath:(NSString *)path size:(NSUInteger)size; + +- (void)close; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PSLMDBKeyValueStore.m b/submodules/LegacyComponents/LegacyComponents/PSLMDBKeyValueStore.m new file mode 100644 index 0000000000..36b0d6041b --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PSLMDBKeyValueStore.m @@ -0,0 +1,237 @@ +#import "PSLMDBKeyValueStore.h" + +#import "LegacyComponentsInternal.h" + +#import "lmdb.h" + +#import "PSLMDBTable.h" +#import "PSLMDBKeyValueReaderWriter.h" + +@interface PSLMDBKeyValueStore () +{ + NSString *_path; + MDB_env *_env; + PSLMDBTable *_table; +} + +@end + +@implementation PSLMDBKeyValueStore + ++ (instancetype)storeWithPath:(NSString *)path size:(NSUInteger)size +{ + if (path.length == 0) + return nil; + + PSLMDBKeyValueStore *result = [[PSLMDBKeyValueStore alloc] init]; + if (result != nil) + { + result->_path = path; + + if (![result _open:size]) + { + [result close]; + + return nil; + } + } + + return result; +} + +- (bool)_open:(NSUInteger)size +{ + int rc = 0; + + rc = mdb_env_create(&_env); + if (rc != MDB_SUCCESS) + return false; + + bool createDirectory = false; + + BOOL isDirectory = false; + if ([[NSFileManager defaultManager] fileExistsAtPath:_path isDirectory:&isDirectory]) + { + if (!isDirectory) + { + [[NSFileManager defaultManager] removeItemAtPath:_path error:nil]; + createDirectory = true; + } + } + else + createDirectory = true; + + if (createDirectory) + [[NSFileManager defaultManager] createDirectoryAtPath:_path withIntermediateDirectories:true attributes:nil error:nil]; + + mdb_env_set_mapsize(_env, (size_t)size); + mdb_env_set_maxdbs(_env, 64); + + rc = mdb_env_open(_env, [_path UTF8String], MDB_NOSYNC, 0664); + if (rc != MDB_SUCCESS) + { + if (rc == MDB_INVALID) + { + [[NSFileManager defaultManager] removeItemAtPath:_path error:nil]; + [[NSFileManager defaultManager] createDirectoryAtPath:_path withIntermediateDirectories:true attributes:nil error:nil]; + + rc = mdb_env_create(&_env); + mdb_env_set_mapsize(_env, (size_t)size); + mdb_env_set_maxdbs(_env, 64); + rc = mdb_env_open(_env, [_path UTF8String], MDB_NOSYNC, 0664); + + if (rc == MDB_INVALID) + return false; + } + else + return false; + } + + int removedReaders = 0; + rc = mdb_reader_check(_env, &removedReaders); + + if (removedReaders != 0) + TGLegacyLog(@"[PSLMDBKeyValueStore removed %d stale readers]", removedReaders); + + _table = [self _createTableWithName:@"main"]; + + return true; +} + +- (void)close +{ + mdb_close(_env, _table.dbi); + + mdb_env_close(_env); + + _env = NULL; +} + +- (void)sync +{ + int rc = 0; + rc = mdb_env_sync(_env, 1); + + if (rc != MDB_SUCCESS) + TGLegacyLog(@"[PSLMDBKeyValueStore sync: mdb_env_sync error %d]", rc); +} + +- (void)panic +{ + +} + +- (PSLMDBTable *)_createTableWithName:(NSString *)name +{ + PSLMDBTable *result = nil; + + if (result == nil) + { + int rc = 0; + + MDB_txn *txn = NULL; + rc = mdb_txn_begin(_env, NULL, 0, &txn); + if (rc != MDB_SUCCESS) + { + TGLegacyLog(@"[PSLMDBKeyValueStore transaction begin failed %d]", rc); + + if (rc == MDB_PANIC) + { + TGLegacyLog(@"[PSLMDBKeyValueStore critical error received]"); + + [self panic]; + } + } + + MDB_dbi dbi; + + rc = mdb_dbi_open(txn, [name UTF8String], MDB_CREATE, &dbi); + if (rc != MDB_SUCCESS) + { + mdb_txn_abort(txn); + + TGLegacyLog(@"[PSLMDBKeyValueStore mdb_dbi_open failed %d]", rc); + } + else + { + mdb_txn_commit(txn); + + PSLMDBTable *createdTable = [[PSLMDBTable alloc] initWithDbi:dbi]; + result = createdTable; + } + } + + return result; +} + +- (void)readInTransaction:(void (^)(id))transaction +{ + if (transaction == nil) + return; + + PSLMDBTable *table = _table; + if (table != nil) + { + int rc = 0; + MDB_txn *txn = NULL; + + rc = mdb_txn_begin(_env, NULL, MDB_RDONLY, &txn); + if (rc != MDB_SUCCESS) + { + TGLegacyLog(@"[PSLMDBKeyValueStore mdb_txn_begin failed %d", rc); + + if (rc == MDB_PANIC) + { + TGLegacyLog(@"[PSLMDBKeyValueStore critical error received]"); + + [self panic]; + } + } + else + { + transaction([[PSLMDBKeyValueReaderWriter alloc] initWithTable:table transaction:txn]); + + rc = mdb_txn_commit(txn); + + if (rc != MDB_SUCCESS) + TGLegacyLog(@"[PSLMDBKeyValueStore mdb_txn_commit error %d]", rc); + } + } +} + +- (void)readWriteInTransaction:(void (^)(id))transaction +{ + if (transaction == nil) + return; + + PSLMDBTable *table = _table; + if (table != nil) + { + int rc = 0; + MDB_txn *txn = NULL; + + rc = mdb_txn_begin(_env, NULL, 0, &txn); + if (rc != MDB_SUCCESS) + { + TGLegacyLog(@"[PSLMDBKeyValueStore mdb_txn_begin failed %d", rc); + + if (rc == MDB_PANIC) + { + TGLegacyLog(@"[PSLMDBKeyValueStore critical error received]"); + + [self panic]; + } + } + else + { + transaction([[PSLMDBKeyValueReaderWriter alloc] initWithTable:table transaction:txn]); + + rc = mdb_txn_commit(txn); + + if (rc != MDB_SUCCESS) + TGLegacyLog(@"[PSLMDBKeyValueStore mdb_txn_commit error %d]", rc); + } + } +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PSLMDBTable.h b/submodules/LegacyComponents/LegacyComponents/PSLMDBTable.h new file mode 100644 index 0000000000..49f9a917c0 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PSLMDBTable.h @@ -0,0 +1,11 @@ +#import + +#import + +@interface PSLMDBTable : NSObject + +@property (nonatomic, assign) MDB_dbi dbi; + +- (instancetype)initWithDbi:(MDB_dbi)dbi; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/PSLMDBTable.m b/submodules/LegacyComponents/LegacyComponents/PSLMDBTable.m new file mode 100644 index 0000000000..74733fa419 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/PSLMDBTable.m @@ -0,0 +1,15 @@ +#import "PSLMDBTable.h" + +@implementation PSLMDBTable + +- (instancetype)initWithDbi:(MDB_dbi)dbi +{ + self = [super init]; + if (self != nil) + { + _dbi = dbi; + } + return self; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/RMPhoneFormat.h b/submodules/LegacyComponents/LegacyComponents/RMPhoneFormat.h new file mode 100644 index 0000000000..bf050f43cb --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/RMPhoneFormat.h @@ -0,0 +1,52 @@ + + +// +// RMPhoneFormat.h v1.0 + +// Copyright (c) 2012, Rick Maddy +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +// +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +// OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#import + +@interface RMPhoneFormat : NSObject + ++ (RMPhoneFormat *)instance; + +- (id)init; +- (id)initWithDefaultCountry:(NSString *)countryCode; + +- (NSString *)format:(NSString *)str implicitPlus:(bool)implicitPlus; + +// Calling code for the user's default country based on their Region Format setting +- (NSString *)defaultCallingCode; +// countryCode must be 2-letter ISO 3166-1 code. Result does not include a leading + +- (NSString *)callingCodeForCountryCode:(NSString *)countryCode; +// callingCode should be 1 to 3 digit calling code. Result is a set of matching, lowercase, 2-letter ISO 3166-1 country codes +- (NSSet *)countriesForCallingCode:(NSString *)callingCode; + +#ifdef DEBUG +- (void)dump; +#endif + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/RMPhoneFormat.m b/submodules/LegacyComponents/LegacyComponents/RMPhoneFormat.m new file mode 100644 index 0000000000..fd00690ef3 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/RMPhoneFormat.m @@ -0,0 +1,754 @@ +#import "RMPhoneFormat.h" + +@interface PhoneRule : NSObject + +@property (nonatomic, assign) int minVal; +@property (nonatomic, assign) int maxVal; +@property (nonatomic, assign) int byte8; +@property (nonatomic, assign) int maxLen; +@property (nonatomic, assign) int otherFlag; +@property (nonatomic, assign) int prefixLen; +@property (nonatomic, assign) int flag12; +@property (nonatomic, assign) int flag13; +@property (nonatomic) NSString *format; +#ifdef DEBUG +@property (nonatomic) NSSet *countries; +@property (nonatomic) NSString *callingCode; +@property (nonatomic, assign) int matchLen; +#endif + +- (NSString *)format:(NSString *)str intlPrefix:(NSString *)intlPrefix trunkPrefix:(NSString *)trunkPrefix; + +@end + +@implementation PhoneRule + +@synthesize minVal = _minVal; +@synthesize maxVal = _maxVal; +@synthesize byte8 = _byte8; +@synthesize maxLen = _maxLen; +@synthesize otherFlag = _otherFlag; +@synthesize prefixLen = _prefixLen; +@synthesize flag12 = _flag12; +@synthesize flag13 = _flag13; +@synthesize format = _format; + +#ifdef DEBUG +@synthesize countries = _countries; +@synthesize callingCode = _callingCode; +@synthesize matchLen = _matchLen; +#endif + +- (NSString *)format:(NSString *)str intlPrefix:(NSString *)intlPrefix trunkPrefix:(NSString *)trunkPrefix { + BOOL hadC = NO; + BOOL hadN = NO; + BOOL hasOpen = NO; + int spot = 0; + NSMutableString *res = [[NSMutableString alloc] initWithCapacity:20]; + for (int i = 0; i < (int)[self.format length]; i++) { + unichar ch = [self.format characterAtIndex:i]; + switch (ch) { + case 'c': + // Add international prefix if there is one. + hadC = YES; + if (intlPrefix != nil) { + [res appendString:intlPrefix]; + } + break; + case 'n': + // Add trunk prefix if there is one. + hadN = YES; + if (trunkPrefix != nil) { + [res appendString:trunkPrefix]; + } + break; + case '#': + // Add next digit from number. If there aren't enough digits left then do nothing unless we need to + // space-fill a pair of parenthesis. + if (spot < (int)[str length]) { + [res appendString:[str substringWithRange:NSMakeRange(spot, 1)]]; + spot++; + } else if (hasOpen) { + [res appendString:@" "]; + } + break; + case '(': + // Flag we found an open paren so it can be space-filled. But only do so if we aren't beyond the + // end of the number. + if (spot < (int)[str length]) { + hasOpen = YES; + } + // fall through + default: // rest like ) and - + // Don't show space after n if no trunkPrefix or after c if no intlPrefix + if (!(ch == ' ' && i > 0 && (([self.format characterAtIndex:i - 1] == 'n' && trunkPrefix == nil) || ([self.format characterAtIndex:i - 1] == 'c' && intlPrefix == nil)))) { + // Only show punctuation if not beyond the end of the supplied number. + // The only exception is to show a close paren if we had found + if (spot < (int)[str length] || (hasOpen && ch == ')')) { + [res appendString:[self.format substringWithRange:NSMakeRange(i, 1)]]; + if (ch == ')') { + hasOpen = NO; // close it + } + } + } + break; + } + } + + // Not all format strings have a 'c' or 'n' in them. If we have an international prefix or a trunk prefix but the + // format string doesn't explictly say where to put it then simply add it to the beginning. + if (intlPrefix != nil && !hadC) { + [res insertString:[NSString stringWithFormat:@"%@ ", intlPrefix] atIndex:0]; + } else if (trunkPrefix != nil && !hadN) { + [res insertString:trunkPrefix atIndex:0]; + } + + return res; +} + +- (NSString *)description { +#ifdef DEBUG + return [NSString stringWithFormat:@"Rule: { countries: %@, calling code: %@, matchlen: %d, minVal: %d, maxVal: %d, byte8: %d, maxLen: %d, nFlag: %d, prefixLen: %d, flag12: %d, flag13: %d, format: %@ }", self.countries, self.callingCode, self.matchLen, self.minVal, self.maxVal, self.byte8, self.maxLen, self.otherFlag, self.prefixLen, self.flag12, self.flag13, self.format]; +#else + return [NSString stringWithFormat:@"Rule: { minVal: %d, maxVal: %d, byte8: %d, maxLen: %d, nFlag: %d, prefixLen: %d, flag12: %d, flag13: %d, format: %@ }", self.minVal, self.maxVal, self.byte8, self.maxLen, self.otherFlag, self.prefixLen, self.flag12, self.flag13, self.format]; +#endif +} + + +@end + + +@interface RuleSet : NSObject + +@property (nonatomic, assign) int matchLen; +@property (nonatomic) NSMutableArray *rules; + +- (NSString *)format:(NSString *)str intlPrefix:(NSString *)intlPrefix trunkPrefix:(NSString *)trunkPrefix prefixRequired:(BOOL)prefixRequired; + +@end + +@implementation RuleSet + +@synthesize matchLen = _matchLen; +@synthesize rules = _rules; + +- (NSString *)format:(NSString *)str intlPrefix:(NSString *)intlPrefix trunkPrefix:(NSString *)trunkPrefix prefixRequired:(BOOL)prefixRequired { + if ((int)[str length] >= self.matchLen) + { + NSString *begin = [str substringToIndex:self.matchLen]; + int val = [begin intValue]; + for (PhoneRule *rule in self.rules) + { + if (val >= rule.minVal && val <= rule.maxVal && (int)[str length] <= rule.maxLen) + { + if (prefixRequired) + { + if ( + ((rule.flag12 & 0x03) == 0 && + trunkPrefix == nil && + intlPrefix == nil) || + (trunkPrefix != nil && (rule.flag12 & 0x01)) || + (intlPrefix != nil && (rule.flag12 & 0x02)) + ) + { + return [rule format:str intlPrefix:intlPrefix trunkPrefix:trunkPrefix]; + } + } + else + { + if ((trunkPrefix == nil && intlPrefix == nil) || (trunkPrefix != nil && (rule.flag12 & 0x01)) || (intlPrefix != nil && (rule.flag12 & 0x02))) + { + return [rule format:str intlPrefix:intlPrefix trunkPrefix:trunkPrefix]; + } + } + } + } + + if (!prefixRequired) + { + if (intlPrefix != nil) + { + for (PhoneRule *rule in self.rules) + { + if (val >= rule.minVal && val <= rule.maxVal && (int)[str length] <= rule.maxLen) + { + if (trunkPrefix == nil || (rule.flag12 & 0x01)) + { + return [rule format:str intlPrefix:intlPrefix trunkPrefix:trunkPrefix]; + } + } + } + } else if (trunkPrefix != nil) + { + for (PhoneRule *rule in self.rules) + { + if (val >= rule.minVal && val <= rule.maxVal && (int)[str length] <= rule.maxLen) + { + if (intlPrefix == nil || (rule.flag12 & 0x02)) + { + return [rule format:str intlPrefix:intlPrefix trunkPrefix:trunkPrefix]; + } + } + } + } + } + + return nil; // no match found + } else { + return nil; // not long enough to compare + } +} + +- (NSString *)description { + NSMutableString *res = [NSMutableString stringWithCapacity:100]; + [res appendFormat:@"RuleSet: { matchLen: %d, rules: %@ }", self.matchLen, self.rules]; + + return res; +} + + +@end + + +@interface CallingCodeInfo : NSObject + +@property (nonatomic) NSSet *countries; +@property (nonatomic) NSString *callingCode; +@property (nonatomic) NSMutableArray *trunkPrefixes; +@property (nonatomic) NSMutableArray *intlPrefixes; +@property (nonatomic) NSMutableArray *ruleSets; +@property (nonatomic) NSMutableArray *formatStrings; + +- (NSString *)matchingAccessCode:(NSString *)str; +- (NSString *)format:(NSString *)str; + +@end + +@implementation CallingCodeInfo + +@synthesize countries = _countries; +@synthesize callingCode = _callingCode; +@synthesize trunkPrefixes = _trunkPrefixes; +@synthesize intlPrefixes = _intlPrefixes; +@synthesize ruleSets = _ruleSets; +@synthesize formatStrings = _formatStrings; + +- (NSString *)matchingAccessCode:(NSString *)str { + for (NSString *code in self.intlPrefixes) { + if ([str hasPrefix:code]) { + return code; + } + } + + return nil; +} + +- (NSString *)matchingTrunkCode:(NSString *)str { + for (NSString *code in self.trunkPrefixes) { + if ([str hasPrefix:code]) { + return code; + } + } + + return nil; +} + +- (NSString *)format:(NSString *)orig +{ + NSString *str = orig; + NSString *trunkPrefix = nil; + NSString *intlPrefix = nil; + if ([str hasPrefix:self.callingCode]) + { + intlPrefix = self.callingCode; + str = [str substringFromIndex:[intlPrefix length]]; + } + else + { + NSString *trunk = [self matchingTrunkCode:str]; + if (trunk) + { + trunkPrefix = trunk; + str = [str substringFromIndex:[trunkPrefix length]]; + } + } + + for (RuleSet *set in self.ruleSets) + { + NSString *phone = [set format:str intlPrefix:intlPrefix trunkPrefix:trunkPrefix prefixRequired:YES]; + if (phone) + { + return phone; + } + } + + for (RuleSet *set in self.ruleSets) + { + NSString *phone = [set format:str intlPrefix:intlPrefix trunkPrefix:trunkPrefix prefixRequired:NO]; + if (phone) + { + return phone; + } + } + + if (intlPrefix != nil && [str length]) + { + return [NSString stringWithFormat:@"%@ %@", intlPrefix, str]; + } + + return orig; +} + +- (NSString *)description { + NSMutableString *res = [NSMutableString stringWithCapacity:100]; + [res appendFormat:@"CountryInfo { countries: %@, code: %@, trunkPrefixes: %@, intlPrefixes: %@", self.countries, self.callingCode, self.trunkPrefixes, self.intlPrefixes]; + [res appendFormat:@", rule sets: %@ }", self.ruleSets]; + + return res; +} + + +@end + +static NSCharacterSet *phoneChars = nil; +#ifdef DEBUG +static NSMutableDictionary *extra1CallingCodes = nil; +static NSMutableDictionary *extra2CallingCodes = nil; +static NSMutableDictionary *extra3CallingCodes = nil; +static NSMutableDictionary *flagRules = nil; +#endif + +@implementation RMPhoneFormat { + NSData *_data; + NSString *_defaultCountry; + NSString *_defaultCallingCode; + NSMutableDictionary *_callingCodeOffsets; + NSMutableDictionary *_callingCodeCountries; + NSMutableDictionary *_callingCodeData; + NSMutableDictionary *_countryCallingCode; +} + ++ (void)initialize { + phoneChars = [NSCharacterSet characterSetWithCharactersInString:@"0123456789+*#"]; + +#ifdef DEBUG + extra1CallingCodes = [[NSMutableDictionary alloc] init]; + extra2CallingCodes = [[NSMutableDictionary alloc] init]; + extra3CallingCodes = [[NSMutableDictionary alloc] init]; + flagRules = [[NSMutableDictionary alloc] init]; +#endif +} + ++ (NSString *)strip:(NSString *)str { + NSMutableString *res = [NSMutableString stringWithString:str]; + for (int i = (int)[res length] - 1; i >= 0; i--) { + if (![phoneChars characterIsMember:[res characterAtIndex:i]]) { + [res deleteCharactersInRange:NSMakeRange(i, 1)]; + } + } + + return res; +} + ++ (RMPhoneFormat *)instance { + static RMPhoneFormat *instance = nil; + static dispatch_once_t predicate = 0; + + dispatch_once(&predicate, ^{ instance = [self new]; }); + + return instance; +} + +- (id)init { + self = [self initWithDefaultCountry:nil]; + + return self; +} + +- (id)initWithDefaultCountry:(NSString *)countryCode { + if ((self = [super init])) { + _data = [NSData dataWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"PhoneFormats" ofType:@"dat"]]; + NSAssert(_data, @"The file PhoneFormats.dat is not in the resource bundle. See the README."); + + if (countryCode.length) { + _defaultCountry = countryCode; + } else { + NSLocale *loc = [NSLocale currentLocale]; + _defaultCountry = [[loc objectForKey:NSLocaleCountryCode] lowercaseString]; + } + _callingCodeOffsets = [[NSMutableDictionary alloc] initWithCapacity:255]; + _callingCodeCountries = [[NSMutableDictionary alloc] initWithCapacity:255]; + _callingCodeData = [[NSMutableDictionary alloc] initWithCapacity:10]; + _countryCallingCode = [[NSMutableDictionary alloc] initWithCapacity:255]; + + [self parseDataHeader]; + } + + return self; +} + +- (NSString *)defaultCallingCode { + return [self callingCodeForCountryCode:_defaultCountry]; +} + +- (NSString *)callingCodeForCountryCode:(NSString *)countryCode { + return [_countryCallingCode objectForKey:[countryCode lowercaseString]]; +} + +- (NSSet *)countriesForCallingCode:(NSString *)callingCode { + if ([callingCode hasPrefix:@"+"]) { + callingCode = [callingCode substringFromIndex:1]; + } + + return [_callingCodeCountries objectForKey:callingCode]; +} + +- (CallingCodeInfo *)findCallingCodeInfo:(NSString *)str { + CallingCodeInfo *res = nil; + for (int i = 0; i < 3; i++) { + if (i < (int)[str length]) { + res = [self callingCodeInfo:[str substringToIndex:i + 1]]; + if (res) { + break; + } + } else { + break; + } + } + + return res; +} + +- (NSString *)format:(NSString *)orig implicitPlus:(bool)implicitPlus +{ + NSString *str = [RMPhoneFormat strip:orig]; + + bool hasPlusPrefix = [str hasPrefix:@"+"]; + + if ([str hasPrefix:@"+"] || implicitPlus) + { + NSString *rest = hasPlusPrefix ? [str substringFromIndex:1] : str; + + CallingCodeInfo *info = [self findCallingCodeInfo:rest]; + if (info) + { + NSString *phone = [info format:rest]; + return [@"+" stringByAppendingString:phone]; + } else + { + return orig; + } + } + else + { + CallingCodeInfo *info = [self callingCodeInfo:_defaultCallingCode]; + if (info == nil) + { + return orig; + } + + NSString *accessCode = [info matchingAccessCode:str]; + if (accessCode) + { + NSString *rest = [str substringFromIndex:[accessCode length]]; + NSString *phone = rest; + + CallingCodeInfo *info2 = [self findCallingCodeInfo:rest]; + if (info2) + { + phone = [info2 format:rest]; + } + + if ([phone length] == 0) + { + return accessCode; + } + else + { + return [NSString stringWithFormat:@"%@ %@", accessCode, phone]; + } + } + else + { + NSString *phone = [info format:str]; + + return phone; + } + } + + return orig; +} + +- (uint32_t)value32:(NSUInteger)offset { + if (offset + 4 <= [_data length]) { + return OSReadLittleInt32([_data bytes], offset); + } else { + return 0; + } +} + +- (int)value16:(NSUInteger)offset { + if (offset + 2 <= [_data length]) { + return OSReadLittleInt16([_data bytes], offset); + } else { + return 0; + } +} + +- (int)value16BE:(NSUInteger)offset { + if (offset + 2 <= [_data length]) { + return OSReadBigInt16([_data bytes], offset); + } else { + return 0; + } +} + +- (CallingCodeInfo *)callingCodeInfo:(NSString *)callingCode +{ + CallingCodeInfo *res = [_callingCodeData objectForKey:callingCode]; + if (res == nil) + { + NSNumber *num = [_callingCodeOffsets objectForKey:callingCode]; + if (num) + { + const uint8_t *bytes = [_data bytes]; + uint32_t start = (int)[num longValue]; + uint32_t offset = start; + res = [[CallingCodeInfo alloc] init]; + res.callingCode = callingCode; + res.countries = [_callingCodeCountries objectForKey:callingCode]; + [_callingCodeData setObject:res forKey:callingCode]; + + uint16_t block1Len = (uint16_t)[self value16:offset]; + offset += 2; +#ifdef DEBUG + uint16_t extra1 = (uint16_t)[self value16:offset]; +#endif + offset += 2; + uint16_t block2Len = (uint16_t)[self value16:offset]; + offset += 2; +#ifdef DEBUG + uint16_t extra2 = (uint16_t)[self value16:offset]; +#endif + offset += 2; + uint16_t setCnt = (uint16_t)[self value16:offset]; + offset += 2; +#ifdef DEBUG + uint16_t extra3 = (uint16_t)[self value16:offset]; +#endif + offset += 2; + +#ifdef DEBUG + if (extra1) { + NSMutableArray *vals = [extra1CallingCodes objectForKey:[NSNumber numberWithInt:extra1]]; + if (!vals) { + vals = [[NSMutableArray alloc] init]; + [extra1CallingCodes setObject:vals forKey:[NSNumber numberWithInt:extra1]]; + } + [vals addObject:res]; + } + if (extra2) { + NSMutableArray *vals = [extra2CallingCodes objectForKey:[NSNumber numberWithInt:extra2]]; + if (!vals) { + vals = [[NSMutableArray alloc] init]; + [extra2CallingCodes setObject:vals forKey:[NSNumber numberWithInt:extra2]]; + } + [vals addObject:res]; + } + if (extra3) { + NSMutableArray *vals = [extra3CallingCodes objectForKey:[NSNumber numberWithInt:extra3]]; + if (!vals) { + vals = [[NSMutableArray alloc] init]; + [extra3CallingCodes setObject:vals forKey:[NSNumber numberWithInt:extra3]]; + } + [vals addObject:res]; + } +#endif + + NSMutableArray *strs = [NSMutableArray arrayWithCapacity:5]; + NSString *str; + while ([(str = [NSString stringWithCString:(char *)bytes + offset encoding:NSUTF8StringEncoding]) length]) { + [strs addObject:str]; + offset += [str length] + 1; + } + res.trunkPrefixes = strs; + offset++; // skip NULL + + strs = [NSMutableArray arrayWithCapacity:5]; + while ([(str = [NSString stringWithCString:(char *)bytes + offset encoding:NSUTF8StringEncoding]) length]) { + [strs addObject:str]; + offset += [str length] + 1; + } + res.intlPrefixes = strs; + + NSMutableArray *ruleSets = [NSMutableArray arrayWithCapacity:setCnt]; + offset = start + block1Len; // Start of rule sets + for (int s = 0; s < setCnt; s++) { + RuleSet *ruleSet = [[RuleSet alloc] init]; + int matchCnt = [self value16:offset]; + ruleSet.matchLen = matchCnt; + offset += 2; + int ruleCnt = [self value16:offset]; + offset += 2; + NSMutableArray *rules = [NSMutableArray arrayWithCapacity:ruleCnt]; + for (int r = 0; r < ruleCnt; r++) { + PhoneRule *rule = [[PhoneRule alloc] init]; + rule.minVal = [self value32:offset]; + offset += 4; + rule.maxVal = [self value32:offset]; + offset += 4; + rule.byte8 = (int)bytes[offset++]; + rule.maxLen = (int)bytes[offset++]; + rule.otherFlag = (int)bytes[offset++]; + rule.prefixLen = (int)bytes[offset++]; + rule.flag12 = (int)bytes[offset++]; + rule.flag13 = (int)bytes[offset++]; + uint16_t strOffset = (uint16_t)[self value16:offset]; + offset += 2; + rule.format = [NSString stringWithCString:(char *)bytes + start + block1Len + block2Len + strOffset encoding:NSUTF8StringEncoding]; + // Several formats contain [[9]] or [[8]]. Using the Contacts app as a test, I can find no use + // for these. Do they mean "optional"? They don't seem to have any use. This code strips out + // anything in [[..]] + NSRange openPos = [rule.format rangeOfString:@"[["]; + if (openPos.location != NSNotFound) { + NSRange closePos = [rule.format rangeOfString:@"]]"]; + rule.format = [NSString stringWithFormat:@"%@%@", [rule.format substringToIndex:openPos.location], [rule.format substringFromIndex:closePos.location + closePos.length]]; + } + + [rules addObject:rule]; +#ifdef DEBUG + rule.countries = res.countries; + rule.callingCode = res.callingCode; + rule.matchLen = matchCnt; + if (rule.byte8) { + NSMutableDictionary *data = [flagRules objectForKey:@"byte8"]; + if (!data) { + data = [[NSMutableDictionary alloc] init]; + [flagRules setObject:data forKey:@"byte8"]; + } + NSMutableArray *list = [data objectForKey:[NSNumber numberWithInt:rule.byte8]]; + if (!list) { + list = [[NSMutableArray alloc] init]; + [data setObject:list forKey:[NSNumber numberWithInt:rule.byte8]]; + } + + [list addObject:rule]; + } + if (rule.prefixLen) { + NSMutableDictionary *data = [flagRules objectForKey:@"prefixLen"]; + if (!data) { + data = [[NSMutableDictionary alloc] init]; + [flagRules setObject:data forKey:@"prefixLen"]; + } + NSMutableArray *list = [data objectForKey:[NSNumber numberWithInt:rule.prefixLen]]; + if (!list) { + list = [[NSMutableArray alloc] init]; + [data setObject:list forKey:[NSNumber numberWithInt:rule.prefixLen]]; + } + + [list addObject:rule]; + } + if (rule.otherFlag) { + NSMutableDictionary *data = [flagRules objectForKey:@"otherFlag"]; + if (!data) { + data = [[NSMutableDictionary alloc] init]; + [flagRules setObject:data forKey:@"otherFlag"]; + } + NSMutableArray *list = [data objectForKey:[NSNumber numberWithInt:rule.otherFlag]]; + if (!list) { + list = [[NSMutableArray alloc] init]; + [data setObject:list forKey:[NSNumber numberWithInt:rule.otherFlag]]; + } + + [list addObject:rule]; + } + if (rule.flag12) { + NSMutableDictionary *data = [flagRules objectForKey:@"flag12"]; + if (!data) { + data = [[NSMutableDictionary alloc] init]; + [flagRules setObject:data forKey:@"flag12"]; + } + NSMutableArray *list = [data objectForKey:[NSNumber numberWithInt:rule.flag12]]; + if (!list) { + list = [[NSMutableArray alloc] init]; + [data setObject:list forKey:[NSNumber numberWithInt:rule.flag12]]; + } + + [list addObject:rule]; + } + if (rule.flag13) { + NSMutableDictionary *data = [flagRules objectForKey:@"flag13"]; + if (!data) { + data = [[NSMutableDictionary alloc] init]; + [flagRules setObject:data forKey:@"flag13"]; + } + NSMutableArray *list = [data objectForKey:[NSNumber numberWithInt:rule.flag13]]; + if (!list) { + list = [[NSMutableArray alloc] init]; + [data setObject:list forKey:[NSNumber numberWithInt:rule.flag13]]; + } + + [list addObject:rule]; + } +#endif + } + ruleSet.rules = rules; + [ruleSets addObject:ruleSet]; + } + res.ruleSets = ruleSets; + } + } + + return res; +} + +- (void)parseDataHeader { + int count = [self value32:0]; + uint32_t base = count * 12 + 4; + const void *bytes = [_data bytes]; + NSUInteger spot = 4; + for (int i = 0; i < count; i++) { + NSString *callingCode = [NSString stringWithCString:bytes + spot encoding:NSUTF8StringEncoding]; + spot += 4; + NSString *country = [NSString stringWithCString:bytes + spot encoding:NSUTF8StringEncoding]; + spot += 4; + uint32_t offset = [self value32:spot] + base; + spot += 4; + + if ([country isEqualToString:_defaultCountry]) { + _defaultCallingCode = callingCode; + } + + [_countryCallingCode setObject:callingCode forKey:country]; + + [_callingCodeOffsets setObject:[NSNumber numberWithLong:offset] forKey:callingCode]; + NSMutableSet *countries = [_callingCodeCountries objectForKey:callingCode]; + if (!countries) { + countries = [[NSMutableSet alloc] init]; + [_callingCodeCountries setObject:countries forKey:callingCode]; + } + [countries addObject:country]; + } + + if (_defaultCallingCode) { + [self callingCodeInfo:_defaultCallingCode]; + } +} + +#ifdef DEBUG +- (void)dump { + NSArray *callingCodes = [[_callingCodeOffsets allKeys] sortedArrayUsingSelector:@selector(compare:)]; + for (NSString *callingCode in callingCodes) { + CallingCodeInfo *info = [self callingCodeInfo:callingCode]; + NSLog(@"%@", info); + } + + NSLog(@"flagRules: %@", flagRules); + NSLog(@"extra1 calling codes: %@", extra1CallingCodes); + NSLog(@"extra2 calling codes: %@", extra2CallingCodes); + NSLog(@"extra3 calling codes: %@", extra3CallingCodes); +} +#endif + + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/AttachmentMenuInteractiveCameraIcon.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/AttachmentMenuInteractiveCameraIcon.png new file mode 100644 index 0000000000..4edffbcd9e Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/AttachmentMenuInteractiveCameraIcon.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/AttachmentMenuInteractiveCameraIcon@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/AttachmentMenuInteractiveCameraIcon@2x.png new file mode 100644 index 0000000000..daadae95df Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/AttachmentMenuInteractiveCameraIcon@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/AttachmentMenuInteractiveCameraIcon@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/AttachmentMenuInteractiveCameraIcon@3x.png new file mode 100644 index 0000000000..aca9c574b7 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/AttachmentMenuInteractiveCameraIcon@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/AttachmentTipIcons@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/AttachmentTipIcons@2x.png new file mode 100644 index 0000000000..eb05b2f187 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/AttachmentTipIcons@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/CalloutAccessory@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/CalloutAccessory@2x.png new file mode 100644 index 0000000000..636dbe1100 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/CalloutAccessory@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/CalloutArrow@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/CalloutArrow@2x.png new file mode 100644 index 0000000000..710487e0db Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/CalloutArrow@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/CalloutArrow_Highlighted@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/CalloutArrow_Highlighted@2x.png new file mode 100644 index 0000000000..ea5c91b128 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/CalloutArrow_Highlighted@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/CalloutBackground@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/CalloutBackground@2x.png new file mode 100644 index 0000000000..d30a7daf77 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/CalloutBackground@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/CalloutBackground_Highlighted@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/CalloutBackground_Highlighted@2x.png new file mode 100644 index 0000000000..72ed8be57d Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/CalloutBackground_Highlighted@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/CalloutDrivingBackground@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/CalloutDrivingBackground@2x.png new file mode 100644 index 0000000000..1812b32a09 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/CalloutDrivingBackground@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/CalloutDrivingBackground_Highlighted@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/CalloutDrivingBackground_Highlighted@2x.png new file mode 100644 index 0000000000..2697187254 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/CalloutDrivingBackground_Highlighted@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/CalloutDrivingIcon@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/CalloutDrivingIcon@2x.png new file mode 100644 index 0000000000..6eb9fa6052 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/CalloutDrivingIcon@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/CameraAutoFocusCrosshairs.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/CameraAutoFocusCrosshairs.png new file mode 100644 index 0000000000..448abbcfc9 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/CameraAutoFocusCrosshairs.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/CameraAutoFocusCrosshairs@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/CameraAutoFocusCrosshairs@2x.png new file mode 100644 index 0000000000..cea8bb3d3f Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/CameraAutoFocusCrosshairs@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/CameraAutoFocusCrosshairs@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/CameraAutoFocusCrosshairs@3x.png new file mode 100644 index 0000000000..4c03577c12 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/CameraAutoFocusCrosshairs@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/CameraDeleteIcon@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/CameraDeleteIcon@2x.png new file mode 100644 index 0000000000..543e6a5d5b Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/CameraDeleteIcon@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/CameraDeleteIcon@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/CameraDeleteIcon@3x.png new file mode 100644 index 0000000000..0feb96bb98 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/CameraDeleteIcon@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/CameraExposureIcon@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/CameraExposureIcon@2x.png new file mode 100644 index 0000000000..763171ed09 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/CameraExposureIcon@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/CameraFlashActive@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/CameraFlashActive@2x.png new file mode 100644 index 0000000000..122e1ea097 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/CameraFlashActive@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/CameraFlashButton@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/CameraFlashButton@2x.png new file mode 100644 index 0000000000..abc8a3dc0c Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/CameraFlashButton@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/CameraFlashButton@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/CameraFlashButton@3x.png new file mode 100644 index 0000000000..923087042b Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/CameraFlashButton@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/CameraFlipButton@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/CameraFlipButton@2x.png new file mode 100644 index 0000000000..8564f9b48c Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/CameraFlipButton@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/CameraFlipButton@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/CameraFlipButton@3x.png new file mode 100644 index 0000000000..c3ecdf5d06 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/CameraFlipButton@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/CameraFocusCrosshairs.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/CameraFocusCrosshairs.png new file mode 100644 index 0000000000..54a116fa5c Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/CameraFocusCrosshairs.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/CameraFocusCrosshairs@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/CameraFocusCrosshairs@2x.png new file mode 100644 index 0000000000..72b27f345b Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/CameraFocusCrosshairs@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/CameraFocusCrosshairs@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/CameraFocusCrosshairs@3x.png new file mode 100644 index 0000000000..cc2d5a0271 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/CameraFocusCrosshairs@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/CameraLargeFlipButton@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/CameraLargeFlipButton@2x.png new file mode 100644 index 0000000000..fafcd5613a Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/CameraLargeFlipButton@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/CameraLargeFlipButton@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/CameraLargeFlipButton@3x.png new file mode 100644 index 0000000000..5c1f542577 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/CameraLargeFlipButton@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/CameraSegmentsBack@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/CameraSegmentsBack@2x.png new file mode 100644 index 0000000000..1a6878b2a4 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/CameraSegmentsBack@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/CoubWatermark@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/CoubWatermark@2x.png new file mode 100644 index 0000000000..1a1078b4d8 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/CoubWatermark@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/DefaultPlayer.html b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/DefaultPlayer.html new file mode 100755 index 0000000000..0a5b5befaf --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/DefaultPlayer.html @@ -0,0 +1,42 @@ + + + + + + +
+ +
+ + + diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/DefaultPlayerInject.js b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/DefaultPlayerInject.js new file mode 100644 index 0000000000..4e3bbaa0b0 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/DefaultPlayerInject.js @@ -0,0 +1,18 @@ +function fixInline() { + var video = document.getElementsByTagName('video')[0]; + video.setAttribute('webkit-playsinline', ''); + video.setAttribute('playsinline', ''); + video.webkitExitFullscreen = undefined; + + video.play(); +} + +function receiveMessage(evt) { + try { + var obj = JSON.parse(evt.data); + if (obj.cmd == 'fixInline') + fixInline(); + } catch (ex) { } +} + +window.addEventListener('message', receiveMessage, false); diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/EmbedVideoFullScreenIcon@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/EmbedVideoFullScreenIcon@2x.png new file mode 100644 index 0000000000..0ad54e3680 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/EmbedVideoFullScreenIcon@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/EmbedVideoPIPIcon@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/EmbedVideoPIPIcon@2x.png new file mode 100644 index 0000000000..9c6de4b160 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/EmbedVideoPIPIcon@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/EmbedVideoPauseIcon@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/EmbedVideoPauseIcon@2x.png new file mode 100644 index 0000000000..54402daf4b Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/EmbedVideoPauseIcon@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/EmbedVideoPlayIcon@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/EmbedVideoPlayIcon@2x.png new file mode 100644 index 0000000000..f2a2e4422e Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/EmbedVideoPlayIcon@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/EmbedVideoTrackHollow@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/EmbedVideoTrackHollow@2x.png new file mode 100644 index 0000000000..7b0f4e5a00 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/EmbedVideoTrackHollow@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/FoursquareAttribution@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/FoursquareAttribution@2x.png new file mode 100644 index 0000000000..6b2650f7a3 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/FoursquareAttribution@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/ImagePickerPhotoCounter_Close@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/ImagePickerPhotoCounter_Close@2x.png new file mode 100644 index 0000000000..eec9efa573 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/ImagePickerPhotoCounter_Close@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/ImagePickerPhotoCounter_Close@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/ImagePickerPhotoCounter_Close@3x.png new file mode 100644 index 0000000000..fb0a4bd6e3 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/ImagePickerPhotoCounter_Close@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/InputMicRecordingOverlay@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/InputMicRecordingOverlay@2x.png new file mode 100644 index 0000000000..55ffca85a9 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/InputMicRecordingOverlay@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/InputMicRecordingOverlay@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/InputMicRecordingOverlay@3x.png new file mode 100644 index 0000000000..2940640582 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/InputMicRecordingOverlay@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/InstagramPlayer.html b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/InstagramPlayer.html new file mode 100755 index 0000000000..b21889acac --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/InstagramPlayer.html @@ -0,0 +1,52 @@ + + + + + + +
+ +
+ + + diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationInfo.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationInfo.png new file mode 100644 index 0000000000..5b1496cd6b Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationInfo.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationInfo@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationInfo@2x.png new file mode 100644 index 0000000000..51f15f8690 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationInfo@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationInfo@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationInfo@3x.png new file mode 100644 index 0000000000..16cdbcec81 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationInfo@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationInfo_Active.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationInfo_Active.png new file mode 100644 index 0000000000..d8470628c4 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationInfo_Active.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationInfo_Active@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationInfo_Active@2x.png new file mode 100644 index 0000000000..b2816988ca Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationInfo_Active@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationInfo_Active@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationInfo_Active@3x.png new file mode 100644 index 0000000000..6c8c87e511 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationInfo_Active@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationMessageLiveIcon@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationMessageLiveIcon@2x.png new file mode 100644 index 0000000000..92170e69ac Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationMessageLiveIcon@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationMessageLiveIcon@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationMessageLiveIcon@3x.png new file mode 100644 index 0000000000..40f071a3b0 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationMessageLiveIcon@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationMessagePinBackground@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationMessagePinBackground@2x.png new file mode 100644 index 0000000000..35384f6e1f Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationMessagePinBackground@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationMessagePinBackground@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationMessagePinBackground@3x.png new file mode 100644 index 0000000000..5eeda02fa0 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationMessagePinBackground@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationMessagePinIcon@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationMessagePinIcon@2x.png new file mode 100644 index 0000000000..cff797ee18 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationMessagePinIcon@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationMessagePinIcon@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationMessagePinIcon@3x.png new file mode 100644 index 0000000000..2c7603bb7d Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationMessagePinIcon@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationMessagePinShadow@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationMessagePinShadow@2x.png new file mode 100644 index 0000000000..709eb48ffe Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationMessagePinShadow@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationMessagePinShadow@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationMessagePinShadow@3x.png new file mode 100644 index 0000000000..38bf099fe4 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationMessagePinShadow@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationPanelEdge@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationPanelEdge@2x.png new file mode 100644 index 0000000000..d1dc083322 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationPanelEdge@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationPanelEdge@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationPanelEdge@3x.png new file mode 100644 index 0000000000..697e0d533e Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationPanelEdge@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationPanelEdge_Highlighted@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationPanelEdge_Highlighted@2x.png new file mode 100644 index 0000000000..d85ebddd58 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationPanelEdge_Highlighted@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationPanelEdge_Highlighted@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationPanelEdge_Highlighted@3x.png new file mode 100644 index 0000000000..7b6345b8ad Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationPanelEdge_Highlighted@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationPin@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationPin@2x.png new file mode 100644 index 0000000000..709d3b08f9 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationPin@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationPinBackground@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationPinBackground@2x.png new file mode 100644 index 0000000000..6e2330dde7 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationPinBackground@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationPinBackground@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationPinBackground@3x.png new file mode 100644 index 0000000000..c99da533a0 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationPinBackground@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationPinIcon@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationPinIcon@2x.png new file mode 100644 index 0000000000..ab6fd29ccb Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationPinIcon@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationPinIcon@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationPinIcon@3x.png new file mode 100644 index 0000000000..a57e0d4252 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationPinIcon@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationPinPoint@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationPinPoint@2x.png new file mode 100644 index 0000000000..7584de8b0f Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationPinPoint@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationPinShadow@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationPinShadow@2x.png new file mode 100644 index 0000000000..836010f30f Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationPinShadow@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationPinShadow@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationPinShadow@3x.png new file mode 100644 index 0000000000..caffa8f166 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationPinShadow@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationSmallCircle@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationSmallCircle@2x.png new file mode 100644 index 0000000000..ae2064ea7c Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationSmallCircle@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationSmallCircle@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationSmallCircle@3x.png new file mode 100644 index 0000000000..5baaa60ba9 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationSmallCircle@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationTopPanel@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationTopPanel@2x.png new file mode 100644 index 0000000000..80c3ac5c2c Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationTopPanel@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationTopPanel@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationTopPanel@3x.png new file mode 100644 index 0000000000..6b3d6b0252 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationTopPanel@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationWave@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationWave@3x.png new file mode 100644 index 0000000000..4819b3c862 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/LocationWave@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/MediaGroupAll@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/MediaGroupAll@2x.png new file mode 100644 index 0000000000..9abbef6249 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/MediaGroupAll@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/MediaGroupBurst@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/MediaGroupBurst@2x.png new file mode 100644 index 0000000000..0556204ada Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/MediaGroupBurst@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/MediaGroupFavorites@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/MediaGroupFavorites@2x.png new file mode 100644 index 0000000000..1d79276cab Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/MediaGroupFavorites@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/MediaGroupPanoramas@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/MediaGroupPanoramas@2x.png new file mode 100644 index 0000000000..77d4038d02 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/MediaGroupPanoramas@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/MediaGroupScreenshots@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/MediaGroupScreenshots@2x.png new file mode 100644 index 0000000000..86f314fb73 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/MediaGroupScreenshots@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/MediaGroupSelfPortraits@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/MediaGroupSelfPortraits@2x.png new file mode 100644 index 0000000000..ae188a3546 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/MediaGroupSelfPortraits@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/MediaGroupSlomo@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/MediaGroupSlomo@2x.png new file mode 100644 index 0000000000..dd9d235263 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/MediaGroupSlomo@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/MediaGroupTimelapse@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/MediaGroupTimelapse@2x.png new file mode 100644 index 0000000000..e45011a63e Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/MediaGroupTimelapse@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/MediaGroupVideo@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/MediaGroupVideo@2x.png new file mode 100644 index 0000000000..b765b548bc Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/MediaGroupVideo@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/MediaPickerGroupPhotosIcon@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/MediaPickerGroupPhotosIcon@2x.png new file mode 100644 index 0000000000..9ec9f7ba5e Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/MediaPickerGroupPhotosIcon@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/MediaPickerGroupPhotosIcon@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/MediaPickerGroupPhotosIcon@3x.png new file mode 100644 index 0000000000..6b1f0b9a58 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/MediaPickerGroupPhotosIcon@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/MediaPickerUngroupPhotosIcon@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/MediaPickerUngroupPhotosIcon@2x.png new file mode 100644 index 0000000000..145cd45739 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/MediaPickerUngroupPhotosIcon@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/MediaPickerUngroupPhotosIcon@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/MediaPickerUngroupPhotosIcon@3x.png new file mode 100644 index 0000000000..6b4ed8fda9 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/MediaPickerUngroupPhotosIcon@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/ModernConversationAudioSlideToCancel@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/ModernConversationAudioSlideToCancel@2x.png new file mode 100644 index 0000000000..4eaca6a590 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/ModernConversationAudioSlideToCancel@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/ModernConversationAudioSlideToCancel@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/ModernConversationAudioSlideToCancel@3x.png new file mode 100644 index 0000000000..5799af0f18 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/ModernConversationAudioSlideToCancel@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/ModernListsDisclosureIndicator@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/ModernListsDisclosureIndicator@2x.png new file mode 100644 index 0000000000..13f35bc881 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/ModernListsDisclosureIndicator@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/ModernListsDisclosureIndicator@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/ModernListsDisclosureIndicator@3x.png new file mode 100644 index 0000000000..f4d32cedec Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/ModernListsDisclosureIndicator@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/ModernMediaEmptyAlbumIcon@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/ModernMediaEmptyAlbumIcon@2x.png new file mode 100644 index 0000000000..a87bc1ba60 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/ModernMediaEmptyAlbumIcon@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/ModernMediaItemSloMoIcon@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/ModernMediaItemSloMoIcon@2x.png new file mode 100644 index 0000000000..1a9f9ef93a Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/ModernMediaItemSloMoIcon@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/ModernMediaItemTimelapseIcon@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/ModernMediaItemTimelapseIcon@2x.png new file mode 100644 index 0000000000..b3bd9f85eb Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/ModernMediaItemTimelapseIcon@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/ModernMediaItemVideoIcon@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/ModernMediaItemVideoIcon@2x.png new file mode 100644 index 0000000000..bbfceac421 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/ModernMediaItemVideoIcon@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/ModernSegmentedControlBackground@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/ModernSegmentedControlBackground@2x.png new file mode 100644 index 0000000000..159b99f7c7 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/ModernSegmentedControlBackground@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/ModernSegmentedControlBackground@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/ModernSegmentedControlBackground@3x.png new file mode 100644 index 0000000000..90b1d048b2 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/ModernSegmentedControlBackground@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/ModernSegmentedControlDivider@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/ModernSegmentedControlDivider@2x.png new file mode 100644 index 0000000000..fedfad1d89 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/ModernSegmentedControlDivider@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/ModernSegmentedControlDivider@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/ModernSegmentedControlDivider@3x.png new file mode 100644 index 0000000000..c165592d51 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/ModernSegmentedControlDivider@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/ModernSegmentedControlHighlighted@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/ModernSegmentedControlHighlighted@2x.png new file mode 100644 index 0000000000..84fc7c3f7c Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/ModernSegmentedControlHighlighted@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/ModernSegmentedControlHighlighted@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/ModernSegmentedControlHighlighted@3x.png new file mode 100644 index 0000000000..5ea1fc964f Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/ModernSegmentedControlHighlighted@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/ModernSegmentedControlSelected@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/ModernSegmentedControlSelected@2x.png new file mode 100644 index 0000000000..4cd1da2062 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/ModernSegmentedControlSelected@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/ModernSegmentedControlSelected@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/ModernSegmentedControlSelected@3x.png new file mode 100644 index 0000000000..d3165fced5 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/ModernSegmentedControlSelected@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/NavigationSearchIcon@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/NavigationSearchIcon@2x.png new file mode 100644 index 0000000000..0aab63871f Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/NavigationSearchIcon@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PaintBrushIcon@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PaintBrushIcon@2x.png new file mode 100644 index 0000000000..f7f8720e25 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PaintBrushIcon@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PaintBrushIcon@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PaintBrushIcon@3x.png new file mode 100644 index 0000000000..50e0696185 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PaintBrushIcon@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PaintCheck@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PaintCheck@2x.png new file mode 100644 index 0000000000..69d47f8abf Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PaintCheck@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PaintEraserIcon@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PaintEraserIcon@2x.png new file mode 100644 index 0000000000..20d8c4f293 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PaintEraserIcon@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PaintEraserIcon@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PaintEraserIcon@3x.png new file mode 100644 index 0000000000..5b7b7e5bda Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PaintEraserIcon@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PaintMirrorIcon@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PaintMirrorIcon@2x.png new file mode 100644 index 0000000000..e39a1d1dca Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PaintMirrorIcon@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PaintMirrorIcon@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PaintMirrorIcon@3x.png new file mode 100644 index 0000000000..2c13f50e35 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PaintMirrorIcon@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PaintPopupCenterBackground@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PaintPopupCenterBackground@2x.png new file mode 100644 index 0000000000..54f036c022 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PaintPopupCenterBackground@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PaintPopupLandscapeLeftBackground@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PaintPopupLandscapeLeftBackground@2x.png new file mode 100644 index 0000000000..1efe4aaf28 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PaintPopupLandscapeLeftBackground@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PaintPopupLandscapeRightBackground@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PaintPopupLandscapeRightBackground@2x.png new file mode 100644 index 0000000000..bf878ccb5f Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PaintPopupLandscapeRightBackground@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PaintPopupPortraitBackground@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PaintPopupPortraitBackground@2x.png new file mode 100644 index 0000000000..41cca8ef2f Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PaintPopupPortraitBackground@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PaintRedoIcon@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PaintRedoIcon@2x.png new file mode 100644 index 0000000000..10be9f4ea8 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PaintRedoIcon@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PaintSegmentedControlHighlighted@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PaintSegmentedControlHighlighted@2x.png new file mode 100644 index 0000000000..c975d225ae Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PaintSegmentedControlHighlighted@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PaintStickersIcon@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PaintStickersIcon@2x.png new file mode 100644 index 0000000000..d06e5f6b3c Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PaintStickersIcon@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PaintStickersIcon@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PaintStickersIcon@3x.png new file mode 100644 index 0000000000..3d757b2e06 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PaintStickersIcon@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PaintTextIcon@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PaintTextIcon@2x.png new file mode 100644 index 0000000000..839068d0dc Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PaintTextIcon@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PaintTextIcon@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PaintTextIcon@3x.png new file mode 100644 index 0000000000..3aceb45c60 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PaintTextIcon@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PaintTextSettingsIcon@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PaintTextSettingsIcon@2x.png new file mode 100644 index 0000000000..b890b20bca Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PaintTextSettingsIcon@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PaintTextSettingsIcon@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PaintTextSettingsIcon@3x.png new file mode 100644 index 0000000000..364eb3d186 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PaintTextSettingsIcon@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PaintUndoIcon@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PaintUndoIcon@2x.png new file mode 100644 index 0000000000..b371bf5377 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PaintUndoIcon@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PaintUndoIcon@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PaintUndoIcon@3x.png new file mode 100644 index 0000000000..b6269a0892 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PaintUndoIcon@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/Paint_Blit.fsh b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/Paint_Blit.fsh new file mode 100755 index 0000000000..a3b4c6d24c --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/Paint_Blit.fsh @@ -0,0 +1,11 @@ +precision highp float; + +varying vec2 varTexcoord; + +uniform sampler2D texture; + +void main (void) +{ + gl_FragColor = texture2D(texture, varTexcoord.st, 0.0); + gl_FragColor.rgb *= gl_FragColor.a; +} diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/Paint_Blit.vsh b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/Paint_Blit.vsh new file mode 100755 index 0000000000..bb6535aa6a --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/Paint_Blit.vsh @@ -0,0 +1,13 @@ +precision highp float; + +uniform mat4 mvpMatrix; + +attribute vec4 inPosition; +attribute vec2 inTexcoord; +varying vec2 varTexcoord; + +void main (void) +{ + gl_Position = mvpMatrix * inPosition; + varTexcoord = inTexcoord; +} diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/Paint_BlitWithEraseMask.fsh b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/Paint_BlitWithEraseMask.fsh new file mode 100755 index 0000000000..8af5d2bcb9 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/Paint_BlitWithEraseMask.fsh @@ -0,0 +1,17 @@ +precision highp float; + +varying vec2 varTexcoord; + +uniform sampler2D texture; +uniform sampler2D mask; + +void main (void) +{ + vec4 dst = texture2D(texture, varTexcoord.st, 0.0); + float srcAlpha = 1.0 - texture2D(mask, varTexcoord.st, 0.0).a; + + float outAlpha = dst.a * srcAlpha; + + gl_FragColor.rgb = dst.rgb * outAlpha; + gl_FragColor.a = outAlpha; +} diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/Paint_BlitWithMask.fsh b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/Paint_BlitWithMask.fsh new file mode 100755 index 0000000000..2db02d9a85 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/Paint_BlitWithMask.fsh @@ -0,0 +1,20 @@ +precision highp float; + +varying vec2 varTexcoord; + +uniform sampler2D texture; +uniform sampler2D mask; +uniform vec4 color; + +void main (void) +{ + vec4 dst = texture2D(texture, varTexcoord.st, 0.0); + float srcAlpha = color.a * texture2D(mask, varTexcoord.st, 0.0).a; + + float outAlpha = srcAlpha + dst.a * (1.0 - srcAlpha); + + gl_FragColor.rgb = (color.rgb * srcAlpha + dst.rgb * dst.a * (1.0 - srcAlpha)) / outAlpha; + gl_FragColor.a = outAlpha; + + gl_FragColor.rgb *= gl_FragColor.a; +} \ No newline at end of file diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/Paint_BlitWithMaskLight.fsh b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/Paint_BlitWithMaskLight.fsh new file mode 100644 index 0000000000..d44696d48b --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/Paint_BlitWithMaskLight.fsh @@ -0,0 +1,26 @@ +precision highp float; + +varying vec2 varTexcoord; + +uniform sampler2D texture; +uniform sampler2D mask; +uniform vec4 color; + +void main (void) +{ + vec4 dst = texture2D(texture, varTexcoord.st, 0.0); + vec3 maskColor = texture2D(mask, varTexcoord.st, 0.0).rgb; + + float srcAlpha = clamp(0.78 * maskColor.r + maskColor.b + maskColor.g, 0.0, 1.0); + + vec3 borderColor = mix(color.rgb, vec3(1.0, 1.0, 1.0), 0.86); + vec3 finalColor = mix(color.rgb, borderColor, maskColor.g); + finalColor = mix(finalColor.rgb, vec3(1.0, 1.0, 1.0), maskColor.b); + + float outAlpha = srcAlpha + dst.a * (1.0 - srcAlpha); + + gl_FragColor.rgb = (finalColor * srcAlpha + dst.rgb * dst.a * (1.0 - srcAlpha)) / outAlpha; + gl_FragColor.a = outAlpha; + + gl_FragColor.rgb *= gl_FragColor.a; +} diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/Paint_Brush.fsh b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/Paint_Brush.fsh new file mode 100755 index 0000000000..d6effd7b91 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/Paint_Brush.fsh @@ -0,0 +1,14 @@ +precision highp float; + +varying vec2 varTexcoord; +varying float varIntensity; + +uniform sampler2D texture; + +void main (void) +{ + float f = texture2D(texture, varTexcoord.st, 0.0).a; + float v = varIntensity * f; + + gl_FragColor = vec4(0, 0, 0, v); +} diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/Paint_Brush.vsh b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/Paint_Brush.vsh new file mode 100755 index 0000000000..b9b6fe4fb1 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/Paint_Brush.vsh @@ -0,0 +1,16 @@ +precision highp float; + +uniform mat4 mvpMatrix; + +attribute vec4 inPosition; +attribute vec2 inTexcoord; +attribute float alpha; +varying vec2 varTexcoord; +varying float varIntensity; + +void main (void) +{ + gl_Position = mvpMatrix * inPosition; + varTexcoord = inTexcoord; + varIntensity = alpha; +} diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/Paint_BrushLight.fsh b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/Paint_BrushLight.fsh new file mode 100644 index 0000000000..9a94a278d0 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/Paint_BrushLight.fsh @@ -0,0 +1,12 @@ +precision highp float; + +varying vec2 varTexcoord; +varying float varIntensity; + +uniform sampler2D texture; + +void main (void) +{ + vec4 f = texture2D(texture, varTexcoord.st, 0.0); + gl_FragColor = vec4(f.r * varIntensity, f.g, f.b, 0.0); +} diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/Paint_BrushLightPreview.fsh b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/Paint_BrushLightPreview.fsh new file mode 100644 index 0000000000..506a4f975b --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/Paint_BrushLightPreview.fsh @@ -0,0 +1,21 @@ +precision highp float; + +varying vec2 varTexcoord; + +uniform sampler2D mask; +uniform vec4 color; + +void main (void) +{ + vec3 maskColor = texture2D(mask, varTexcoord.st, 0.0).rgb; + float srcAlpha = color.a * clamp(maskColor.r + maskColor.b, 0.0, 1.0); + + vec3 borderColor = vec3(1.0, 1.0, 1.0); + //vec3 finalColor = mix(color.rgb, borderColor, maskColor.g); + vec3 finalColor = mix(color.rgb, vec3(1.0, 1.0, 1.0), maskColor.b); + + gl_FragColor.rgb = (finalColor * srcAlpha) / srcAlpha; + gl_FragColor.a = srcAlpha; + + gl_FragColor.rgb *= gl_FragColor.a; +} diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/Paint_CompositeWithEraseMask.fsh b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/Paint_CompositeWithEraseMask.fsh new file mode 100755 index 0000000000..1d2e7f189b --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/Paint_CompositeWithEraseMask.fsh @@ -0,0 +1,13 @@ +precision highp float; + +varying vec2 varTexcoord; + +uniform sampler2D texture; +uniform sampler2D mask; + +void main (void) +{ + gl_FragColor = texture2D(texture, varTexcoord.st, 0.0); + float srcAlpha = 1.0 - texture2D(mask, varTexcoord.st, 0.0).a; + gl_FragColor.a *= srcAlpha; +} diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/Paint_CompositeWithMask.fsh b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/Paint_CompositeWithMask.fsh new file mode 100755 index 0000000000..3f0b4d26ea --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/Paint_CompositeWithMask.fsh @@ -0,0 +1,18 @@ +precision highp float; + +varying vec2 varTexcoord; + +uniform sampler2D texture; +uniform sampler2D mask; +uniform vec4 color; + +void main (void) +{ + vec4 dst = texture2D(texture, varTexcoord.st, 0.0); + float srcAlpha = color.a * texture2D(mask, varTexcoord.st, 0.0).a; + + float outAlpha = srcAlpha + dst.a * (1.0 - srcAlpha); + + gl_FragColor.rgb = (color.rgb * srcAlpha + dst.rgb * dst.a * (1.0 - srcAlpha)) / outAlpha; + gl_FragColor.a = outAlpha; +} diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/Paint_CompositeWithMaskLight.fsh b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/Paint_CompositeWithMaskLight.fsh new file mode 100644 index 0000000000..aeee1dcf09 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/Paint_CompositeWithMaskLight.fsh @@ -0,0 +1,24 @@ +precision highp float; + +varying vec2 varTexcoord; + +uniform sampler2D texture; +uniform sampler2D mask; +uniform vec4 color; + +void main (void) +{ + vec4 dst = texture2D(texture, varTexcoord.st, 0.0); + vec3 maskColor = texture2D(mask, varTexcoord.st, 0.0).rgb; + + float srcAlpha = clamp(0.78 * maskColor.r + maskColor.b + maskColor.g, 0.0, 1.0); + + vec3 borderColor = mix(color.rgb, vec3(1.0, 1.0, 1.0), 0.86); + vec3 finalColor = mix(color.rgb, borderColor, maskColor.g); + finalColor = mix(finalColor.rgb, vec3(1.0, 1.0, 1.0), maskColor.b); + + float outAlpha = srcAlpha + dst.a * (1.0 - srcAlpha); + + gl_FragColor.rgb = (finalColor * srcAlpha + dst.rgb * dst.a * (1.0 - srcAlpha)) / outAlpha; + gl_FragColor.a = outAlpha; +} diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/Paint_NonPremultipliedBlit.fsh b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/Paint_NonPremultipliedBlit.fsh new file mode 100755 index 0000000000..fe2a39f049 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/Paint_NonPremultipliedBlit.fsh @@ -0,0 +1,10 @@ +precision highp float; + +varying vec2 varTexcoord; + +uniform sampler2D texture; + +void main (void) +{ + gl_FragColor = texture2D(texture, varTexcoord.st, 0.0); +} \ No newline at end of file diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/Passthrough.fsh b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/Passthrough.fsh new file mode 100644 index 0000000000..acbf3fdbef --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/Passthrough.fsh @@ -0,0 +1,9 @@ +precision mediump float; + +varying mediump vec2 varTexcoord; +uniform sampler2D texture; + +void main() +{ + gl_FragColor = texture2D(texture, varTexcoord); +} diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/Passthrough.vsh b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/Passthrough.vsh new file mode 100644 index 0000000000..78e3bb95f6 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/Passthrough.vsh @@ -0,0 +1,10 @@ +attribute vec4 inPosition; +attribute mediump vec4 inTexcoord; +varying mediump vec2 varTexcoord; + +void main() +{ + gl_Position = inPosition; + varTexcoord = inTexcoord.xy; +} + diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PermissionSwitchOff@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PermissionSwitchOff@2x.png new file mode 100644 index 0000000000..3d440aca23 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PermissionSwitchOff@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PermissionSwitchOff@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PermissionSwitchOff@3x.png new file mode 100644 index 0000000000..721dd962d4 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PermissionSwitchOff@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PermissionSwitchOn@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PermissionSwitchOn@2x.png new file mode 100644 index 0000000000..0731b92a71 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PermissionSwitchOn@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PermissionSwitchOn@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PermissionSwitchOn@3x.png new file mode 100644 index 0000000000..b3855cbcfe Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PermissionSwitchOn@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorAspectRatioIcon@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorAspectRatioIcon@2x.png new file mode 100644 index 0000000000..c1944b5f66 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorAspectRatioIcon@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorAspectRatioIcon@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorAspectRatioIcon@3x.png new file mode 100644 index 0000000000..e4c5bd9b29 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorAspectRatioIcon@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorBlurIcon@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorBlurIcon@2x.png new file mode 100644 index 0000000000..17b1e58928 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorBlurIcon@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorBlurIcon@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorBlurIcon@3x.png new file mode 100644 index 0000000000..0ff98548c3 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorBlurIcon@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorBlurLinear.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorBlurLinear.png new file mode 100644 index 0000000000..1223538117 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorBlurLinear.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorBlurLinear@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorBlurLinear@2x.png new file mode 100644 index 0000000000..e3f67c4449 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorBlurLinear@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorBlurLinear@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorBlurLinear@3x.png new file mode 100644 index 0000000000..eb6279040c Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorBlurLinear@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorBlurOff.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorBlurOff.png new file mode 100644 index 0000000000..037d980c35 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorBlurOff.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorBlurOff@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorBlurOff@2x.png new file mode 100644 index 0000000000..0dc203196e Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorBlurOff@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorBlurOff@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorBlurOff@3x.png new file mode 100644 index 0000000000..1de339b991 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorBlurOff@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorBlurRadial.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorBlurRadial.png new file mode 100644 index 0000000000..f5903ed9ad Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorBlurRadial.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorBlurRadial@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorBlurRadial@2x.png new file mode 100644 index 0000000000..e689c5f6fd Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorBlurRadial@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorBlurRadial@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorBlurRadial@3x.png new file mode 100644 index 0000000000..a6601bfdb3 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorBlurRadial@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorCamera@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorCamera@2x.png new file mode 100644 index 0000000000..0f60a96a92 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorCamera@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorCamera@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorCamera@3x.png new file mode 100644 index 0000000000..9f67c1b5d4 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorCamera@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorCaption.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorCaption.png new file mode 100644 index 0000000000..1124711660 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorCaption.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorCaption@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorCaption@2x.png new file mode 100644 index 0000000000..6ed8cb0d45 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorCaption@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorCaption@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorCaption@3x.png new file mode 100644 index 0000000000..3c785a48c1 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorCaption@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorCrop@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorCrop@2x.png new file mode 100644 index 0000000000..a6ed7807f2 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorCrop@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorCrop@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorCrop@3x.png new file mode 100644 index 0000000000..1490c67dca Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorCrop@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorCropCorners@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorCropCorners@2x.png new file mode 100644 index 0000000000..b9a0ac370f Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorCropCorners@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorCurvesIcon@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorCurvesIcon@2x.png new file mode 100644 index 0000000000..568807854e Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorCurvesIcon@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorCurvesIcon@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorCurvesIcon@3x.png new file mode 100644 index 0000000000..48ce0ee86f Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorCurvesIcon@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorGif@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorGif@2x.png new file mode 100644 index 0000000000..2e10938f3d Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorGif@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorGif@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorGif@3x.png new file mode 100644 index 0000000000..cbc638ee95 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorGif@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorGifShadow@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorGifShadow@2x.png new file mode 100644 index 0000000000..43c6612c0c Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorGifShadow@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorGifShadow@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorGifShadow@3x.png new file mode 100644 index 0000000000..9dca9739b5 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorGifShadow@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorGroupPhotosIcon@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorGroupPhotosIcon@2x.png new file mode 100644 index 0000000000..a579e71442 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorGroupPhotosIcon@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorGroupPhotosIcon@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorGroupPhotosIcon@3x.png new file mode 100644 index 0000000000..ba3dd42034 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorGroupPhotosIcon@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorMirrorIcon@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorMirrorIcon@2x.png new file mode 100644 index 0000000000..ec7a1401a2 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorMirrorIcon@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorMirrorIcon@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorMirrorIcon@3x.png new file mode 100644 index 0000000000..06abfe2818 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorMirrorIcon@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorMute@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorMute@2x.png new file mode 100644 index 0000000000..3012a49a60 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorMute@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorMuteActive@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorMuteActive@2x.png new file mode 100644 index 0000000000..61175da4aa Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorMuteActive@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorPaint@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorPaint@2x.png new file mode 100644 index 0000000000..41f3d9765d Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorPaint@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorPaint@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorPaint@3x.png new file mode 100644 index 0000000000..fb8c602ad1 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorPaint@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorQuality@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorQuality@2x.png new file mode 100644 index 0000000000..fff3a37e75 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorQuality@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorQuality@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorQuality@3x.png new file mode 100644 index 0000000000..9e6e570152 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorQuality@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorRotateIcon@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorRotateIcon@2x.png new file mode 100644 index 0000000000..4cf8c66ef8 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorRotateIcon@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorRotateIcon@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorRotateIcon@3x.png new file mode 100644 index 0000000000..937885fcaa Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorRotateIcon@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorRotationNeedle@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorRotationNeedle@2x.png new file mode 100644 index 0000000000..c59b218a2c Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorRotationNeedle@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorRotationWheel@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorRotationWheel@2x.png new file mode 100644 index 0000000000..218551f7f4 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorRotationWheel@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorRotationWheelLeft@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorRotationWheelLeft@2x.png new file mode 100644 index 0000000000..fb4b81797e Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorRotationWheelLeft@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorRotationWheelRight@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorRotationWheelRight@2x.png new file mode 100644 index 0000000000..9853411408 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorRotationWheelRight@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorTimer0@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorTimer0@2x.png new file mode 100644 index 0000000000..064c43d18a Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorTimer0@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorTimer0@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorTimer0@3x.png new file mode 100644 index 0000000000..9010b48edd Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorTimer0@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorTimer@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorTimer@2x.png new file mode 100644 index 0000000000..304b362929 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorTimer@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorTimer@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorTimer@3x.png new file mode 100644 index 0000000000..996683b0a2 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorTimer@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorTintIcon@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorTintIcon@2x.png new file mode 100644 index 0000000000..87138622f8 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorTintIcon@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorTintIcon@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorTintIcon@3x.png new file mode 100644 index 0000000000..133880fe7c Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorTintIcon@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorTintTool@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorTintTool@2x.png new file mode 100644 index 0000000000..fb64ec8327 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorTintTool@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorTools@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorTools@2x.png new file mode 100644 index 0000000000..c814f6114a Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorTools@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorTools@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorTools@3x.png new file mode 100644 index 0000000000..df1bb1879f Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoEditorTools@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoPickerArrow@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoPickerArrow@2x.png new file mode 100644 index 0000000000..11824c738a Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoPickerArrow@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoPickerArrow@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoPickerArrow@3x.png new file mode 100644 index 0000000000..c792cbadc2 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoPickerArrow@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoPickerBackIcon@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoPickerBackIcon@2x.png new file mode 100644 index 0000000000..483a034b64 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoPickerBackIcon@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoPickerBackIcon@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoPickerBackIcon@3x.png new file mode 100644 index 0000000000..3fb90131b3 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoPickerBackIcon@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoPickerCancelIcon@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoPickerCancelIcon@2x.png new file mode 100644 index 0000000000..dec7b64a1a Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoPickerCancelIcon@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoPickerCancelIcon@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoPickerCancelIcon@3x.png new file mode 100644 index 0000000000..512bcad695 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoPickerCancelIcon@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoPickerDoneIcon@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoPickerDoneIcon@2x.png new file mode 100644 index 0000000000..cf3cd7d76e Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoPickerDoneIcon@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoPickerDoneIcon@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoPickerDoneIcon@3x.png new file mode 100644 index 0000000000..f356577224 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoPickerDoneIcon@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoPickerSendIcon@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoPickerSendIcon@2x.png new file mode 100644 index 0000000000..719bcbd280 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoPickerSendIcon@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoPickerSendIcon@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoPickerSendIcon@3x.png new file mode 100644 index 0000000000..bd23c60ed9 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PhotoPickerSendIcon@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PreviewSheetShadow@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PreviewSheetShadow@2x.png new file mode 100644 index 0000000000..c6f2a7b926 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PreviewSheetShadow@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PreviewUpArrow@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PreviewUpArrow@2x.png new file mode 100644 index 0000000000..eb133303b0 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/PreviewUpArrow@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/RecordSendIcon@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/RecordSendIcon@2x.png new file mode 100644 index 0000000000..6c5628c51f Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/RecordSendIcon@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/RecordSendIcon@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/RecordSendIcon@3x.png new file mode 100644 index 0000000000..b1457fe819 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/RecordSendIcon@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/RecordVideoIconOverlay@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/RecordVideoIconOverlay@2x.png new file mode 100644 index 0000000000..163a2e3434 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/RecordVideoIconOverlay@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/RecordVideoIconOverlay@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/RecordVideoIconOverlay@3x.png new file mode 100644 index 0000000000..7a571bb998 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/RecordVideoIconOverlay@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/Star@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/Star@2x.png new file mode 100644 index 0000000000..0cbe785016 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/Star@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/Star@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/Star@3x.png new file mode 100644 index 0000000000..5da86261c6 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/Star@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/StickerKeyboardFavoriteTab@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/StickerKeyboardFavoriteTab@2x.png new file mode 100644 index 0000000000..d30bef4955 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/StickerKeyboardFavoriteTab@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/StickerKeyboardFavoriteTab@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/StickerKeyboardFavoriteTab@3x.png new file mode 100644 index 0000000000..64706d7681 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/StickerKeyboardFavoriteTab@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/StickerKeyboardGifIcon@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/StickerKeyboardGifIcon@2x.png new file mode 100644 index 0000000000..382c35e033 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/StickerKeyboardGifIcon@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/StickerKeyboardGifIcon@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/StickerKeyboardGifIcon@3x.png new file mode 100644 index 0000000000..d760fa4947 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/StickerKeyboardGifIcon@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/StickerKeyboardRecentTab@1x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/StickerKeyboardRecentTab@1x.png new file mode 100644 index 0000000000..b48545a8d7 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/StickerKeyboardRecentTab@1x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/StickerKeyboardRecentTab@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/StickerKeyboardRecentTab@2x.png new file mode 100644 index 0000000000..95cc50e35f Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/StickerKeyboardRecentTab@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/StickerKeyboardRecentTab@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/StickerKeyboardRecentTab@3x.png new file mode 100644 index 0000000000..045a394615 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/StickerKeyboardRecentTab@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/StickerKeyboardSettingsIcon@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/StickerKeyboardSettingsIcon@2x.png new file mode 100644 index 0000000000..d0dc89b9ff Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/StickerKeyboardSettingsIcon@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/StickerKeyboardSettingsIcon@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/StickerKeyboardSettingsIcon@3x.png new file mode 100644 index 0000000000..54b04299e5 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/StickerKeyboardSettingsIcon@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/StickerKeyboardTrendingIcon@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/StickerKeyboardTrendingIcon@2x.png new file mode 100644 index 0000000000..67de340019 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/StickerKeyboardTrendingIcon@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/StickerKeyboardTrendingIcon@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/StickerKeyboardTrendingIcon@3x.png new file mode 100644 index 0000000000..ed14979353 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/StickerKeyboardTrendingIcon@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/TooltipArrow@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/TooltipArrow@2x.png new file mode 100644 index 0000000000..132cca2b05 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/TooltipArrow@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/TooltipArrow@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/TooltipArrow@3x.png new file mode 100644 index 0000000000..5f84f22d83 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/TooltipArrow@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/TooltipBackground@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/TooltipBackground@2x.png new file mode 100644 index 0000000000..280f2a4f42 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/TooltipBackground@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/TooltipBackground@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/TooltipBackground@3x.png new file mode 100644 index 0000000000..7718e92336 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/TooltipBackground@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/TrackingHeading@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/TrackingHeading@2x.png new file mode 100644 index 0000000000..be5ecb85d3 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/TrackingHeading@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/TrackingHeading@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/TrackingHeading@3x.png new file mode 100644 index 0000000000..a365339ceb Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/TrackingHeading@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/TrackingLocation@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/TrackingLocation@2x.png new file mode 100644 index 0000000000..e2dae9e8ff Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/TrackingLocation@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/TrackingLocation@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/TrackingLocation@3x.png new file mode 100644 index 0000000000..7a78214280 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/TrackingLocation@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/TrackingLocationOff@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/TrackingLocationOff@2x.png new file mode 100644 index 0000000000..01f4da7cbc Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/TrackingLocationOff@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/TrackingLocationOff@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/TrackingLocationOff@3x.png new file mode 100644 index 0000000000..be8ac1a6ab Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/TrackingLocationOff@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/TwitchPlayer.html b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/TwitchPlayer.html new file mode 100755 index 0000000000..3522534816 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/TwitchPlayer.html @@ -0,0 +1,53 @@ + + + + + + +
+ +
+ + + diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/TwitchPlayerInject.js b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/TwitchPlayerInject.js new file mode 100644 index 0000000000..1325020db9 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/TwitchPlayerInject.js @@ -0,0 +1,129 @@ +var video; + +function hidePlayButton() { + var css = 'video::-webkit-media-controls { display: none !important } video::--webkit-media-controls-play-button { display: none !important; -webkit-appearance: none; } video::-webkit-media-controls-start-playback-button { display: none !important; -webkit-appearance: none; }', + head = document.head || document.getElementsByTagName('head')[0], + style = document.createElement('style'); + + style.type = 'text/css'; + if (style.styleSheet){ + style.styleSheet.cssText = css; + } else { + style.appendChild(document.createTextNode(css)); + } + + head.appendChild(style); +} + +function fixPlayer() { + var controls = document.getElementsByClassName('player-controls-bottom')[0]; + if (controls == null) { + controls = document.getElementsByClassName('player-overlay-container')[0]; + } + if (controls != null) { + controls.style.display = 'none'; + } + + var topBar = document.getElementById('top-bar'); + if (topBar == null) { + topBar = document.getElementsByClassName('player-controls-top')[0]; + } + if (topBar != null) { + topBar.style.display = 'none'; + } + + var pauseOverlay = document.getElementsByClassName('player-play-overlay')[0]; + if (pauseOverlay == null) { + pauseOverlay = document.getElementsByClassName('player-controls-bottom')[0]; + } + if (pauseOverlay != null) { + pauseOverlay.style.display = 'none'; + } + + var statusOverlay = document.getElementsByClassName('player-streamstatus')[0]; + if (statusOverlay != null) { + statusOverlay.style.right = undefined; + statusOverlay.style.left = '0px'; + statusOverlay.style.padding = '1.5em 1.5em 5.5em 2.5em'; + } + + var recommendationOverlay = document.getElementById('js-player-recommendations-overlay'); + if (recommendationOverlay != null) { + recommendationOverlay.style.display = 'none'; + } + + var adOverlay = document.getElementsByClassName('player-ad-overlay')[0]; + if (adOverlay != null) { + adOverlay.style.display = 'none'; + } + + var alertOverlay = document.getElementById('js-player-alert-container'); + if (alertOverlay != null) { + alertOverlay.style.display = 'none'; + } + + var video = document.getElementsByTagName('video')[0]; + video.setAttribute('webkit-playsinline', ''); + video.setAttribute('playsinline', ''); + video.webkitEnterFullscreen = undefined; + + video.addEventListener("playing", onPlaybackStart, false); + + video.play(); + + hidePlayButton(); + + var ageButton = document.getElementById('mature-link'); + if (ageButton != null) { + eventFire(ageButton, 'click'); + } +} + +function onPlaybackStart() { + window.parent.postMessage('playbackStarted', '*'); +} + +function switchToPIP() { + var video = document.getElementsByTagName('video')[0]; + video.webkitSetPresentationMode('picture-in-picture'); +} + +function eventFire(el, etype){ + if (el.fireEvent) { + el.fireEvent('on' + etype); + } else { + var evObj = document.createEvent('Events'); + evObj.initEvent(etype, true, false); + el.dispatchEvent(evObj); + } +} + +function play() { + var playButton = document.getElementsByClassName('js-control-playpause-button')[0]; + if (playButton == null) { + playButton = document.getElementsByClassName('player-button--playpause')[0]; + } + + eventFire(playButton, 'click'); +} + +function receiveMessage(evt) { + if ((typeof evt.data) != 'string') + return; + + try { + var obj = JSON.parse(evt.data); + if (!obj.event || obj.event != 'inject') + return; + + if (obj.cmd == 'fixPlayer') + fixPlayer(); + else if (obj.cmd == 'play') + play(); + else if (obj.cmd == 'switchToPIP') + switchToPIP(); + } catch (ex) { } +} + +window.addEventListener('message', receiveMessage, false); + diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/VideoMessage.fsh b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/VideoMessage.fsh new file mode 100644 index 0000000000..6fc0dbdf41 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/VideoMessage.fsh @@ -0,0 +1,32 @@ +precision mediump float; + +varying mediump vec2 varTexcoord; +uniform sampler2D texture; +uniform sampler2D previousTexture; +uniform mediump float opacity; +uniform mediump float aspectRatio; +uniform mediump float noMirror; + +void main() +{ + vec2 texcoord = vec2(1.0 - varTexcoord.x * noMirror + clamp(noMirror, -1.0, 0.0), varTexcoord.y); + vec2 prevTexcoord = vec2(texcoord.x, 1.0 - texcoord.y); + + vec4 white = vec4(1.0, 1.0, 1.0, 1.0); + vec4 color = texture2D(texture, texcoord); + vec4 previousColor = texture2D(previousTexture, prevTexcoord); + + color.rgb = mix(color.rgb, previousColor.rgb, opacity); + + vec2 c = vec2((varTexcoord.x * 2.0 - 1.0) * aspectRatio, varTexcoord.y * 2.0 - 1.0) * 0.996; + vec2 c1 = c - 0.005; + vec2 c2 = c + 0.005; + vec2 c3 = vec2(c1.x, c2.y); + vec2 c4 = vec2(c2.x, c1.y); + float s1 = floor(dot(c1, c1)); + float s2 = floor(dot(c2, c2)); + float s3 = floor(dot(c3, c3)); + float s4 = floor(dot(c4, c4)); + + gl_FragColor = mix(color, white, (s1 + s2 + s3 + s4) * 0.25); +} diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/VideoMessage.vsh b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/VideoMessage.vsh new file mode 100644 index 0000000000..78e3bb95f6 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/VideoMessage.vsh @@ -0,0 +1,10 @@ +attribute vec4 inPosition; +attribute mediump vec4 inTexcoord; +varying mediump vec2 varTexcoord; + +void main() +{ + gl_Position = inPosition; + varTexcoord = inTexcoord.xy; +} + diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/VideoMessageLeftHandle@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/VideoMessageLeftHandle@3x.png new file mode 100644 index 0000000000..c10475513b Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/VideoMessageLeftHandle@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/VideoMessageMutedIcon@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/VideoMessageMutedIcon@2x.png new file mode 100644 index 0000000000..6e933e1e40 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/VideoMessageMutedIcon@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/VideoMessageMutedIcon@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/VideoMessageMutedIcon@3x.png new file mode 100644 index 0000000000..167ceb57dd Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/VideoMessageMutedIcon@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/VideoMessagePlaceholder.jpg b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/VideoMessagePlaceholder.jpg new file mode 100644 index 0000000000..5de9839e6f Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/VideoMessagePlaceholder.jpg differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/VideoMessageRightHandle@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/VideoMessageRightHandle@3x.png new file mode 100644 index 0000000000..a165ba40a9 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/VideoMessageRightHandle@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/VideoMessageScrubberLeftMask@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/VideoMessageScrubberLeftMask@3x.png new file mode 100644 index 0000000000..41d4258698 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/VideoMessageScrubberLeftMask@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/VideoMessageScrubberRightMask@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/VideoMessageScrubberRightMask@3x.png new file mode 100644 index 0000000000..a3d0141774 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/VideoMessageScrubberRightMask@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/VideoMessageShadow@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/VideoMessageShadow@2x.png new file mode 100644 index 0000000000..7f9680c2be Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/VideoMessageShadow@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/VideoMessageShadow@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/VideoMessageShadow@3x.png new file mode 100644 index 0000000000..de8d7887ac Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/VideoMessageShadow@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/VideoRecordArrow@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/VideoRecordArrow@2x.png new file mode 100644 index 0000000000..ad187da93b Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/VideoRecordArrow@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/VideoRecordArrow@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/VideoRecordArrow@3x.png new file mode 100644 index 0000000000..31a6e5fd32 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/VideoRecordArrow@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/VideoRecordPositionSwitch@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/VideoRecordPositionSwitch@2x.png new file mode 100644 index 0000000000..da64f34de3 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/VideoRecordPositionSwitch@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/VideoRecordPositionSwitch@3x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/VideoRecordPositionSwitch@3x.png new file mode 100644 index 0000000000..b4ea66d10c Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/VideoRecordPositionSwitch@3x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/VideoScrubberLeftArrow@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/VideoScrubberLeftArrow@2x.png new file mode 100644 index 0000000000..c5299dce46 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/VideoScrubberLeftArrow@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/VideoScrubberRightArrow@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/VideoScrubberRightArrow@2x.png new file mode 100644 index 0000000000..86d32b4f80 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/VideoScrubberRightArrow@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/VimeoPlayer.html b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/VimeoPlayer.html new file mode 100755 index 0000000000..5175d13427 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/VimeoPlayer.html @@ -0,0 +1,95 @@ + + + + + + +
+ +
+ + + diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/VimeoPlayerInject.js b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/VimeoPlayerInject.js new file mode 100644 index 0000000000..8b1cd8bc28 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/VimeoPlayerInject.js @@ -0,0 +1,52 @@ +function fixPlayer() { + var controls = document.getElementsByClassName('controls')[0]; + controls.style.display = 'none'; + + var sidedock = document.getElementsByClassName('sidedock')[0]; + sidedock.style.display = 'none'; + + var video = document.getElementsByTagName('video')[0]; + video.setAttribute('webkit-playsinline', ''); + video.setAttribute('playsinline', ''); + video.webkitEnterFullscreen = undefined; +} + +function switchToPIP() { + var video = document.getElementsByTagName('video')[0]; + video.webkitSetPresentationMode('picture-in-picture'); +} + +function eventFire(el, etype){ + if (el.fireEvent) { + el.fireEvent('on' + etype); + } else { + var evObj = document.createEvent('Events'); + evObj.initEvent(etype, true, false); + el.dispatchEvent(evObj); + } +} + +function initialPlay() { + var playButton = document.getElementsByClassName('play')[0]; + eventFire(playButton, 'click'); +} + +function receiveMessage(evt) { + if ((typeof evt.data) != 'string') + return; + + try { + var obj = JSON.parse(evt.data); + if (!obj.event || obj.event != 'inject') + return; + + if (obj.cmd == 'fixPlayer') + fixPlayer(); + else if (obj.cmd == 'initialPlay') + initialPlay(); + else if (obj.cmd == 'switchToPIP') + switchToPIP(); + } catch (ex) { } +} + +window.addEventListener('message', receiveMessage, false); diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/VinePlayer.html b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/VinePlayer.html new file mode 100755 index 0000000000..2fba84115c --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/VinePlayer.html @@ -0,0 +1,51 @@ + + + + + + +
+ +
+ + + diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/VinePlayerInject.js b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/VinePlayerInject.js new file mode 100644 index 0000000000..cc57644e2a --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/VinePlayerInject.js @@ -0,0 +1,80 @@ +function hidePlayButton() { + var css = 'video::-webkit-media-controls { display: none !important }', + head = document.head || document.getElementsByTagName('head')[0], + style = document.createElement('style'); + + style.type = 'text/css'; + if (style.styleSheet){ + style.styleSheet.cssText = css; + } else { + style.appendChild(document.createTextNode(css)); + } + + head.appendChild(style); +} + +var video; + +function fixPlayer() { + var controls = document.getElementsByClassName('VolumeControl')[0]; + controls.style.display = 'none'; + + video.setAttribute('webkit-playsinline', ''); + video.setAttribute('playsinline', ''); + video.webkitEnterFullscreen = undefined; + + hidePlayButton(); +} + +function play() { + video.play(); +} + +function pause() { + video.pause(); +} + +function showWatermark() { + var logo = document.getElementsByClassName('vine-logo')[0]; + logo.style.display = 'block'; +} + +function hideWatermark() { + var logo = document.getElementsByClassName('vine-logo')[0]; + logo.style.display = 'none'; +} + +function receiveMessage(evt) { + if ((typeof evt.data) != 'string') + return; + + try { + var obj = JSON.parse(evt.data); + if (!obj.event || obj.event != 'inject') + return; + + if (obj.cmd == 'play') + play(); + else if (obj.cmd == 'pause') + pause(); + else if (obj.cmd == 'showWatermark') + showWatermark(); + else if (obj.cmd == 'hideWatermark') + hideWatermark(); + } catch (ex) { } +} + +window.addEventListener('message', receiveMessage, false); + +var video = document.getElementsByTagName('video')[0]; + +fixPlayer(); +hidePlayButton(); + +video.addEventListener("playing", onPlaybackStart, false); + +window.parent.postMessage(JSON.stringify({ "event": "src", "data": video.src }), '*'); + +function onPlaybackStart() { + window.parent.postMessage('playbackStarted', '*'); +} diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/VineWatermark@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/VineWatermark@2x.png new file mode 100644 index 0000000000..3457c7df07 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/VineWatermark@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/YoutubePlayer.html b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/YoutubePlayer.html new file mode 100755 index 0000000000..8beffcad61 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/YoutubePlayer.html @@ -0,0 +1,98 @@ + + + + + + +
+
+
+ + + + diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/YoutubePlayerInject.js b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/YoutubePlayerInject.js new file mode 100644 index 0000000000..5783654581 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/YoutubePlayerInject.js @@ -0,0 +1,51 @@ +function fixChrome() { + var watermark = document.getElementsByClassName('ytp-watermark')[0]; + watermark.style.display = 'none'; + + var button = document.getElementsByClassName('ytp-large-play-button')[0]; + button.style.display = 'none'; + button.style.opacity = '0'; + + var progress = document.getElementsByClassName('ytp-spinner-container')[0]; + progress.style.display = 'none'; + progress.style.opacity = '0'; + + var video = document.getElementsByTagName('video')[0]; + video.setAttribute('webkit-playsinline', ''); + video.setAttribute('playsinline', ''); + video.webkitEnterFullscreen = undefined; +} + +function initial() { + var css = 'video::-webkit-media-controls { display: none !important } video::--webkit-media-controls-play-button { display: none !important; -webkit-appearance: none; } video::-webkit-media-controls-start-playback-button { display: none !important; -webkit-appearance: none; }', + head = document.head || document.getElementsByTagName('head')[0], + style = document.createElement('style'); + + style.type = 'text/css'; + if (style.styleSheet){ + style.styleSheet.cssText = css; + } else { + style.appendChild(document.createTextNode(css)); + } + + head.appendChild(style); +} + +function switchToPIP() { + var video = document.getElementsByTagName('video')[0]; + video.webkitSetPresentationMode('picture-in-picture'); +} + +function receiveMessage(evt) { + try { + var obj = JSON.parse(evt.data); + if (obj.cmd == 'fixChrome') + fixChrome(); + else if (obj.cmd == 'initial') + initial(); + else if (obj.cmd == 'switchToPIP') + switchToPIP(); + } catch (ex) { } +} + +window.addEventListener('message', receiveMessage, false); diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/YoutubeWatermark@2x.png b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/YoutubeWatermark@2x.png new file mode 100644 index 0000000000..63ac731795 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/YoutubeWatermark@2x.png differ diff --git a/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/ocr_nn.bin b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/ocr_nn.bin new file mode 100755 index 0000000000..ccab89e554 Binary files /dev/null and b/submodules/LegacyComponents/LegacyComponents/Resources/LegacyComponentsResources.bundle/ocr_nn.bin differ diff --git a/submodules/LegacyComponents/LegacyComponents/SGraphListNode.h b/submodules/LegacyComponents/LegacyComponents/SGraphListNode.h new file mode 100644 index 0000000000..c20ddc4550 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/SGraphListNode.h @@ -0,0 +1,9 @@ +#import + +@interface SGraphListNode : SGraphNode + +@property (nonatomic, strong) NSArray *items; + +- (id)initWithItems:(NSArray *)items; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/SGraphListNode.m b/submodules/LegacyComponents/LegacyComponents/SGraphListNode.m new file mode 100644 index 0000000000..3214653721 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/SGraphListNode.m @@ -0,0 +1,17 @@ +#import "SGraphListNode.h" + +@implementation SGraphListNode + +@synthesize items = _items; + +- (id)initWithItems:(NSArray *)items +{ + self = [super init]; + if (self != nil) + { + _items = items; + } + return self; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/SGraphNode.h b/submodules/LegacyComponents/LegacyComponents/SGraphNode.h new file mode 100644 index 0000000000..779a3dc29c --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/SGraphNode.h @@ -0,0 +1,5 @@ +#import + +@interface SGraphNode : NSObject + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/SGraphNode.m b/submodules/LegacyComponents/LegacyComponents/SGraphNode.m new file mode 100644 index 0000000000..79e1d31ba9 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/SGraphNode.m @@ -0,0 +1,5 @@ +#import "SGraphNode.h" + +@implementation SGraphNode + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/SGraphObjectNode.h b/submodules/LegacyComponents/LegacyComponents/SGraphObjectNode.h new file mode 100644 index 0000000000..1e94dec6d7 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/SGraphObjectNode.h @@ -0,0 +1,9 @@ +#import + +@interface SGraphObjectNode : SGraphNode + +@property (nonatomic, strong) id object; + +- (id)initWithObject:(id)object; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/SGraphObjectNode.m b/submodules/LegacyComponents/LegacyComponents/SGraphObjectNode.m new file mode 100644 index 0000000000..2e1542d4c9 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/SGraphObjectNode.m @@ -0,0 +1,17 @@ +#import "SGraphObjectNode.h" + +@implementation SGraphObjectNode + +@synthesize object = _object; + +- (id)initWithObject:(id)object +{ + self = [super init]; + if (self != nil) + { + _object = object; + } + return self; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/STKAudioPlayer.h b/submodules/LegacyComponents/LegacyComponents/STKAudioPlayer.h new file mode 100755 index 0000000000..0ce6d53852 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/STKAudioPlayer.h @@ -0,0 +1,266 @@ +/********************************************************************************** + AudioPlayer.m + + Created by Thong Nguyen on 14/05/2012. + https://github.com/tumtumtum/StreamingKit + + Inspired by Matt Gallagher's AudioStreamer: + https://github.com/mattgallagher/AudioStreamer + + Copyright (c) 2012-2014 Thong Nguyen (tumtumtum@gmail.com). All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + 3. All advertising materials mentioning features or use of this software + must display the following acknowledgement: + This product includes software developed by Thong Nguyen (tumtumtum@gmail.com) + 4. Neither the name of Thong Nguyen nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY Thong Nguyen''AS IS'' AND ANY + EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **********************************************************************************/ + +#import +#import +#import "STKDataSource.h" +#import + +#if TARGET_OS_IPHONE +#include "UIKit/UIApplication.h" +#endif + +typedef enum +{ + STKAudioPlayerStateReady, + STKAudioPlayerStateRunning = 1, + STKAudioPlayerStatePlaying = (1 << 1) | STKAudioPlayerStateRunning, + STKAudioPlayerStateBuffering = (1 << 2) | STKAudioPlayerStateRunning, + STKAudioPlayerStatePaused = (1 << 3) | STKAudioPlayerStateRunning, + STKAudioPlayerStateStopped = (1 << 4), + STKAudioPlayerStateError = (1 << 5), + STKAudioPlayerStateDisposed = (1 << 6) +} +STKAudioPlayerState; + +typedef enum +{ + STKAudioPlayerStopReasonNone = 0, + STKAudioPlayerStopReasonEof, + STKAudioPlayerStopReasonUserAction, + STKAudioPlayerStopReasonPendingNext, + STKAudioPlayerStopReasonDisposed, + STKAudioPlayerStopReasonError = 0xffff +} +STKAudioPlayerStopReason; + +typedef enum +{ + STKAudioPlayerErrorNone = 0, + STKAudioPlayerErrorDataSource, + STKAudioPlayerErrorStreamParseBytesFailed, + STKAudioPlayerErrorAudioSystemError, + STKAudioPlayerErrorCodecError, + STKAudioPlayerErrorDataNotFound, + STKAudioPlayerErrorOther = 0xffff +} +STKAudioPlayerErrorCode; + +typedef struct +{ + /// If YES then seeking a track will cause all pending items to be flushed from the queue + BOOL flushQueueOnSeek; + /// If YES then volume control will be enabled on iOS + BOOL enableVolumeMixer; + /// A pointer to a 0 terminated array of band frequencies (iOS 5.0 and later, OSX 10.9 and later) + Float32 equalizerBandFrequencies[24]; + /// The size of the internal I/O read buffer. This data in this buffer is transient and does not need to be larger. + UInt32 readBufferSize; + /// The size of the decompressed buffer (Default is 10 seconds which uses about 1.7MB of RAM) + UInt32 bufferSizeInSeconds; + /// Number of seconds of decompressed audio is required before playback first starts for each item (Default is 0.5 seconds. Must be larger than bufferSizeInSeconds) + Float32 secondsRequiredToStartPlaying; + /// Seconds after a seek is performed before data needs to come in (after which the state will change to playing/buffering) + Float32 gracePeriodAfterSeekInSeconds; + /// Number of seconds of decompressed audio required before playback resumes after a buffer underrun (Default is 5 seconds. Must be larger than bufferSizeinSeconds) + Float32 secondsRequiredToStartPlayingAfterBufferUnderun; +} +STKAudioPlayerOptions; + +typedef void(^STKFrameFilter)(UInt32 channelsPerFrame, UInt32 bytesPerFrame, UInt32 frameCount, void* frames); + +@interface STKFrameFilterEntry : NSObject +@property (readonly) NSString* name; +@property (readonly) STKFrameFilter filter; +@end + +@class STKAudioPlayer; + +@protocol STKAudioPlayerDelegate + +/// Raised when an item has started playing +-(void) audioPlayer:(STKAudioPlayer*)audioPlayer didStartPlayingQueueItemId:(NSObject*)queueItemId; +/// Raised when an item has finished buffering (may or may not be the currently playing item) +/// This event may be raised multiple times for the same item if seek is invoked on the player +-(void) audioPlayer:(STKAudioPlayer*)audioPlayer didFinishBufferingSourceWithQueueItemId:(NSObject*)queueItemId; +/// Raised when the state of the player has changed +-(void) audioPlayer:(STKAudioPlayer*)audioPlayer stateChanged:(STKAudioPlayerState)state previousState:(STKAudioPlayerState)previousState; +/// Raised when an item has finished playing +-(void) audioPlayer:(STKAudioPlayer*)audioPlayer didFinishPlayingQueueItemId:(NSObject*)queueItemId withReason:(STKAudioPlayerStopReason)stopReason andProgress:(double)progress andDuration:(double)duration; +/// Raised when an unexpected and possibly unrecoverable error has occured (usually best to recreate the STKAudioPlauyer) +-(void) audioPlayer:(STKAudioPlayer*)audioPlayer unexpectedError:(STKAudioPlayerErrorCode)errorCode; +@optional +/// Optionally implemented to get logging information from the STKAudioPlayer (used internally for debugging) +-(void) audioPlayer:(STKAudioPlayer*)audioPlayer logInfo:(NSString*)line; +/// Raised when items queued items are cleared (usually because of a call to play, setDataSource or stop) +-(void) audioPlayer:(STKAudioPlayer*)audioPlayer didCancelQueuedItems:(NSArray*)queuedItems; + +@end + +@interface STKAudioPlayer : NSObject + +/// Gets or sets the volume (ranges 0 - 1.0). +/// On iOS the STKAudioPlayerOptionEnableMultichannelMixer option must be enabled for volume to work. +@property (readwrite) Float32 volume; +/// Gets or sets the player muted state +@property (readwrite) BOOL muted; +/// Gets the current item duration in seconds +@property (readonly) double duration; +/// Gets the current item progress in seconds +@property (readonly) double progress; +/// Enables or disables peak and average decibel meteting +@property (readwrite) BOOL meteringEnabled; +/// Enables or disables the EQ +@property (readwrite) BOOL equalizerEnabled; +/// Returns an array of STKFrameFilterEntry objects representing the filters currently in use +@property (readonly) NSArray* frameFilters; +/// Returns the items pending to be played (includes buffering and upcoming items but does not include the current item) +@property (readonly) NSArray* pendingQueue; +/// The number of items pending to be played (includes buffering and upcoming items but does not include the current item) +@property (readonly) NSUInteger pendingQueueCount; +/// Gets the most recently queued item that is still pending to play +@property (readonly) NSObject* mostRecentlyQueuedStillPendingItem; +/// Gets the current state of the player +@property (readwrite) STKAudioPlayerState state; +/// Gets the options provided to the player on startup +@property (readonly) STKAudioPlayerOptions options; +/// Gets the reason why the player is stopped (if any) +@property (readonly) STKAudioPlayerStopReason stopReason; +/// Gets and sets the delegate used for receiving events from the STKAudioPlayer +@property (readwrite, unsafe_unretained) id delegate; + +/// Creates a datasource from a given URL. +/// URLs with FILE schemes will return an STKLocalFileDataSource. +/// URLs with HTTP schemes will return an STKHTTPDataSource wrapped within an STKAutoRecoveringHTTPDataSource. +/// URLs with unrecognised schemes will return nil. ++(STKDataSource*) dataSourceFromURL:(NSURL*)url; + +/// Initializes a new STKAudioPlayer with the default options +-(id) init; + +/// Initializes a new STKAudioPlayer with the given options +-(id) initWithOptions:(STKAudioPlayerOptions)optionsIn; + +/// Plays an item from the given URL string (all pending queued items are removed). +/// The NSString is used as the queue item ID +-(void) play:(NSString*)urlString; + +/// Plays an item from the given URL (all pending queued items are removed) +-(void) play:(NSString*)urlString withQueueItemID:(NSObject*)queueItemId; + +/// Plays an item from the given URL (all pending queued items are removed) +/// The NSURL is used as the queue item ID +-(void) playURL:(NSURL*)url; + +/// Plays an item from the given URL (all pending queued items are removed) +-(void) playURL:(NSURL*)url withQueueItemID:(NSObject*)queueItemId; + +/// Plays the given item (all pending queued items are removed) +/// The STKDataSource is used as the queue item ID +-(void) playDataSource:(STKDataSource*)dataSource; + +/// Plays the given item (all pending queued items are removed) +-(void) playDataSource:(STKDataSource*)dataSource withQueueItemID:(NSObject*)queueItemId; + +/// Queues the URL string for playback and uses the NSString as the queueItemID +-(void) queue:(NSString*)urlString; + +/// Queues the URL string for playback with the given queueItemID +-(void) queue:(NSString*)urlString withQueueItemId:(NSObject*)queueItemId; + +/// Queues the URL for playback and uses the NSURL as the queueItemID +-(void) queueURL:(NSURL*)url; + +/// Queues the URL for playback with the given queueItemID +-(void) queueURL:(NSURL*)url withQueueItemId:(NSObject*)queueItemId; + +/// Queues a DataSource with the given queueItemId +-(void) queueDataSource:(STKDataSource*)dataSource withQueueItemId:(NSObject*)queueItemId; + +/// Plays the given item (all pending queued items are removed) +-(void) setDataSource:(STKDataSource*)dataSourceIn withQueueItemId:(NSObject*)queueItemId; + +/// Seeks to a specific time (in seconds) +-(void) seekToTime:(double)value; + +/// Clears any upcoming items already queued for playback (does not stop the current item). +/// The didCancelItems event will be raised for the items removed from the queue. +-(void) clearQueue; + +/// Pauses playback +-(void) pause; + +/// Resumes playback from pause +-(void) resume; + +/// Stops playback of the current file, flushes all the buffers and removes any pending queued items +-(void) stop; + +/// Mutes playback +-(void) mute; + +/// Unmutes playback +-(void) unmute; + +/// Disposes the STKAudioPlayer and frees up all resources before returning +-(void) dispose; + +/// The QueueItemId of the currently playing item +-(NSObject*) currentlyPlayingQueueItemId; + +/// Removes a frame filter with the given name +-(void) removeFrameFilterWithName:(NSString*)name; + +/// Appends a frame filter with the given name and filter block to the end of the filter chain +-(void) appendFrameFilterWithName:(NSString*)name block:(STKFrameFilter)block; + +/// Appends a frame filter with the given name and filter block just after the filter with the given name. +/// If the given name is nil, the filter will be inserted at the beginning of the filter change +-(void) addFrameFilterWithName:(NSString*)name afterFilterWithName:(NSString*)afterFilterWithName block:(STKFrameFilter)block; + +/// Reads the peak power in decibals for the given channel (0 or 1). +/// Return values are between -60 (low) and 0 (high). +-(float) peakPowerInDecibelsForChannel:(NSUInteger)channelNumber; + +/// Reads the average power in decibals for the given channel (0 or 1) +/// Return values are between -60 (low) and 0 (high). +-(float) averagePowerInDecibelsForChannel:(NSUInteger)channelNumber; + +/// Sets the gain value (from -96 low to +24 high) for an equalizer band (0 based index) +-(void) setGain:(float)gain forEqualizerBand:(int)bandIndex; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/STKAudioPlayer.m b/submodules/LegacyComponents/LegacyComponents/STKAudioPlayer.m new file mode 100755 index 0000000000..a00a883d42 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/STKAudioPlayer.m @@ -0,0 +1,3157 @@ +/********************************************************************************** + AudioPlayer.m + + Created by Thong Nguyen on 14/05/2012. + https://github.com/tumtumtum/StreamingKit + + Copyright (c) 2014 Thong Nguyen (tumtumtum@gmail.com). All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + 3. All advertising materials mentioning features or use of this software + must display the following acknowledgement: + This product includes software developed by Thong Nguyen (tumtumtum@gmail.com) + 4. Neither the name of Thong Nguyen nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY Thong Nguyen''AS IS'' AND ANY + EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **********************************************************************************/ + +#import "STKAudioPlayer.h" +#import "AudioToolbox/AudioToolbox.h" +#import "STKHTTPDataSource.h" +#import "STKAutoRecoveringHTTPDataSource.h" +#import "STKLocalFileDataSource.h" +#import "STKQueueEntry.h" +#import "NSMutableArray+STKAudioPlayer.h" +#import "libkern/OSAtomic.h" +#import + +#ifndef DBL_MAX +#define DBL_MAX 1.7976931348623157e+308 +#endif + +#pragma mark Defines + +#define kOutputBus 0 +#define kInputBus 1 + +#define STK_DBMIN (-60) +#define STK_DBOFFSET (-74.0) +#define STK_LOWPASSFILTERTIMESLICE (0.0005) + +#define STK_DEFAULT_PCM_BUFFER_SIZE_IN_SECONDS (10) +#define STK_DEFAULT_SECONDS_REQUIRED_TO_START_PLAYING (1) +#define STK_DEFAULT_SECONDS_REQUIRED_TO_START_PLAYING_AFTER_BUFFER_UNDERRUN (7.5) +#define STK_MAX_COMPRESSED_PACKETS_FOR_BITRATE_CALCULATION (4096) +#define STK_DEFAULT_READ_BUFFER_SIZE (64 * 1024) +#define STK_DEFAULT_PACKET_BUFFER_SIZE (2048) +#define STK_DEFAULT_GRACE_PERIOD_AFTER_SEEK_SECONDS (0.5) + +#define LOGINFO(x) [self logInfo:[NSString stringWithFormat:@"%s %@", sel_getName(_cmd), x]]; + +static void PopulateOptionsWithDefault(STKAudioPlayerOptions* options) +{ + if (options->bufferSizeInSeconds == 0) + { + options->bufferSizeInSeconds = STK_DEFAULT_PCM_BUFFER_SIZE_IN_SECONDS; + } + + if (options->readBufferSize == 0) + { + options->readBufferSize = STK_DEFAULT_READ_BUFFER_SIZE; + } + + if (options->secondsRequiredToStartPlaying == 0) + { + options->secondsRequiredToStartPlaying = MIN(STK_DEFAULT_SECONDS_REQUIRED_TO_START_PLAYING, options->bufferSizeInSeconds); + } + + if (options->secondsRequiredToStartPlayingAfterBufferUnderun == 0) + { + options->secondsRequiredToStartPlayingAfterBufferUnderun = MIN(STK_DEFAULT_SECONDS_REQUIRED_TO_START_PLAYING_AFTER_BUFFER_UNDERRUN, options->bufferSizeInSeconds); + } + + if (options->gracePeriodAfterSeekInSeconds == 0) + { + options->gracePeriodAfterSeekInSeconds = MIN(STK_DEFAULT_GRACE_PERIOD_AFTER_SEEK_SECONDS, options->bufferSizeInSeconds); + } +} + +#define CHECK_STATUS_AND_REPORT(call) \ + if ((status = (call))) \ + { \ + [self unexpectedError:STKAudioPlayerErrorAudioSystemError]; \ + } + +#define CHECK_STATUS_AND_RETURN(call) \ + if ((status = (call))) \ + { \ + [self unexpectedError:STKAudioPlayerErrorAudioSystemError]; \ + return;\ + } + +#define CHECK_STATUS_AND_RETURN_VALUE(call, value) \ + if ((status = (call))) \ + { \ + [self unexpectedError:STKAudioPlayerErrorAudioSystemError]; \ + return value;\ + } + +typedef enum +{ + STKAudioPlayerInternalStateInitialised = 0, + STKAudioPlayerInternalStateRunning = 1, + STKAudioPlayerInternalStatePlaying = (1 << 1) | STKAudioPlayerInternalStateRunning, + STKAudioPlayerInternalStateRebuffering = (1 << 2) | STKAudioPlayerInternalStateRunning, + STKAudioPlayerInternalStateStartingThread = (1 << 3) | STKAudioPlayerInternalStateRunning, + STKAudioPlayerInternalStateWaitingForData = (1 << 4) | STKAudioPlayerInternalStateRunning, + /* Same as STKAudioPlayerInternalStateWaitingForData but isn't immediately raised as a buffering event */ + STKAudioPlayerInternalStateWaitingForDataAfterSeek = (1 << 5) | STKAudioPlayerInternalStateRunning, + STKAudioPlayerInternalStatePaused = (1 << 6) | STKAudioPlayerInternalStateRunning, + STKAudioPlayerInternalStateStopped = (1 << 9), + STKAudioPlayerInternalStatePendingNext = (1 << 10), + STKAudioPlayerInternalStateDisposed = (1 << 30), + STKAudioPlayerInternalStateError = (1 << 31) +} +STKAudioPlayerInternalState; + +#pragma mark STKFrameFilterEntry + +@interface STKFrameFilterEntry() +{ +@public + NSString* name; + STKFrameFilter filter; +} +@end + +@implementation STKFrameFilterEntry +-(id) initWithFilter:(STKFrameFilter)filterIn andName:(NSString*)nameIn +{ + if (self = [super init]) + { + self->filter = [filterIn copy]; + self->name = nameIn; + } + + return self; +} + +-(NSString*) name +{ + return self->name; +} + +-(STKFrameFilter) filter +{ + return self->filter; +} +@end + +#pragma mark STKAudioPlayer + +static UInt32 maxFramesPerSlice = 4096; + +static AudioComponentDescription mixerDescription; +static AudioComponentDescription nbandUnitDescription; +static AudioComponentDescription outputUnitDescription; +static AudioComponentDescription convertUnitDescription; +static AudioStreamBasicDescription canonicalAudioStreamBasicDescription; + +@interface STKAudioPlayer() +{ + BOOL muted; + + UInt8* readBuffer; + int readBufferSize; + STKAudioPlayerInternalState internalState; + + Float32 volume; + Float32 peakPowerDb[2]; + Float32 averagePowerDb[2]; + + BOOL meteringEnabled; + BOOL equalizerOn; + BOOL equalizerEnabled; + STKAudioPlayerOptions options; + + NSMutableArray* converterNodes; + + AUGraph audioGraph; + AUNode eqNode; + AUNode mixerNode; + AUNode outputNode; + + AUNode eqInputNode; + AUNode eqOutputNode; + AUNode mixerInputNode; + AUNode mixerOutputNode; + + AudioComponentInstance eqUnit; + AudioComponentInstance mixerUnit; + AudioComponentInstance outputUnit; + + UInt32 eqBandCount; + int32_t waitingForDataAfterSeekFrameCount; + + UInt32 framesRequiredToStartPlaying; + UInt32 framesRequiredToPlayAfterRebuffering; + UInt32 framesRequiredBeforeWaitingForDataAfterSeekBecomesPlaying; + + STKQueueEntry* volatile currentlyPlayingEntry; + STKQueueEntry* volatile currentlyReadingEntry; + + NSMutableArray* upcomingQueue; + NSMutableArray* bufferingQueue; + + OSSpinLock pcmBufferSpinLock; + OSSpinLock internalStateLock; + volatile UInt32 pcmBufferTotalFrameCount; + volatile UInt32 pcmBufferFrameStartIndex; + volatile UInt32 pcmBufferUsedFrameCount; + volatile UInt32 pcmBufferFrameSizeInBytes; + + AudioBuffer* pcmAudioBuffer; + AudioBufferList pcmAudioBufferList; + AudioConverterRef audioConverterRef; + + AudioStreamBasicDescription audioConverterAudioStreamBasicDescription; + + BOOL deallocating; + BOOL discontinuous; + NSArray* frameFilters; + NSThread* playbackThread; + NSRunLoop* playbackThreadRunLoop; + AudioFileStreamID audioFileStream; + NSConditionLock* threadStartedLock; + NSConditionLock* threadFinishedCondLock; + + void(^stopBackBackgroundTaskBlock)(); + + int32_t seekVersion; + OSSpinLock seekLock; + OSSpinLock currentEntryReferencesLock; + + pthread_mutex_t playerMutex; + pthread_cond_t playerThreadReadyCondition; + pthread_mutex_t mainThreadSyncCallMutex; + pthread_cond_t mainThreadSyncCallReadyCondition; + + volatile BOOL waiting; + volatile double requestedSeekTime; + volatile BOOL disposeWasRequested; + volatile BOOL seekToTimeWasRequested; + volatile STKAudioPlayerStopReason stopReason; +} + +@property (readwrite) STKAudioPlayerInternalState internalState; +@property (readwrite) STKAudioPlayerInternalState stateBeforePaused; + +-(void) handlePropertyChangeForFileStream:(AudioFileStreamID)audioFileStreamIn fileStreamPropertyID:(AudioFileStreamPropertyID)propertyID ioFlags:(UInt32*)ioFlags; +-(void) handleAudioPackets:(const void*)inputData numberBytes:(UInt32)numberBytes numberPackets:(UInt32)numberPackets packetDescriptions:(AudioStreamPacketDescription*)packetDescriptions; +@end + +static void AudioFileStreamPropertyListenerProc(void* clientData, AudioFileStreamID audioFileStream, AudioFileStreamPropertyID propertyId, UInt32* flags) +{ + STKAudioPlayer* player = (__bridge STKAudioPlayer*)clientData; + + [player handlePropertyChangeForFileStream:audioFileStream fileStreamPropertyID:propertyId ioFlags:flags]; +} + +static void AudioFileStreamPacketsProc(void* clientData, UInt32 numberBytes, UInt32 numberPackets, const void* inputData, AudioStreamPacketDescription* packetDescriptions) +{ + STKAudioPlayer* player = (__bridge STKAudioPlayer*)clientData; + + [player handleAudioPackets:inputData numberBytes:numberBytes numberPackets:numberPackets packetDescriptions:packetDescriptions]; +} + +@implementation STKAudioPlayer + ++(void) initialize +{ + convertUnitDescription = (AudioComponentDescription) + { + .componentManufacturer = kAudioUnitManufacturer_Apple, + .componentType = kAudioUnitType_FormatConverter, + .componentSubType = kAudioUnitSubType_AUConverter, + .componentFlags = 0, + .componentFlagsMask = 0 + }; + + const int bytesPerSample = sizeof(AudioSampleType); + + canonicalAudioStreamBasicDescription = (AudioStreamBasicDescription) + { + .mSampleRate = 44100.00, + .mFormatID = kAudioFormatLinearPCM, + .mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsPacked, + .mFramesPerPacket = 1, + .mChannelsPerFrame = 2, + .mBytesPerFrame = bytesPerSample * 2 /*channelsPerFrame*/, + .mBitsPerChannel = 8 * bytesPerSample, + .mBytesPerPacket = (bytesPerSample * 2) + }; + + outputUnitDescription = (AudioComponentDescription) + { + .componentType = kAudioUnitType_Output, +#if TARGET_OS_IPHONE + .componentSubType = kAudioUnitSubType_RemoteIO, +#else + .componentSubType = kAudioUnitSubType_DefaultOutput, +#endif + .componentFlags = 0, + .componentFlagsMask = 0, + .componentManufacturer = kAudioUnitManufacturer_Apple + }; + + mixerDescription = (AudioComponentDescription) + { + .componentType = kAudioUnitType_Mixer, + .componentSubType = kAudioUnitSubType_MultiChannelMixer, + .componentFlags = 0, + .componentFlagsMask = 0, + .componentManufacturer = kAudioUnitManufacturer_Apple + }; + + nbandUnitDescription = (AudioComponentDescription) + { + .componentType = kAudioUnitType_Effect, + .componentSubType = kAudioUnitSubType_NBandEQ, + .componentManufacturer=kAudioUnitManufacturer_Apple + }; +} +-(STKAudioPlayerOptions) options +{ + return options; +} + +-(STKAudioPlayerInternalState) internalState +{ + return internalState; +} + +-(void) setInternalState:(STKAudioPlayerInternalState)value +{ + [self setInternalState:value ifInState:NULL]; +} + +-(void) setInternalState:(STKAudioPlayerInternalState)value ifInState:(BOOL(^)(STKAudioPlayerInternalState))ifInState +{ + STKAudioPlayerState newState; + + switch (value) + { + case STKAudioPlayerInternalStateInitialised: + newState = STKAudioPlayerStateReady; + stopReason = STKAudioPlayerStopReasonNone; + break; + case STKAudioPlayerInternalStateRunning: + case STKAudioPlayerInternalStateStartingThread: + case STKAudioPlayerInternalStatePlaying: + case STKAudioPlayerInternalStateWaitingForDataAfterSeek: + newState = STKAudioPlayerStatePlaying; + stopReason = STKAudioPlayerStopReasonNone; + break; + case STKAudioPlayerInternalStatePendingNext: + case STKAudioPlayerInternalStateRebuffering: + case STKAudioPlayerInternalStateWaitingForData: + newState = STKAudioPlayerStateBuffering; + stopReason = STKAudioPlayerStopReasonNone; + break; + case STKAudioPlayerInternalStateStopped: + newState = STKAudioPlayerStateStopped; + break; + case STKAudioPlayerInternalStatePaused: + newState = STKAudioPlayerStatePaused; + stopReason = STKAudioPlayerStopReasonNone; + break; + case STKAudioPlayerInternalStateDisposed: + newState = STKAudioPlayerStateDisposed; + stopReason = STKAudioPlayerStopReasonUserAction; + break; + case STKAudioPlayerInternalStateError: + newState = STKAudioPlayerStateError; + stopReason = STKAudioPlayerStopReasonError; + break; + } + + OSSpinLockLock(&internalStateLock); + + waitingForDataAfterSeekFrameCount = 0; + + if (value == internalState) + { + OSSpinLockUnlock(&internalStateLock); + + return; + } + + if (ifInState != NULL) + { + if (!ifInState(self->internalState)) + { + OSSpinLockUnlock(&internalStateLock); + + return; + } + } + + internalState = value; + + STKAudioPlayerState previousState = self.state; + + if (newState != previousState && !deallocating) + { + self.state = newState; + + OSSpinLockUnlock(&internalStateLock); + + dispatch_async(dispatch_get_main_queue(), ^ + { + [self.delegate audioPlayer:self stateChanged:self.state previousState:previousState]; + }); + } + else + { + OSSpinLockUnlock(&internalStateLock); + } +} + +-(STKAudioPlayerStopReason) stopReason +{ + return stopReason; +} + +-(void) logInfo:(NSString*)line +{ + if ([NSThread currentThread].isMainThread) + { + if ([self.delegate respondsToSelector:@selector(audioPlayer:logInfo:)]) + { + [self.delegate audioPlayer:self logInfo:line]; + } + } + else + { + if ([self.delegate respondsToSelector:@selector(audioPlayer:logInfo:)]) + { + [self.delegate audioPlayer:self logInfo:line]; + } + } +} + +-(id) init +{ + return [self initWithOptions:(STKAudioPlayerOptions){}]; +} + +-(id) initWithOptions:(STKAudioPlayerOptions)optionsIn +{ + if (self = [super init]) + { + options = optionsIn; + + self->volume = 1.0; + self->equalizerEnabled = optionsIn.equalizerBandFrequencies[0] != 0; + + PopulateOptionsWithDefault(&options); + + framesRequiredToStartPlaying = canonicalAudioStreamBasicDescription.mSampleRate * options.secondsRequiredToStartPlaying; + framesRequiredToPlayAfterRebuffering = canonicalAudioStreamBasicDescription.mSampleRate * options.secondsRequiredToStartPlayingAfterBufferUnderun; + framesRequiredBeforeWaitingForDataAfterSeekBecomesPlaying = canonicalAudioStreamBasicDescription.mSampleRate * options.gracePeriodAfterSeekInSeconds; + + pcmAudioBuffer = &pcmAudioBufferList.mBuffers[0]; + + pcmAudioBufferList.mNumberBuffers = 1; + pcmAudioBufferList.mBuffers[0].mDataByteSize = (canonicalAudioStreamBasicDescription.mSampleRate * options.bufferSizeInSeconds) * canonicalAudioStreamBasicDescription.mBytesPerFrame; + pcmAudioBufferList.mBuffers[0].mData = (void*)calloc(pcmAudioBuffer->mDataByteSize, 1); + pcmAudioBufferList.mBuffers[0].mNumberChannels = 2; + + pcmBufferFrameSizeInBytes = canonicalAudioStreamBasicDescription.mBytesPerFrame; + pcmBufferTotalFrameCount = pcmAudioBuffer->mDataByteSize / pcmBufferFrameSizeInBytes; + + readBufferSize = options.readBufferSize; + readBuffer = calloc(sizeof(UInt8), readBufferSize); + + pthread_mutexattr_t attr; + + pthread_mutexattr_init(&attr); + pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); + + pthread_mutex_init(&playerMutex, &attr); + pthread_mutex_init(&mainThreadSyncCallMutex, NULL); + pthread_cond_init(&playerThreadReadyCondition, NULL); + pthread_cond_init(&mainThreadSyncCallReadyCondition, NULL); + + threadStartedLock = [[NSConditionLock alloc] initWithCondition:0]; + threadFinishedCondLock = [[NSConditionLock alloc] initWithCondition:0]; + + self.internalState = STKAudioPlayerInternalStateInitialised; + + upcomingQueue = [[NSMutableArray alloc] init]; + bufferingQueue = [[NSMutableArray alloc] init]; + + [self resetPcmBuffers]; + [self createAudioGraph]; + [self createPlaybackThread]; + } + + return self; +} + +-(void) destroyAudioResources +{ + if (currentlyReadingEntry) + { + currentlyReadingEntry.dataSource.delegate = nil; + [currentlyReadingEntry.dataSource unregisterForEvents]; + + currentlyReadingEntry = nil; + } + + if (currentlyPlayingEntry) + { + currentlyPlayingEntry.dataSource.delegate = nil; + [currentlyReadingEntry.dataSource unregisterForEvents]; + + OSSpinLockLock(¤tEntryReferencesLock); + + currentlyPlayingEntry = nil; + + OSSpinLockUnlock(¤tEntryReferencesLock); + } + + [self stopAudioUnitWithReason:STKAudioPlayerStopReasonDisposed]; + + [self clearQueue]; + + if (audioFileStream) + { + AudioFileStreamClose(audioFileStream); + audioFileStream = nil; + } + + if (audioConverterRef) + { + AudioConverterDispose(audioConverterRef); + audioConverterRef = nil; + } + + if (audioGraph) + { + AUGraphUninitialize(audioGraph); + AUGraphClose(audioGraph); + DisposeAUGraph(audioGraph); + + audioGraph = nil; + } +} + +-(void) dealloc +{ + NSLog(@"STKAudioPlayer dealloc"); + + deallocating = YES; + + [self destroyAudioResources]; + + pthread_mutex_destroy(&playerMutex); + pthread_mutex_destroy(&mainThreadSyncCallMutex); + pthread_cond_destroy(&playerThreadReadyCondition); + pthread_cond_destroy(&mainThreadSyncCallReadyCondition); + + free(readBuffer); +} + +-(void) startSystemBackgroundTask +{ +/*#if TARGET_OS_IPHONE + __block UIBackgroundTaskIdentifier backgroundTaskId = UIBackgroundTaskInvalid; + + backgroundTaskId = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^ + { + [[UIApplication sharedApplication] endBackgroundTask:backgroundTaskId]; + backgroundTaskId = UIBackgroundTaskInvalid; + }]; + + stopBackBackgroundTaskBlock = [^ + { + if (backgroundTaskId != UIBackgroundTaskInvalid) + { + [[UIApplication sharedApplication] endBackgroundTask:backgroundTaskId]; + backgroundTaskId = UIBackgroundTaskInvalid; + } + } copy]; +#endif*/ +} + +-(void) stopSystemBackgroundTask +{ +#if TARGET_OS_IPHONE + if (stopBackBackgroundTaskBlock != NULL) + { + stopBackBackgroundTaskBlock(); + stopBackBackgroundTaskBlock = NULL; + } +#endif +} + ++(STKDataSource*) dataSourceFromURL:(NSURL*)url +{ + STKDataSource* retval = nil; + + if ([url.scheme isEqualToString:@"file"]) + { + retval = [[STKLocalFileDataSource alloc] initWithFilePath:url.path]; + } + else if ([url.scheme caseInsensitiveCompare:@"http"] == NSOrderedSame || [url.scheme caseInsensitiveCompare:@"https"] == NSOrderedSame) + { + retval = [[STKAutoRecoveringHTTPDataSource alloc] initWithHTTPDataSource:[[STKHTTPDataSource alloc] initWithURL:url]]; + } + + return retval; +} + +-(void) clearQueue +{ + pthread_mutex_lock(&playerMutex); + { + if ([self.delegate respondsToSelector:@selector(audioPlayer:didCancelQueuedItems:)]) + { + NSMutableArray* array = [[NSMutableArray alloc] initWithCapacity:bufferingQueue.count + upcomingQueue.count]; + + for (STKQueueEntry* entry in upcomingQueue) + { + [array addObject:entry.queueItemId]; + } + + for (STKQueueEntry* entry in bufferingQueue) + { + [array addObject:entry.queueItemId]; + } + + [upcomingQueue removeAllObjects]; + [bufferingQueue removeAllObjects]; + + if (array.count > 0) + { + [self playbackThreadQueueMainThreadSyncBlock:^ + { + if ([self.delegate respondsToSelector:@selector(audioPlayer:didCancelQueuedItems:)]) + { + [self.delegate audioPlayer:self didCancelQueuedItems:array]; + } + }]; + } + } + else + { + [bufferingQueue removeAllObjects]; + [upcomingQueue removeAllObjects]; + } + } + pthread_mutex_unlock(&playerMutex); +} + +-(void) play:(NSString*)urlString +{ + [self play:urlString withQueueItemID:urlString]; +} + +-(void) play:(NSString*)urlString withQueueItemID:(NSObject*)queueItemId +{ + NSURL* url = [NSURL URLWithString:urlString]; + + [self setDataSource:[STKAudioPlayer dataSourceFromURL:url] withQueueItemId:queueItemId]; +} + +-(void) playURL:(NSURL*)url +{ + [self playURL:url withQueueItemID:url]; +} + +-(void) playURL:(NSURL*)url withQueueItemID:(NSObject*)queueItemId +{ + [self setDataSource:[STKAudioPlayer dataSourceFromURL:url] withQueueItemId:queueItemId]; +} + +-(void) playDataSource:(STKDataSource*)dataSource +{ + [self playDataSource:dataSource withQueueItemID:dataSource]; +} + +-(void) playDataSource:(STKDataSource*)dataSource withQueueItemID:(NSObject *)queueItemId +{ + [self setDataSource:dataSource withQueueItemId:queueItemId]; +} + +-(void) setDataSource:(STKDataSource*)dataSourceIn withQueueItemId:(NSObject*)queueItemId +{ + pthread_mutex_lock(&playerMutex); + { + LOGINFO(([NSString stringWithFormat:@"Playing: %@", [queueItemId description]])); + + if (![self audioGraphIsRunning]) + { + [self startSystemBackgroundTask]; + } + + [self clearQueue]; + + [upcomingQueue enqueue:[[STKQueueEntry alloc] initWithDataSource:dataSourceIn andQueueItemId:queueItemId]]; + + self.internalState = STKAudioPlayerInternalStatePendingNext; + } + pthread_mutex_unlock(&playerMutex); + + [self wakeupPlaybackThread]; +} + +-(void) queue:(NSString*)urlString +{ + return [self queueURL:[NSURL URLWithString:urlString] withQueueItemId:urlString]; +} + +-(void) queue:(NSString*)urlString withQueueItemId:(NSObject*)queueItemId +{ + [self queueURL:[NSURL URLWithString:urlString] withQueueItemId:queueItemId]; +} + +-(void) queueURL:(NSURL*)url +{ + [self queueURL:url withQueueItemId:url]; +} + +-(void) queueURL:(NSURL*)url withQueueItemId:(NSObject*)queueItemId +{ + [self queueDataSource:[STKAudioPlayer dataSourceFromURL:url] withQueueItemId:queueItemId]; +} + +-(void) queueDataSource:(STKDataSource*)dataSourceIn withQueueItemId:(NSObject*)queueItemId +{ + pthread_mutex_lock(&playerMutex); + { + if (![self audioGraphIsRunning]) + { + [self startSystemBackgroundTask]; + } + + [upcomingQueue enqueue:[[STKQueueEntry alloc] initWithDataSource:dataSourceIn andQueueItemId:queueItemId]]; + } + pthread_mutex_unlock(&playerMutex); + + [self wakeupPlaybackThread]; +} + +-(void) handlePropertyChangeForFileStream:(AudioFileStreamID)inAudioFileStream fileStreamPropertyID:(AudioFileStreamPropertyID)inPropertyID ioFlags:(UInt32*)ioFlags +{ + OSStatus error; + + if (!currentlyReadingEntry) + { + return; + } + + switch (inPropertyID) + { + case kAudioFileStreamProperty_DataOffset: + { + SInt64 offset; + UInt32 offsetSize = sizeof(offset); + + AudioFileStreamGetProperty(audioFileStream, kAudioFileStreamProperty_DataOffset, &offsetSize, &offset); + + currentlyReadingEntry->parsedHeader = YES; + currentlyReadingEntry->audioDataOffset = offset; + + break; + } + case kAudioFileStreamProperty_FileFormat: + { + char fileFormat[4]; + UInt32 fileFormatSize = sizeof(fileFormat); + + AudioFileStreamGetProperty(inAudioFileStream, kAudioFileStreamProperty_FileFormat, &fileFormatSize, &fileFormat); + + break; + } + case kAudioFileStreamProperty_DataFormat: + { + AudioStreamBasicDescription newBasicDescription; + STKQueueEntry* entryToUpdate = currentlyReadingEntry; + + if (!currentlyReadingEntry->parsedHeader) + { + UInt32 size = sizeof(newBasicDescription); + + AudioFileStreamGetProperty(inAudioFileStream, kAudioFileStreamProperty_DataFormat, &size, &newBasicDescription); + + pthread_mutex_lock(&playerMutex); + + entryToUpdate->audioStreamBasicDescription = newBasicDescription; + entryToUpdate->sampleRate = entryToUpdate->audioStreamBasicDescription.mSampleRate; + entryToUpdate->packetDuration = entryToUpdate->audioStreamBasicDescription.mFramesPerPacket / entryToUpdate->sampleRate; + + UInt32 packetBufferSize = 0; + UInt32 sizeOfPacketBufferSize = sizeof(packetBufferSize); + + error = AudioFileStreamGetProperty(audioFileStream, kAudioFileStreamProperty_PacketSizeUpperBound, &sizeOfPacketBufferSize, &packetBufferSize); + + if (error || packetBufferSize == 0) + { + error = AudioFileStreamGetProperty(audioFileStream, kAudioFileStreamProperty_MaximumPacketSize, &sizeOfPacketBufferSize, &packetBufferSize); + + if (error || packetBufferSize == 0) + { + entryToUpdate->packetBufferSize = STK_DEFAULT_PACKET_BUFFER_SIZE; + } + else + { + entryToUpdate->packetBufferSize = packetBufferSize; + } + } + else + { + entryToUpdate->packetBufferSize = packetBufferSize; + } + + [self createAudioConverter:¤tlyReadingEntry->audioStreamBasicDescription]; + + pthread_mutex_unlock(&playerMutex); + } + + break; + } + case kAudioFileStreamProperty_AudioDataByteCount: + { + UInt64 audioDataByteCount; + UInt32 byteCountSize = sizeof(audioDataByteCount); + + AudioFileStreamGetProperty(inAudioFileStream, kAudioFileStreamProperty_AudioDataByteCount, &byteCountSize, &audioDataByteCount); + + currentlyReadingEntry->audioDataByteCount = audioDataByteCount; + + break; + } + case kAudioFileStreamProperty_ReadyToProducePackets: + { + if (!audioConverterAudioStreamBasicDescription.mFormatID == kAudioFormatLinearPCM) + { + discontinuous = YES; + } + + break; + } + case kAudioFileStreamProperty_FormatList: + { + Boolean outWriteable; + UInt32 formatListSize; + OSStatus err = AudioFileStreamGetPropertyInfo(inAudioFileStream, kAudioFileStreamProperty_FormatList, &formatListSize, &outWriteable); + + if (err) + { + break; + } + + AudioFormatListItem* formatList = malloc(formatListSize); + + err = AudioFileStreamGetProperty(inAudioFileStream, kAudioFileStreamProperty_FormatList, &formatListSize, formatList); + + if (err) + { + free(formatList); + break; + } + + for (int i = 0; i * sizeof(AudioFormatListItem) < formatListSize; i += sizeof(AudioFormatListItem)) + { + AudioStreamBasicDescription pasbd = formatList[i].mASBD; + + if (pasbd.mFormatID == kAudioFormatMPEG4AAC_HE || pasbd.mFormatID == kAudioFormatMPEG4AAC_HE_V2) + { + // + // We've found HE-AAC, remember this to tell the audio queue + // when we construct it. + // +#if !TARGET_IPHONE_SIMULATOR + currentlyReadingEntry->audioStreamBasicDescription = pasbd; +#endif + break; + } + } + + free(formatList); + + break; + } + + } +} + +-(Float64) currentTimeInFrames +{ + if (audioGraph == nil) + { + return 0; + } + + return 0; +} + +-(void) unexpectedError:(STKAudioPlayerErrorCode)errorCodeIn +{ + self.internalState = STKAudioPlayerInternalStateError; + + [self playbackThreadQueueMainThreadSyncBlock:^ + { + [self.delegate audioPlayer:self unexpectedError:errorCodeIn]; + }]; +} + +-(double) duration +{ + if (self.internalState == STKAudioPlayerInternalStatePendingNext) + { + return 0; + } + + OSSpinLockLock(¤tEntryReferencesLock); + STKQueueEntry* entry = currentlyPlayingEntry; + OSSpinLockUnlock(¤tEntryReferencesLock); + + if (entry == nil) + { + return 0; + } + + double retval = [entry duration]; + double progress = [self progress]; + + if (retval < progress && retval > 0) + { + return progress; + } + + return retval; +} + +-(double) progress +{ + if (seekToTimeWasRequested) + { + return requestedSeekTime; + } + + if (self.internalState == STKAudioPlayerInternalStatePendingNext) + { + return 0; + } + + OSSpinLockLock(¤tEntryReferencesLock); + STKQueueEntry* entry = currentlyPlayingEntry; + OSSpinLockUnlock(¤tEntryReferencesLock); + + if (entry == nil) + { + return 0; + } + + OSSpinLockLock(&entry->spinLock); + double retval = entry->seekTime + (entry->framesPlayed / canonicalAudioStreamBasicDescription.mSampleRate); + OSSpinLockUnlock(&entry->spinLock); + + return retval; +} + +-(BOOL) invokeOnPlaybackThread:(void(^)())block +{ + NSRunLoop* runLoop = playbackThreadRunLoop; + + if (runLoop) + { + CFRunLoopPerformBlock([runLoop getCFRunLoop], NSRunLoopCommonModes, block); + CFRunLoopWakeUp([runLoop getCFRunLoop]); + + return YES; + } + + return NO; +} + +-(void) wakeupPlaybackThread +{ + [self invokeOnPlaybackThread:^ + { + [self processRunloop]; + }]; + + pthread_mutex_lock(&playerMutex); + + if (waiting) + { + pthread_cond_signal(&playerThreadReadyCondition); + } + + pthread_mutex_unlock(&playerMutex); +} + +-(void) seekToTime:(double)value +{ + if (currentlyPlayingEntry == nil) + { + return; + } + + OSSpinLockLock(&seekLock); + + BOOL seekAlreadyRequested = seekToTimeWasRequested; + + seekToTimeWasRequested = YES; + requestedSeekTime = value; + + if (!seekAlreadyRequested) + { + OSAtomicIncrement32(&seekVersion); + + OSSpinLockUnlock(&seekLock); + + [self wakeupPlaybackThread]; + + return; + } + + OSSpinLockUnlock(&seekLock); +} + +-(void) createPlaybackThread +{ + playbackThread = [[NSThread alloc] initWithTarget:self selector:@selector(startInternal) object:nil]; + + [playbackThread start]; + + [threadStartedLock lockWhenCondition:1]; + [threadStartedLock unlockWithCondition:0]; + + NSAssert(playbackThreadRunLoop != nil, @"playbackThreadRunLoop != nil"); +} + +-(void) audioQueueFinishedPlaying:(STKQueueEntry*)entry +{ + STKQueueEntry* next = [bufferingQueue dequeue]; + + [self processFinishPlayingIfAnyAndPlayingNext:entry withNext:next]; + [self processRunloop]; +} + +-(void) setCurrentlyReadingEntry:(STKQueueEntry*)entry andStartPlaying:(BOOL)startPlaying +{ + [self setCurrentlyReadingEntry:entry andStartPlaying:startPlaying clearQueue:YES]; +} + +-(void) setCurrentlyReadingEntry:(STKQueueEntry*)entry andStartPlaying:(BOOL)startPlaying clearQueue:(BOOL)clearQueue +{ + LOGINFO(([entry description])); + + if (startPlaying) + { + memset(&pcmAudioBuffer->mData[0], 0, pcmBufferTotalFrameCount * pcmBufferFrameSizeInBytes); + } + + if (audioFileStream) + { + AudioFileStreamClose(audioFileStream); + + audioFileStream = 0; + } + + if (currentlyReadingEntry) + { + currentlyReadingEntry.dataSource.delegate = nil; + [currentlyReadingEntry.dataSource unregisterForEvents]; + [currentlyReadingEntry.dataSource close]; + } + + OSSpinLockLock(¤tEntryReferencesLock); + currentlyReadingEntry = entry; + OSSpinLockUnlock(¤tEntryReferencesLock); + + currentlyReadingEntry.dataSource.delegate = self; + [currentlyReadingEntry.dataSource registerForEvents:[NSRunLoop currentRunLoop]]; + [currentlyReadingEntry.dataSource seekToOffset:0]; + + if (startPlaying) + { + if (clearQueue) + { + [self clearQueue]; + } + + [self processFinishPlayingIfAnyAndPlayingNext:currentlyPlayingEntry withNext:entry]; + + [self startAudioGraph]; + } + else + { + [bufferingQueue enqueue:entry]; + } +} + +-(void) processFinishPlayingIfAnyAndPlayingNext:(STKQueueEntry*)entry withNext:(STKQueueEntry*)next +{ + if (entry != currentlyPlayingEntry) + { + return; + } + + LOGINFO(([NSString stringWithFormat:@"Finished: %@, Next: %@, buffering.count=%d,upcoming.count=%d", entry ? [entry description] : @"nothing", [next description], (int)bufferingQueue.count, (int)upcomingQueue.count])); + + NSObject* queueItemId = entry.queueItemId; + double progress = [entry progressInFrames] / canonicalAudioStreamBasicDescription.mSampleRate; + double duration = [entry duration]; + + BOOL isPlayingSameItemProbablySeek = currentlyPlayingEntry == next; + + if (next) + { + if (!isPlayingSameItemProbablySeek) + { + OSSpinLockLock(&next->spinLock); + next->seekTime = 0; + OSSpinLockUnlock(&next->spinLock); + + OSSpinLockLock(&seekLock); + seekToTimeWasRequested = NO; + OSSpinLockUnlock(&seekLock); + } + + OSSpinLockLock(¤tEntryReferencesLock); + currentlyPlayingEntry = next; + NSObject* playingQueueItemId = playingQueueItemId = currentlyPlayingEntry.queueItemId; + OSSpinLockUnlock(¤tEntryReferencesLock); + + if (!isPlayingSameItemProbablySeek && entry) + { + [self playbackThreadQueueMainThreadSyncBlock:^ + { + [self.delegate audioPlayer:self didFinishPlayingQueueItemId:queueItemId withReason:stopReason andProgress:progress andDuration:duration]; + }]; + } + + if (!isPlayingSameItemProbablySeek) + { + [self setInternalState:STKAudioPlayerInternalStateWaitingForData]; + + [self playbackThreadQueueMainThreadSyncBlock:^ + { + [self.delegate audioPlayer:self didStartPlayingQueueItemId:playingQueueItemId]; + }]; + } + } + else + { + OSSpinLockLock(¤tEntryReferencesLock); + currentlyPlayingEntry = nil; + OSSpinLockUnlock(¤tEntryReferencesLock); + + if (!isPlayingSameItemProbablySeek && entry) + { + [self playbackThreadQueueMainThreadSyncBlock:^ + { + [self.delegate audioPlayer:self didFinishPlayingQueueItemId:queueItemId withReason:stopReason andProgress:progress andDuration:duration]; + }]; + } + } + + [self wakeupPlaybackThread]; +} + +-(void) dispatchSyncOnMainThread:(void(^)())block +{ + __block BOOL finished = NO; + + if (disposeWasRequested) + { + return; + } + + dispatch_async(dispatch_get_main_queue(), ^ + { + if (!disposeWasRequested) + { + block(); + } + + pthread_mutex_lock(&mainThreadSyncCallMutex); + finished = YES; + pthread_cond_signal(&mainThreadSyncCallReadyCondition); + pthread_mutex_unlock(&mainThreadSyncCallMutex); + }); + + pthread_mutex_lock(&mainThreadSyncCallMutex); + + while (true) + { + if (disposeWasRequested) + { + break; + } + + if (finished) + { + break; + } + + pthread_cond_wait(&mainThreadSyncCallReadyCondition, &mainThreadSyncCallMutex); + } + + pthread_mutex_unlock(&mainThreadSyncCallMutex); +} + +-(void) playbackThreadQueueMainThreadSyncBlock:(void(^)())block +{ + block = [block copy]; + + [self invokeOnPlaybackThread:^ + { + if (disposeWasRequested) + { + return; + } + + [self dispatchSyncOnMainThread:block]; + }]; +} + +-(void) requeueBufferingEntries +{ + if (bufferingQueue.count > 0) + { + for (STKQueueEntry* queueEntry in bufferingQueue) + { + queueEntry->parsedHeader = NO; + + [queueEntry reset]; + } + + [upcomingQueue skipQueueWithQueue:bufferingQueue]; + + [bufferingQueue removeAllObjects]; + } +} + +-(BOOL) processRunloop +{ + pthread_mutex_lock(&playerMutex); + { + if (disposeWasRequested) + { + pthread_mutex_unlock(&playerMutex); + + return NO; + } + else if (self.internalState == STKAudioPlayerInternalStatePaused) + { + pthread_mutex_unlock(&playerMutex); + + return YES; + } + else if (self.internalState == STKAudioPlayerInternalStatePendingNext) + { + STKQueueEntry* entry = [upcomingQueue dequeue]; + + self.internalState = STKAudioPlayerInternalStateWaitingForData; + + [self setCurrentlyReadingEntry:entry andStartPlaying:YES]; + [self resetPcmBuffers]; + } + else if (seekToTimeWasRequested && currentlyPlayingEntry && currentlyPlayingEntry != currentlyReadingEntry) + { + currentlyPlayingEntry->parsedHeader = NO; + [currentlyPlayingEntry reset]; + + if (currentlyReadingEntry != nil) + { + currentlyReadingEntry.dataSource.delegate = nil; + [currentlyReadingEntry.dataSource unregisterForEvents]; + } + + if (self->options.flushQueueOnSeek) + { + self.internalState = STKAudioPlayerInternalStateWaitingForDataAfterSeek; + [self setCurrentlyReadingEntry:currentlyPlayingEntry andStartPlaying:YES clearQueue:YES]; + } + else + { + [self requeueBufferingEntries]; + + self.internalState = STKAudioPlayerInternalStateWaitingForDataAfterSeek; + [self setCurrentlyReadingEntry:currentlyPlayingEntry andStartPlaying:YES clearQueue:NO]; + } + } + else if (currentlyReadingEntry == nil) + { + if (upcomingQueue.count > 0) + { + STKQueueEntry* entry = [upcomingQueue dequeue]; + + BOOL startPlaying = currentlyPlayingEntry == nil; + + self.internalState = STKAudioPlayerInternalStateWaitingForData; + [self setCurrentlyReadingEntry:entry andStartPlaying:startPlaying]; + } + else if (currentlyPlayingEntry == nil) + { + if (self.internalState != STKAudioPlayerInternalStateStopped) + { + [self stopAudioUnitWithReason:STKAudioPlayerStopReasonEof]; + } + } + } + + if (currentlyPlayingEntry && currentlyPlayingEntry->parsedHeader && [currentlyPlayingEntry calculatedBitRate] > 0.0) + { + int32_t originalSeekVersion; + BOOL originalSeekToTimeRequested; + + OSSpinLockLock(&seekLock); + originalSeekVersion = seekVersion; + originalSeekToTimeRequested = seekToTimeWasRequested; + OSSpinLockUnlock(&seekLock); + + if (originalSeekToTimeRequested && currentlyReadingEntry == currentlyPlayingEntry) + { + [self processSeekToTime]; + + OSSpinLockLock(&seekLock); + if (originalSeekVersion == seekVersion) + { + seekToTimeWasRequested = NO; + } + OSSpinLockUnlock(&seekLock); + } + } + else if (currentlyPlayingEntry == nil && seekToTimeWasRequested) + { + seekToTimeWasRequested = NO; + } + } + pthread_mutex_unlock(&playerMutex); + + return YES; +} + +-(void) startInternal +{ + @autoreleasepool + { + playbackThreadRunLoop = [NSRunLoop currentRunLoop]; + NSThread.currentThread.threadPriority = 1; + + [threadStartedLock lockWhenCondition:0]; + [threadStartedLock unlockWithCondition:1]; + + [playbackThreadRunLoop addPort:[NSPort port] forMode:NSDefaultRunLoopMode]; + + while (true) + { + @autoreleasepool + { + if (![self processRunloop]) + { + break; + } + } + + NSDate* date = [[NSDate alloc] initWithTimeIntervalSinceNow:10]; + [playbackThreadRunLoop runMode:NSDefaultRunLoopMode beforeDate:date]; + } + + disposeWasRequested = NO; + seekToTimeWasRequested = NO; + + [currentlyReadingEntry.dataSource unregisterForEvents]; + [currentlyPlayingEntry.dataSource unregisterForEvents]; + + currentlyReadingEntry.dataSource.delegate = nil; + currentlyPlayingEntry.dataSource.delegate = nil; + + pthread_mutex_lock(&playerMutex); + OSSpinLockLock(¤tEntryReferencesLock); + currentlyPlayingEntry = nil; + currentlyReadingEntry = nil; + OSSpinLockUnlock(¤tEntryReferencesLock); + pthread_mutex_unlock(&playerMutex); + + self.internalState = STKAudioPlayerInternalStateDisposed; + + playbackThreadRunLoop = nil; + + [self destroyAudioResources]; + + [threadFinishedCondLock lock]; + [threadFinishedCondLock unlockWithCondition:1]; + } +} + +-(void) processSeekToTime +{ + OSStatus error; + STKQueueEntry* currentEntry = currentlyReadingEntry; + + NSAssert(currentEntry == currentlyPlayingEntry, @"playing and reading must be the same"); + + if (!currentEntry || ([currentEntry calculatedBitRate] == 0.0 || currentlyPlayingEntry.dataSource.length <= 0)) + { + return; + } + + SInt64 seekByteOffset = currentEntry->audioDataOffset + (requestedSeekTime / self.duration) * (currentlyReadingEntry.audioDataLengthInBytes); + + if (seekByteOffset > currentEntry.dataSource.length - (2 * currentEntry->packetBufferSize)) + { + seekByteOffset = currentEntry.dataSource.length - 2 * currentEntry->packetBufferSize; + } + + OSSpinLockLock(¤tEntry->spinLock); + currentEntry->seekTime = requestedSeekTime; + OSSpinLockUnlock(¤tEntry->spinLock); + + double calculatedBitRate = [currentEntry calculatedBitRate]; + + if (currentEntry->packetDuration > 0 && calculatedBitRate > 0) + { + UInt32 ioFlags = 0; + SInt64 packetAlignedByteOffset; + SInt64 seekPacket = floor(requestedSeekTime / currentEntry->packetDuration); + + error = AudioFileStreamSeek(audioFileStream, seekPacket, &packetAlignedByteOffset, &ioFlags); + + if (!error && !(ioFlags & kAudioFileStreamSeekFlag_OffsetIsEstimated)) + { + double delta = ((seekByteOffset - (SInt64)currentEntry->audioDataOffset) - packetAlignedByteOffset) / calculatedBitRate * 8; + + OSSpinLockLock(¤tEntry->spinLock); + currentEntry->seekTime -= delta; + OSSpinLockUnlock(¤tEntry->spinLock); + + seekByteOffset = packetAlignedByteOffset + currentEntry->audioDataOffset; + } + } + + if (audioConverterRef) + { + AudioConverterReset(audioConverterRef); + } + + [currentEntry reset]; + [currentEntry.dataSource seekToOffset:seekByteOffset]; + + self->waitingForDataAfterSeekFrameCount = 0; + + self.internalState = STKAudioPlayerInternalStateWaitingForDataAfterSeek; + + if (audioGraph) + { + [self resetPcmBuffers]; + } +} + +-(void) dataSourceDataAvailable:(STKDataSource*)dataSourceIn +{ + OSStatus error; + + if (currentlyReadingEntry.dataSource != dataSourceIn) + { + return; + } + + if (!currentlyReadingEntry.dataSource.hasBytesAvailable) + { + return; + } + + int read = [currentlyReadingEntry.dataSource readIntoBuffer:readBuffer withSize:readBufferSize]; + + if (read == 0) + { + return; + } + + if (audioFileStream == 0) + { + error = AudioFileStreamOpen((__bridge void*)self, AudioFileStreamPropertyListenerProc, AudioFileStreamPacketsProc, dataSourceIn.audioFileTypeHint, &audioFileStream); + + if (error) + { + [self unexpectedError:STKAudioPlayerErrorAudioSystemError]; + + return; + } + } + + if (read < 0) + { + // iOS will shutdown network connections if the app is backgrounded (i.e. device is locked when player is paused) + // We try to reopen -- should probably add a back-off protocol in the future + + SInt64 position = currentlyReadingEntry.dataSource.position; + + [currentlyReadingEntry.dataSource seekToOffset:position]; + + return; + } + + int flags = 0; + + if (discontinuous) + { + flags = kAudioFileStreamParseFlag_Discontinuity; + } + + if (audioFileStream) + { + error = AudioFileStreamParseBytes(audioFileStream, read, readBuffer, flags); + + if (error) + { + if (dataSourceIn == currentlyPlayingEntry.dataSource) + { + [self unexpectedError:STKAudioPlayerErrorStreamParseBytesFailed]; + } + + return; + } + + OSSpinLockLock(¤tEntryReferencesLock); + + if (currentlyReadingEntry == nil) + { + [dataSourceIn unregisterForEvents]; + [dataSourceIn close]; + } + + OSSpinLockUnlock(¤tEntryReferencesLock); + } +} + +-(void) dataSourceErrorOccured:(STKDataSource*)dataSourceIn +{ + if (currentlyReadingEntry.dataSource != dataSourceIn) + { + return; + } + + [self unexpectedError:STKAudioPlayerErrorDataNotFound]; +} + +-(void) dataSourceEof:(STKDataSource*)dataSourceIn +{ + if (currentlyReadingEntry == nil || currentlyReadingEntry.dataSource != dataSourceIn) + { + dataSourceIn.delegate = nil; + [dataSourceIn unregisterForEvents]; + [dataSourceIn close]; + + return; + } + + if (disposeWasRequested) + { + return; + } + + NSObject* queueItemId = currentlyReadingEntry.queueItemId; + + [self dispatchSyncOnMainThread:^ + { + [self.delegate audioPlayer:self didFinishBufferingSourceWithQueueItemId:queueItemId]; + }]; + + pthread_mutex_lock(&playerMutex); + + if (currentlyReadingEntry == nil) + { + dataSourceIn.delegate = nil; + [dataSourceIn unregisterForEvents]; + [dataSourceIn close]; + + return; + } + + OSSpinLockLock(¤tlyReadingEntry->spinLock); + currentlyReadingEntry->lastFrameQueued = currentlyReadingEntry->framesQueued; + OSSpinLockUnlock(¤tlyReadingEntry->spinLock); + + currentlyReadingEntry.dataSource.delegate = nil; + [currentlyReadingEntry.dataSource unregisterForEvents]; + [currentlyReadingEntry.dataSource close]; + + OSSpinLockLock(¤tEntryReferencesLock); + currentlyReadingEntry = nil; + OSSpinLockUnlock(¤tEntryReferencesLock); + + pthread_mutex_unlock(&playerMutex); + + [self processRunloop]; +} + +-(void) pause +{ + pthread_mutex_lock(&playerMutex); + { + OSStatus error; + + if (self.internalState != STKAudioPlayerInternalStatePaused && (self.internalState & STKAudioPlayerInternalStateRunning)) + { + self.stateBeforePaused = self.internalState; + self.internalState = STKAudioPlayerInternalStatePaused; + + if (audioGraph) + { + error = AUGraphStop(audioGraph); + + if (error) + { + [self unexpectedError:STKAudioPlayerErrorAudioSystemError]; + + pthread_mutex_unlock(&playerMutex); + + return; + } + } + + [self wakeupPlaybackThread]; + } + } + pthread_mutex_unlock(&playerMutex); +} + +-(void) resume +{ + pthread_mutex_lock(&playerMutex); + { + OSStatus error; + + if (self.internalState == STKAudioPlayerInternalStatePaused) + { + self.internalState = self.stateBeforePaused; + + if (seekToTimeWasRequested) + { + [self resetPcmBuffers]; + } + + if (audioGraph != nil) + { + error = AUGraphStart(audioGraph); + + if (error) + { + [self unexpectedError:STKAudioPlayerErrorAudioSystemError]; + + pthread_mutex_unlock(&playerMutex); + + return; + } + } + + [self wakeupPlaybackThread]; + } + } + pthread_mutex_unlock(&playerMutex); +} + +-(void) resetPcmBuffers +{ + OSSpinLockLock(&pcmBufferSpinLock); + + self->pcmBufferFrameStartIndex = 0; + self->pcmBufferUsedFrameCount = 0; + self->peakPowerDb[0] = STK_DBMIN; + self->peakPowerDb[1] = STK_DBMIN; + self->averagePowerDb[0] = STK_DBMIN; + self->averagePowerDb[1] = STK_DBMIN; + + OSSpinLockUnlock(&pcmBufferSpinLock); +} + +-(void) stop +{ + pthread_mutex_lock(&playerMutex); + { + if (self.internalState == STKAudioPlayerInternalStateStopped) + { + pthread_mutex_unlock(&playerMutex); + + return; + } + + [self stopAudioUnitWithReason:STKAudioPlayerStopReasonUserAction]; + + [self resetPcmBuffers]; + + [self invokeOnPlaybackThread:^ + { + pthread_mutex_lock(&playerMutex); + { + currentlyReadingEntry.dataSource.delegate = nil; + [currentlyReadingEntry.dataSource unregisterForEvents]; + [currentlyReadingEntry.dataSource close]; + + if (currentlyPlayingEntry) + { + [self processFinishPlayingIfAnyAndPlayingNext:currentlyPlayingEntry withNext:nil]; + } + + [self clearQueue]; + + OSSpinLockLock(¤tEntryReferencesLock); + currentlyPlayingEntry = nil; + currentlyReadingEntry = nil; + seekToTimeWasRequested = NO; + OSSpinLockUnlock(¤tEntryReferencesLock); + } + pthread_mutex_unlock(&playerMutex); + }]; + + [self wakeupPlaybackThread]; + } + pthread_mutex_unlock(&playerMutex); +} + +-(void) stopThread +{ + BOOL wait = NO; + + NSRunLoop* runLoop = playbackThreadRunLoop; + + if (runLoop != nil) + { + wait = YES; + + [self invokeOnPlaybackThread:^ + { + disposeWasRequested = YES; + + CFRunLoopStop(CFRunLoopGetCurrent()); + }]; + + pthread_mutex_lock(&playerMutex); + disposeWasRequested = YES; + pthread_cond_signal(&playerThreadReadyCondition); + pthread_mutex_unlock(&playerMutex); + + pthread_mutex_lock(&mainThreadSyncCallMutex); + disposeWasRequested = YES; + pthread_cond_signal(&mainThreadSyncCallReadyCondition); + pthread_mutex_unlock(&mainThreadSyncCallMutex); + } + + if (wait) + { + [threadFinishedCondLock lockWhenCondition:1]; + [threadFinishedCondLock unlockWithCondition:0]; + } + + [self destroyAudioResources]; + + runLoop = nil; + playbackThread = nil; + playbackThreadRunLoop = nil; +} + +-(BOOL) muted +{ + return self->muted; +} + +-(void) setMuted:(BOOL)value +{ + self->muted = value; +} + +-(void) mute +{ + self.muted = YES; +} + +-(void) unmute +{ + self.muted = NO; +} + +-(void) dispose +{ + [self stop]; + [self stopThread]; +} + +-(NSObject*) currentlyPlayingQueueItemId +{ + OSSpinLockLock(¤tEntryReferencesLock); + + STKQueueEntry* entry = currentlyPlayingEntry; + + if (entry == nil) + { + OSSpinLockUnlock(¤tEntryReferencesLock); + + return nil; + } + + NSObject* retval = entry.queueItemId; + + OSSpinLockUnlock(¤tEntryReferencesLock); + + return retval; +} + +static BOOL GetHardwareCodecClassDesc(UInt32 formatId, AudioClassDescription* classDesc) +{ +#if TARGET_OS_IPHONE + UInt32 size; + + if (AudioFormatGetPropertyInfo(kAudioFormatProperty_Decoders, sizeof(formatId), &formatId, &size) != 0) + { + return NO; + } + + UInt32 decoderCount = size / sizeof(AudioClassDescription); + AudioClassDescription encoderDescriptions[decoderCount]; + + if (AudioFormatGetProperty(kAudioFormatProperty_Decoders, sizeof(formatId), &formatId, &size, encoderDescriptions) != 0) + { + return NO; + } + + for (UInt32 i = 0; i < decoderCount; ++i) + { + if (encoderDescriptions[i].mManufacturer == kAppleHardwareAudioCodecManufacturer) + { + *classDesc = encoderDescriptions[i]; + + return YES; + } + } +#endif + + return NO; +} + +-(void) destroyAudioConverter +{ + if (audioConverterRef) + { + AudioConverterDispose(audioConverterRef); + + audioConverterRef = nil; + } +} + +-(void) createAudioConverter:(AudioStreamBasicDescription*)asbd +{ + OSStatus status; + Boolean writable; + UInt32 cookieSize; + + if (memcmp(asbd, &audioConverterAudioStreamBasicDescription, sizeof(AudioStreamBasicDescription)) == 0) + { + AudioConverterReset(audioConverterRef); + + return; + } + + [self destroyAudioConverter]; + + AudioClassDescription classDesc; + + if (GetHardwareCodecClassDesc(asbd->mFormatID, &classDesc)) + { + AudioConverterNewSpecific(asbd, &canonicalAudioStreamBasicDescription, 1, &classDesc, &audioConverterRef); + } + + if (!audioConverterRef) + { + status = AudioConverterNew(asbd, &canonicalAudioStreamBasicDescription, &audioConverterRef); + + if (status) + { + [self unexpectedError:STKAudioPlayerErrorAudioSystemError]; + + return; + } + } + + audioConverterAudioStreamBasicDescription = *asbd; + + status = AudioFileStreamGetPropertyInfo(audioFileStream, kAudioFileStreamProperty_MagicCookieData, &cookieSize, &writable); + + if (!status) + { + void* cookieData = alloca(cookieSize); + + status = AudioFileStreamGetProperty(audioFileStream, kAudioFileStreamProperty_MagicCookieData, &cookieSize, cookieData); + + if (status) + { + return; + } + + status = AudioConverterSetProperty(audioConverterRef, kAudioConverterDecompressionMagicCookie, cookieSize, &cookieData); + + if (status) + { + return; + } + } +} + +-(void) createOutputUnit +{ + OSStatus status; + + CHECK_STATUS_AND_RETURN(AUGraphAddNode(audioGraph, &outputUnitDescription, &outputNode)); + CHECK_STATUS_AND_RETURN(AUGraphNodeInfo(audioGraph, outputNode, &outputUnitDescription, &outputUnit)); + +#if TARGET_OS_IPHONE + UInt32 flag = 1; + + CHECK_STATUS_AND_RETURN(AudioUnitSetProperty(outputUnit, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Output, kOutputBus, &flag, sizeof(flag))); + + flag = 0; + + CHECK_STATUS_AND_RETURN(AudioUnitSetProperty(outputUnit, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Input, kInputBus, &flag, sizeof(flag))); +#endif + + CHECK_STATUS_AND_RETURN(AudioUnitSetProperty(outputUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, kOutputBus, &canonicalAudioStreamBasicDescription, sizeof(canonicalAudioStreamBasicDescription))); +} + +-(void) createMixerUnit +{ + OSStatus status; + + if (!self.options.enableVolumeMixer) + { + return; + } + + CHECK_STATUS_AND_RETURN(AUGraphAddNode(audioGraph, &mixerDescription, &mixerNode)); + CHECK_STATUS_AND_RETURN(AUGraphNodeInfo(audioGraph, mixerNode, &mixerDescription, &mixerUnit)); + CHECK_STATUS_AND_RETURN(AudioUnitSetProperty(mixerUnit, kAudioUnitProperty_MaximumFramesPerSlice, kAudioUnitScope_Global, 0, &maxFramesPerSlice, sizeof(maxFramesPerSlice))); + + UInt32 busCount = 1; + + CHECK_STATUS_AND_RETURN(AudioUnitSetProperty(mixerUnit, kAudioUnitProperty_ElementCount, kAudioUnitScope_Input, 0, &busCount, sizeof(busCount))); + + Float64 graphSampleRate = 44100.0; + + CHECK_STATUS_AND_RETURN(AudioUnitSetProperty(mixerUnit, kAudioUnitProperty_SampleRate, kAudioUnitScope_Output, 0, &graphSampleRate, sizeof(graphSampleRate))); + CHECK_STATUS_AND_RETURN(AudioUnitSetParameter(mixerUnit, kMultiChannelMixerParam_Volume, kAudioUnitScope_Input, 0, 1.0, 0)); +} + +-(void) createEqUnit +{ +#if TARGET_OS_MAC && MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_9 + return; +#else + OSStatus status; + + if (self->options.equalizerBandFrequencies[0] == 0) + { + return; + } + + CHECK_STATUS_AND_RETURN(AUGraphAddNode(audioGraph, &nbandUnitDescription, &eqNode)); + CHECK_STATUS_AND_RETURN(AUGraphNodeInfo(audioGraph, eqNode, NULL, &eqUnit)); + CHECK_STATUS_AND_RETURN(AudioUnitSetProperty(eqUnit, kAudioUnitProperty_MaximumFramesPerSlice, kAudioUnitScope_Global, 0, &maxFramesPerSlice, sizeof(maxFramesPerSlice))); + + while (self->options.equalizerBandFrequencies[eqBandCount] != 0) + { + eqBandCount++; + } + + CHECK_STATUS_AND_RETURN(AudioUnitSetProperty(eqUnit, kAUNBandEQProperty_NumberOfBands, kAudioUnitScope_Global, 0, &eqBandCount, sizeof(eqBandCount))); + + for (int i = 0; i < eqBandCount; i++) + { + CHECK_STATUS_AND_RETURN(AudioUnitSetParameter(eqUnit, kAUNBandEQParam_Frequency + i, kAudioUnitScope_Global, 0, (AudioUnitParameterValue)self->options.equalizerBandFrequencies[i], 0)); + } + + for (int i = 0; i < eqBandCount; i++) + { + CHECK_STATUS_AND_RETURN(AudioUnitSetParameter(eqUnit, kAUNBandEQParam_BypassBand + i, kAudioUnitScope_Global, 0, (AudioUnitParameterValue)0, 0)); + } +#endif +} + +-(void) setGain:(float)gain forEqualizerBand:(int)bandIndex +{ + if (!eqUnit) + { + return; + } + + OSStatus status; + + CHECK_STATUS_AND_RETURN(AudioUnitSetParameter(eqUnit, kAUNBandEQParam_Gain + bandIndex, kAudioUnitScope_Global, 0, gain, 0)); +} + +-(AUNode) createConverterNode:(AudioStreamBasicDescription)srcFormat desFormat:(AudioStreamBasicDescription)desFormat +{ + OSStatus status; + AUNode convertNode; + AudioComponentInstance convertUnit; + + CHECK_STATUS_AND_RETURN_VALUE(AUGraphAddNode(audioGraph, &convertUnitDescription, &convertNode), 0); + CHECK_STATUS_AND_RETURN_VALUE(AUGraphNodeInfo(audioGraph, convertNode, &mixerDescription, &convertUnit), 0); + CHECK_STATUS_AND_RETURN_VALUE(AudioUnitSetProperty(convertUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 0, &srcFormat, sizeof(srcFormat)), 0); + CHECK_STATUS_AND_RETURN_VALUE(AudioUnitSetProperty(convertUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, 0, &desFormat, sizeof(desFormat)), 0); + CHECK_STATUS_AND_RETURN_VALUE(AudioUnitSetProperty(convertUnit, kAudioUnitProperty_MaximumFramesPerSlice, kAudioUnitScope_Global, 0, &maxFramesPerSlice, sizeof(maxFramesPerSlice)), 0); + + [converterNodes addObject:@(convertNode)]; + + return convertNode; +} + +-(void) connectNodes:(AUNode)srcNode desNode:(AUNode)desNode srcUnit:(AudioComponentInstance)srcUnit desUnit:(AudioComponentInstance)desUnit +{ + OSStatus status; + BOOL addConverter = NO; + AudioStreamBasicDescription srcFormat, desFormat; + UInt32 size = sizeof(AudioStreamBasicDescription); + + CHECK_STATUS_AND_RETURN(AudioUnitGetProperty(srcUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, 0, &srcFormat, &size)); + CHECK_STATUS_AND_RETURN(AudioUnitGetProperty(desUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 0, &desFormat, &size)); + + addConverter = memcmp(&srcFormat, &desFormat, sizeof(srcFormat)) != 0; + + if (addConverter) + { + status = AudioUnitSetProperty(desUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 0, &srcFormat, sizeof(srcFormat)); + + addConverter = status != 0; + } + + if (addConverter) + { + AUNode convertNode = [self createConverterNode:srcFormat desFormat:desFormat]; + + CHECK_STATUS_AND_RETURN(AUGraphConnectNodeInput(audioGraph, srcNode, 0, convertNode, 0)); + CHECK_STATUS_AND_RETURN(AUGraphConnectNodeInput(audioGraph, convertNode, 0, desNode, 0)); + } + else + { + CHECK_STATUS_AND_RETURN(AUGraphConnectNodeInput(audioGraph, srcNode, 0, desNode, 0)); + } +} + +-(void) setOutputCallbackForFirstNode:(AUNode)firstNode firstUnit:(AudioComponentInstance)firstUnit +{ + OSStatus status; + AURenderCallbackStruct callbackStruct; + + callbackStruct.inputProc = OutputRenderCallback; + callbackStruct.inputProcRefCon = (__bridge void*)self; + + status = AudioUnitSetProperty(firstUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 0, &canonicalAudioStreamBasicDescription, sizeof(canonicalAudioStreamBasicDescription)); + + if (status) + { + AudioStreamBasicDescription format; + UInt32 size = sizeof(format); + + CHECK_STATUS_AND_RETURN(AudioUnitGetProperty(firstUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 0, &format, &size)); + + AUNode converterNode = [self createConverterNode:canonicalAudioStreamBasicDescription desFormat:format]; + + CHECK_STATUS_AND_RETURN(AUGraphSetNodeInputCallback(audioGraph, converterNode, 0, &callbackStruct)); + + CHECK_STATUS_AND_RETURN(AUGraphConnectNodeInput(audioGraph, converterNode, 0, firstNode, 0)); + } + else + { + CHECK_STATUS_AND_RETURN(AUGraphSetNodeInputCallback(audioGraph, firstNode, 0, &callbackStruct)); + } +} + +-(void) createAudioGraph +{ + OSStatus status; + converterNodes = [[NSMutableArray alloc] init]; + + CHECK_STATUS_AND_RETURN(NewAUGraph(&audioGraph)); + CHECK_STATUS_AND_RETURN(AUGraphOpen(audioGraph)); + + [self createEqUnit]; + [self createMixerUnit]; + [self createOutputUnit]; + + [self connectGraph]; + + CHECK_STATUS_AND_RETURN(AUGraphInitialize(audioGraph)); + + self.volume = self->volume; +} + +-(void) connectGraph +{ + OSStatus status; + NSMutableArray* nodes = [[NSMutableArray alloc] init]; + NSMutableArray* units = [[NSMutableArray alloc] init]; + + AUGraphClearConnections(audioGraph); + + for (NSNumber* converterNode in converterNodes) + { + CHECK_STATUS_AND_REPORT(AUGraphRemoveNode(audioGraph, (AUNode)converterNode.intValue)); + } + + [converterNodes removeAllObjects]; + + if (eqNode) + { + if (self->equalizerEnabled) + { + [nodes addObject:@(eqNode)]; + [units addObject:[NSValue valueWithPointer:eqUnit]]; + + self->equalizerOn = YES; + } + else + { + self->equalizerOn = NO; + } + } + else + { + self->equalizerOn = NO; + } + + if (mixerNode) + { + [nodes addObject:@(mixerNode)]; + [units addObject:[NSValue valueWithPointer:mixerUnit]]; + } + + if (outputNode) + { + [nodes addObject:@(outputNode)]; + [units addObject:[NSValue valueWithPointer:outputUnit]]; + } + + [self setOutputCallbackForFirstNode:(AUNode)[[nodes objectAtIndex:0] intValue] firstUnit:(AudioComponentInstance)[[units objectAtIndex:0] pointerValue]]; + + for (int i = 0; i < nodes.count - 1; i++) + { + AUNode srcNode = [[nodes objectAtIndex:i] intValue]; + AUNode desNode = [[nodes objectAtIndex:i + 1] intValue]; + AudioComponentInstance srcUnit = (AudioComponentInstance)[[units objectAtIndex:i] pointerValue]; + AudioComponentInstance desUnit = (AudioComponentInstance)[[units objectAtIndex:i + 1] pointerValue]; + + [self connectNodes:srcNode desNode:desNode srcUnit:srcUnit desUnit:desUnit]; + } +} + +-(BOOL) audioGraphIsRunning +{ + OSStatus status; + Boolean isRunning; + + status = AUGraphIsRunning(audioGraph, &isRunning); + + if (status) + { + return NO; + } + + return isRunning; +} + +-(BOOL) startAudioGraph +{ + OSStatus status; + + [self resetPcmBuffers]; + + if ([self audioGraphIsRunning]) + { + return NO; + } + + status = AUGraphStart(audioGraph); + + if (status) + { + [self unexpectedError:STKAudioPlayerErrorAudioSystemError]; + + return NO; + } + + [self stopSystemBackgroundTask]; + + return YES; +} + +-(void) stopAudioUnitWithReason:(STKAudioPlayerStopReason)stopReasonIn +{ + OSStatus status; + + if (!audioGraph) + { + stopReason = stopReasonIn; + self.internalState = STKAudioPlayerInternalStateStopped; + + return; + } + + Boolean isRunning; + + status = AUGraphIsRunning(audioGraph, &isRunning); + + if (status) + { + [self unexpectedError:STKAudioPlayerErrorAudioSystemError]; + } + else if (!isRunning) + { + return; + } + + status = AUGraphStop(audioGraph); + + if (status) + { + [self unexpectedError:STKAudioPlayerErrorAudioSystemError]; + } + + [self resetPcmBuffers]; + + stopReason = stopReasonIn; + self.internalState = STKAudioPlayerInternalStateStopped; +} + +typedef struct +{ + BOOL done; + UInt32 numberOfPackets; + AudioBuffer audioBuffer; + AudioStreamPacketDescription* packetDescriptions; +} +AudioConvertInfo; + +OSStatus AudioConverterCallback(AudioConverterRef inAudioConverter, UInt32* ioNumberDataPackets, AudioBufferList* ioData, AudioStreamPacketDescription **outDataPacketDescription, void* inUserData) +{ + AudioConvertInfo* convertInfo = (AudioConvertInfo*)inUserData; + + if (convertInfo->done) + { + ioNumberDataPackets = 0; + + return 100; + } + + ioData->mNumberBuffers = 1; + ioData->mBuffers[0] = convertInfo->audioBuffer; + + if (outDataPacketDescription) + { + *outDataPacketDescription = convertInfo->packetDescriptions; + } + + *ioNumberDataPackets = convertInfo->numberOfPackets; + convertInfo->done = YES; + + return 0; +} + +-(void) handleAudioPackets:(const void*)inputData numberBytes:(UInt32)numberBytes numberPackets:(UInt32)numberPackets packetDescriptions:(AudioStreamPacketDescription*)packetDescriptionsIn +{ + if (currentlyReadingEntry == nil) + { + return; + } + + if (!currentlyReadingEntry->parsedHeader) + { + return; + } + + if (disposeWasRequested) + { + return; + } + + if (audioConverterRef == nil) + { + return; + } + + if ((seekToTimeWasRequested && [currentlyPlayingEntry calculatedBitRate] > 0.0)) + { + [self wakeupPlaybackThread]; + + return; + } + + discontinuous = NO; + + OSStatus status; + + AudioConvertInfo convertInfo; + + convertInfo.done = NO; + convertInfo.numberOfPackets = numberPackets; + convertInfo.packetDescriptions = packetDescriptionsIn; + convertInfo.audioBuffer.mData = (void *)inputData; + convertInfo.audioBuffer.mDataByteSize = numberBytes; + convertInfo.audioBuffer.mNumberChannels = audioConverterAudioStreamBasicDescription.mChannelsPerFrame; + + if (packetDescriptionsIn && currentlyReadingEntry->processedPacketsCount < STK_MAX_COMPRESSED_PACKETS_FOR_BITRATE_CALCULATION) + { + int count = MIN(numberPackets, STK_MAX_COMPRESSED_PACKETS_FOR_BITRATE_CALCULATION - currentlyReadingEntry->processedPacketsCount); + + for (int i = 0; i < count; i++) + { + SInt64 packetSize; + + packetSize = packetDescriptionsIn[i].mDataByteSize; + + OSAtomicAdd32((int32_t)packetSize, ¤tlyReadingEntry->processedPacketsSizeTotal); + OSAtomicIncrement32(¤tlyReadingEntry->processedPacketsCount); + } + } + + while (true) + { + OSSpinLockLock(&pcmBufferSpinLock); + UInt32 used = pcmBufferUsedFrameCount; + UInt32 start = pcmBufferFrameStartIndex; + UInt32 end = (pcmBufferFrameStartIndex + pcmBufferUsedFrameCount) % pcmBufferTotalFrameCount; + UInt32 framesLeftInsideBuffer = pcmBufferTotalFrameCount - used; + OSSpinLockUnlock(&pcmBufferSpinLock); + + if (framesLeftInsideBuffer == 0) + { + pthread_mutex_lock(&playerMutex); + + while (true) + { + OSSpinLockLock(&pcmBufferSpinLock); + used = pcmBufferUsedFrameCount; + start = pcmBufferFrameStartIndex; + end = (pcmBufferFrameStartIndex + pcmBufferUsedFrameCount) % pcmBufferTotalFrameCount; + framesLeftInsideBuffer = pcmBufferTotalFrameCount - used; + OSSpinLockUnlock(&pcmBufferSpinLock); + + if (framesLeftInsideBuffer > 0) + { + break; + } + + if (disposeWasRequested + || self.internalState == STKAudioPlayerInternalStateStopped + || self.internalState == STKAudioPlayerInternalStateDisposed + || self.internalState == STKAudioPlayerInternalStatePendingNext) + { + pthread_mutex_unlock(&playerMutex); + + return; + } + + if (seekToTimeWasRequested && [currentlyPlayingEntry calculatedBitRate] > 0.0) + { + pthread_mutex_unlock(&playerMutex); + + [self wakeupPlaybackThread]; + + return; + } + + waiting = YES; + + pthread_cond_wait(&playerThreadReadyCondition, &playerMutex); + + waiting = NO; + } + + pthread_mutex_unlock(&playerMutex); + } + + AudioBuffer* localPcmAudioBuffer; + AudioBufferList localPcmBufferList; + + localPcmBufferList.mNumberBuffers = 1; + localPcmAudioBuffer = &localPcmBufferList.mBuffers[0]; + + if (end >= start) + { + UInt32 framesAdded = 0; + UInt32 framesToDecode = pcmBufferTotalFrameCount - end; + + localPcmAudioBuffer->mData = pcmAudioBuffer->mData + (end * pcmBufferFrameSizeInBytes); + localPcmAudioBuffer->mDataByteSize = framesToDecode * pcmBufferFrameSizeInBytes; + localPcmAudioBuffer->mNumberChannels = pcmAudioBuffer->mNumberChannels; + + status = AudioConverterFillComplexBuffer(audioConverterRef, AudioConverterCallback, (void*)&convertInfo, &framesToDecode, &localPcmBufferList, NULL); + + framesAdded = framesToDecode; + + if (status == 100) + { + OSSpinLockLock(&pcmBufferSpinLock); + pcmBufferUsedFrameCount += framesAdded; + OSSpinLockUnlock(&pcmBufferSpinLock); + + OSSpinLockLock(¤tlyReadingEntry->spinLock); + currentlyReadingEntry->framesQueued += framesAdded; + OSSpinLockUnlock(¤tlyReadingEntry->spinLock); + + return; + } + else if (status != 0) + { + [self unexpectedError:STKAudioPlayerErrorCodecError]; + + return; + } + + framesToDecode = start; + + if (framesToDecode == 0) + { + OSSpinLockLock(&pcmBufferSpinLock); + pcmBufferUsedFrameCount += framesAdded; + OSSpinLockUnlock(&pcmBufferSpinLock); + + OSSpinLockLock(¤tlyReadingEntry->spinLock); + currentlyReadingEntry->framesQueued += framesAdded; + OSSpinLockUnlock(¤tlyReadingEntry->spinLock); + + continue; + } + + localPcmAudioBuffer->mData = pcmAudioBuffer->mData; + localPcmAudioBuffer->mDataByteSize = framesToDecode * pcmBufferFrameSizeInBytes; + localPcmAudioBuffer->mNumberChannels = pcmAudioBuffer->mNumberChannels; + + status = AudioConverterFillComplexBuffer(audioConverterRef, AudioConverterCallback, (void*)&convertInfo, &framesToDecode, &localPcmBufferList, NULL); + + framesAdded += framesToDecode; + + if (status == 100) + { + OSSpinLockLock(&pcmBufferSpinLock); + pcmBufferUsedFrameCount += framesAdded; + OSSpinLockUnlock(&pcmBufferSpinLock); + + OSSpinLockLock(¤tlyReadingEntry->spinLock); + currentlyReadingEntry->framesQueued += framesAdded; + OSSpinLockUnlock(¤tlyReadingEntry->spinLock); + + return; + } + else if (status == 0) + { + OSSpinLockLock(&pcmBufferSpinLock); + pcmBufferUsedFrameCount += framesAdded; + OSSpinLockUnlock(&pcmBufferSpinLock); + + OSSpinLockLock(¤tlyReadingEntry->spinLock); + currentlyReadingEntry->framesQueued += framesAdded; + OSSpinLockUnlock(¤tlyReadingEntry->spinLock); + + continue; + } + else if (status != 0) + { + [self unexpectedError:STKAudioPlayerErrorCodecError]; + + return; + } + } + else + { + UInt32 framesAdded = 0; + UInt32 framesToDecode = start - end; + + localPcmAudioBuffer->mData = pcmAudioBuffer->mData + (end * pcmBufferFrameSizeInBytes); + localPcmAudioBuffer->mDataByteSize = framesToDecode * pcmBufferFrameSizeInBytes; + localPcmAudioBuffer->mNumberChannels = pcmAudioBuffer->mNumberChannels; + + status = AudioConverterFillComplexBuffer(audioConverterRef, AudioConverterCallback, (void*)&convertInfo, &framesToDecode, &localPcmBufferList, NULL); + + framesAdded = framesToDecode; + + if (status == 100) + { + OSSpinLockLock(&pcmBufferSpinLock); + pcmBufferUsedFrameCount += framesAdded; + OSSpinLockUnlock(&pcmBufferSpinLock); + + OSSpinLockLock(¤tlyReadingEntry->spinLock); + currentlyReadingEntry->framesQueued += framesAdded; + OSSpinLockUnlock(¤tlyReadingEntry->spinLock); + + return; + } + else if (status == 0) + { + OSSpinLockLock(&pcmBufferSpinLock); + pcmBufferUsedFrameCount += framesAdded; + OSSpinLockUnlock(&pcmBufferSpinLock); + + OSSpinLockLock(¤tlyReadingEntry->spinLock); + currentlyReadingEntry->framesQueued += framesAdded; + OSSpinLockUnlock(¤tlyReadingEntry->spinLock); + + continue; + } + else if (status != 0) + { + [self unexpectedError:STKAudioPlayerErrorCodecError]; + + return; + } + } + } +} + +static OSStatus OutputRenderCallback(void* inRefCon, AudioUnitRenderActionFlags* ioActionFlags, const AudioTimeStamp* inTimeStamp, UInt32 inBusNumber, UInt32 inNumberFrames, AudioBufferList* ioData) +{ + STKAudioPlayer* audioPlayer = (__bridge STKAudioPlayer*)inRefCon; + + OSSpinLockLock(&audioPlayer->currentEntryReferencesLock); + STKQueueEntry* entry = audioPlayer->currentlyPlayingEntry; + STKQueueEntry* currentlyReadingEntry = audioPlayer->currentlyReadingEntry; + OSSpinLockUnlock(&audioPlayer->currentEntryReferencesLock); + + OSSpinLockLock(&audioPlayer->pcmBufferSpinLock); + + BOOL waitForBuffer = NO; + BOOL muted = audioPlayer->muted; + AudioBuffer* audioBuffer = audioPlayer->pcmAudioBuffer; + UInt32 frameSizeInBytes = audioPlayer->pcmBufferFrameSizeInBytes; + UInt32 used = audioPlayer->pcmBufferUsedFrameCount; + UInt32 start = audioPlayer->pcmBufferFrameStartIndex; + STKAudioPlayerInternalState state = audioPlayer->internalState; + UInt32 end = (audioPlayer->pcmBufferFrameStartIndex + audioPlayer->pcmBufferUsedFrameCount) % audioPlayer->pcmBufferTotalFrameCount; + BOOL signal = audioPlayer->waiting && used < audioPlayer->pcmBufferTotalFrameCount / 2; + NSArray* frameFilters = audioPlayer->frameFilters; + + if (entry) + { + if (state == STKAudioPlayerInternalStateWaitingForData) + { + SInt64 framesRequiredToStartPlaying = audioPlayer->framesRequiredToStartPlaying; + + if (entry->lastFrameQueued >= 0) + { + framesRequiredToStartPlaying = MIN(framesRequiredToStartPlaying, entry->lastFrameQueued); + } + + if (entry && currentlyReadingEntry == entry + && entry->framesQueued < framesRequiredToStartPlaying) + { + waitForBuffer = YES; + } + } + else if (state == STKAudioPlayerInternalStateRebuffering) + { + SInt64 framesRequiredToStartPlaying = audioPlayer->framesRequiredToPlayAfterRebuffering; + + if (entry->lastFrameQueued >= 0) + { + framesRequiredToStartPlaying = MIN(framesRequiredToStartPlaying, entry->lastFrameQueued - entry->framesQueued); + } + + if (used < framesRequiredToStartPlaying) + { + waitForBuffer = YES; + } + } + else if (state == STKAudioPlayerInternalStateWaitingForDataAfterSeek) + { + SInt64 framesRequiredToStartPlaying = 1024; + + if (entry->lastFrameQueued >= 0) + { + framesRequiredToStartPlaying = MIN(framesRequiredToStartPlaying, entry->lastFrameQueued - entry->framesQueued); + } + + if (used < framesRequiredToStartPlaying) + { + waitForBuffer = YES; + } + } + } + + OSSpinLockUnlock(&audioPlayer->pcmBufferSpinLock); + + UInt32 totalFramesCopied = 0; + + if (used > 0 && !waitForBuffer && entry != nil && ((state & STKAudioPlayerInternalStateRunning) && state != STKAudioPlayerInternalStatePaused)) + { + if (state == STKAudioPlayerInternalStateWaitingForData) + { + // Starting + } + else if (state == STKAudioPlayerInternalStateRebuffering) + { + // Resuming from buffering + } + + if (end > start) + { + UInt32 framesToCopy = MIN(inNumberFrames, used); + + ioData->mBuffers[0].mNumberChannels = 2; + ioData->mBuffers[0].mDataByteSize = frameSizeInBytes * framesToCopy; + + if (muted) + { + memset(ioData->mBuffers[0].mData, 0, ioData->mBuffers[0].mDataByteSize); + } + else + { + memcpy(ioData->mBuffers[0].mData, audioBuffer->mData + (start * frameSizeInBytes), ioData->mBuffers[0].mDataByteSize); + } + + totalFramesCopied = framesToCopy; + + OSSpinLockLock(&audioPlayer->pcmBufferSpinLock); + audioPlayer->pcmBufferFrameStartIndex = (audioPlayer->pcmBufferFrameStartIndex + totalFramesCopied) % audioPlayer->pcmBufferTotalFrameCount; + audioPlayer->pcmBufferUsedFrameCount -= totalFramesCopied; + OSSpinLockUnlock(&audioPlayer->pcmBufferSpinLock); + } + else + { + UInt32 framesToCopy = MIN(inNumberFrames, audioPlayer->pcmBufferTotalFrameCount - start); + + ioData->mBuffers[0].mNumberChannels = 2; + ioData->mBuffers[0].mDataByteSize = frameSizeInBytes * framesToCopy; + + if (muted) + { + memset(ioData->mBuffers[0].mData, 0, ioData->mBuffers[0].mDataByteSize); + } + else + { + memcpy(ioData->mBuffers[0].mData, audioBuffer->mData + (start * frameSizeInBytes), ioData->mBuffers[0].mDataByteSize); + } + + UInt32 moreFramesToCopy = 0; + UInt32 delta = inNumberFrames - framesToCopy; + + if (delta > 0) + { + moreFramesToCopy = MIN(delta, end); + + ioData->mBuffers[0].mNumberChannels = 2; + ioData->mBuffers[0].mDataByteSize += frameSizeInBytes * moreFramesToCopy; + + if (muted) + { + memset(ioData->mBuffers[0].mData + (framesToCopy * frameSizeInBytes), 0, frameSizeInBytes * moreFramesToCopy); + } + else + { + memcpy(ioData->mBuffers[0].mData + (framesToCopy * frameSizeInBytes), audioBuffer->mData, frameSizeInBytes * moreFramesToCopy); + } + } + + totalFramesCopied = framesToCopy + moreFramesToCopy; + + OSSpinLockLock(&audioPlayer->pcmBufferSpinLock); + audioPlayer->pcmBufferFrameStartIndex = (audioPlayer->pcmBufferFrameStartIndex + totalFramesCopied) % audioPlayer->pcmBufferTotalFrameCount; + audioPlayer->pcmBufferUsedFrameCount -= totalFramesCopied; + OSSpinLockUnlock(&audioPlayer->pcmBufferSpinLock); + } + + [audioPlayer setInternalState:STKAudioPlayerInternalStatePlaying ifInState:^BOOL(STKAudioPlayerInternalState state) + { + return (state & STKAudioPlayerInternalStateRunning) && state != STKAudioPlayerInternalStatePaused; + }]; + } + + if (totalFramesCopied < inNumberFrames) + { + UInt32 delta = inNumberFrames - totalFramesCopied; + + memset(ioData->mBuffers[0].mData + (totalFramesCopied * frameSizeInBytes), 0, delta * frameSizeInBytes); + + if (!(entry == nil || state == STKAudioPlayerInternalStateWaitingForDataAfterSeek || state == STKAudioPlayerInternalStateWaitingForData || state == STKAudioPlayerInternalStateRebuffering)) + { + // Buffering + + [audioPlayer setInternalState:STKAudioPlayerInternalStateRebuffering ifInState:^BOOL(STKAudioPlayerInternalState state) + { + return (state & STKAudioPlayerInternalStateRunning) && state != STKAudioPlayerInternalStatePaused; + }]; + } + else if (state == STKAudioPlayerInternalStateWaitingForDataAfterSeek) + { + if (totalFramesCopied == 0) + { + OSAtomicAdd32(inNumberFrames - totalFramesCopied, &audioPlayer->waitingForDataAfterSeekFrameCount); + + if (audioPlayer->waitingForDataAfterSeekFrameCount > audioPlayer->framesRequiredBeforeWaitingForDataAfterSeekBecomesPlaying) + { + [audioPlayer setInternalState:STKAudioPlayerInternalStatePlaying ifInState:^BOOL(STKAudioPlayerInternalState state) + { + return (state & STKAudioPlayerInternalStateRunning) && state != STKAudioPlayerInternalStatePaused; + }]; + } + } + else + { + audioPlayer->waitingForDataAfterSeekFrameCount = 0; + } + } + } + + if (frameFilters) + { + NSUInteger count = frameFilters.count; + AudioStreamBasicDescription asbd = canonicalAudioStreamBasicDescription; + + for (int i = 0; i < count; i++) + { + STKFrameFilterEntry* entry = [frameFilters objectAtIndex:i]; + + entry->filter(asbd.mChannelsPerFrame, asbd.mBytesPerFrame, inNumberFrames, ioData->mBuffers[0].mData); + } + } + + if (audioPlayer->equalizerEnabled != audioPlayer->equalizerOn) + { + Boolean isUpdated; + + [audioPlayer connectGraph]; + + AUGraphUpdate(audioPlayer->audioGraph, &isUpdated); + + isUpdated = isUpdated; + } + + if (entry == nil) + { + return 0; + } + + OSSpinLockLock(&entry->spinLock); + + SInt64 extraFramesPlayedNotAssigned = 0; + SInt64 framesPlayedForCurrent = totalFramesCopied; + + if (entry->lastFrameQueued >= 0) + { + framesPlayedForCurrent = MIN(entry->lastFrameQueued - entry->framesPlayed, framesPlayedForCurrent); + } + + entry->framesPlayed += framesPlayedForCurrent; + extraFramesPlayedNotAssigned = totalFramesCopied - framesPlayedForCurrent; + + BOOL lastFramePlayed = entry->framesPlayed == entry->lastFrameQueued; + + OSSpinLockUnlock(&entry->spinLock); + + if (signal || lastFramePlayed) + { + pthread_mutex_lock(&audioPlayer->playerMutex); + + OSSpinLockLock(&audioPlayer->currentEntryReferencesLock); + STKQueueEntry* currentlyPlayingEntry = audioPlayer->currentlyPlayingEntry; + OSSpinLockUnlock(&audioPlayer->currentEntryReferencesLock); + + if (lastFramePlayed && entry == currentlyPlayingEntry) + { + [audioPlayer audioQueueFinishedPlaying:entry]; + + while (extraFramesPlayedNotAssigned > 0) + { + OSSpinLockLock(&audioPlayer->currentEntryReferencesLock); + STKQueueEntry* newEntry = audioPlayer->currentlyPlayingEntry; + OSSpinLockUnlock(&audioPlayer->currentEntryReferencesLock); + + if (newEntry != nil) + { + SInt64 framesPlayedForCurrent = extraFramesPlayedNotAssigned; + + OSSpinLockLock(&newEntry->spinLock); + + if (newEntry->lastFrameQueued > 0) + { + framesPlayedForCurrent = MIN(newEntry->lastFrameQueued - newEntry->framesPlayed, framesPlayedForCurrent); + } + + newEntry->framesPlayed += framesPlayedForCurrent; + + if (newEntry->framesPlayed == newEntry->lastFrameQueued) + { + OSSpinLockUnlock(&newEntry->spinLock); + + [audioPlayer audioQueueFinishedPlaying:newEntry]; + } + else + { + OSSpinLockUnlock(&newEntry->spinLock); + } + + extraFramesPlayedNotAssigned -= framesPlayedForCurrent; + } + else + { + break; + } + } + } + + pthread_cond_signal(&audioPlayer->playerThreadReadyCondition); + pthread_mutex_unlock(&audioPlayer->playerMutex); + } + + return 0; +} + +-(NSArray*) pendingQueue +{ + pthread_mutex_lock(&playerMutex); + + NSArray* retval; + NSMutableArray* mutableArray = [[NSMutableArray alloc] initWithCapacity:upcomingQueue.count + bufferingQueue.count]; + + for (STKQueueEntry* entry in upcomingQueue) + { + [mutableArray addObject:[entry queueItemId]]; + } + + for (STKQueueEntry* entry in bufferingQueue) + { + [mutableArray addObject:[entry queueItemId]]; + } + + retval = [NSArray arrayWithArray:mutableArray]; + + pthread_mutex_unlock(&playerMutex); + + return retval; +} + +-(NSUInteger) pendingQueueCount +{ + pthread_mutex_lock(&playerMutex); + + NSUInteger retval = upcomingQueue.count + bufferingQueue.count; + + pthread_mutex_unlock(&playerMutex); + + return retval; +} + +-(NSObject*) mostRecentlyQueuedStillPendingItem +{ + pthread_mutex_lock(&playerMutex); + + if (upcomingQueue.count > 0) + { + NSObject* retval = [[upcomingQueue objectAtIndex:0] queueItemId]; + + pthread_mutex_unlock(&playerMutex); + + return retval; + } + + if (bufferingQueue.count > 0) + { + NSObject* retval = [[bufferingQueue objectAtIndex:0] queueItemId]; + + pthread_mutex_unlock(&playerMutex); + + return retval; + } + + pthread_mutex_unlock(&playerMutex); + + return nil; +} + +-(float) peakPowerInDecibelsForChannel:(NSUInteger)channelNumber +{ + if (channelNumber >= canonicalAudioStreamBasicDescription.mChannelsPerFrame) + { + return 0; + } + + return peakPowerDb[channelNumber]; +} + +-(float) averagePowerInDecibelsForChannel:(NSUInteger)channelNumber +{ + if (channelNumber >= canonicalAudioStreamBasicDescription.mChannelsPerFrame) + { + return 0; + } + + return averagePowerDb[channelNumber]; +} + +-(BOOL) meteringEnabled +{ + return self->meteringEnabled; +} + +#define CALCULATE_METER(channel) \ + Float32 currentFilteredValueOfSampleAmplitude##channel = STK_LOWPASSFILTERTIMESLICE * absoluteValueOfSampleAmplitude##channel + (1.0 - STK_LOWPASSFILTERTIMESLICE) * previousFilteredValueOfSampleAmplitude##channel; \ + previousFilteredValueOfSampleAmplitude##channel = currentFilteredValueOfSampleAmplitude##channel; \ + Float32 sampleDB##channel = 20.0 * log10(currentFilteredValueOfSampleAmplitude##channel) + STK_DBOFFSET; \ + if ((sampleDB##channel == sampleDB##channel) && (sampleDB##channel != -DBL_MAX)) \ + { \ + if(sampleDB##channel > peakValue##channel) \ + { \ + peakValue##channel = sampleDB##channel; \ + } \ + if (sampleDB##channel > -DBL_MAX) \ + { \ + count##channel++; \ + totalValue##channel += sampleDB##channel; \ + } \ + decibels##channel = peakValue##channel; \ + }; + +-(void) setMeteringEnabled:(BOOL)value +{ + if (self->meteringEnabled == value) + { + return; + } + + if (!value) + { + [self removeFrameFilterWithName:@"STKMeteringFilter"]; + self->meteringEnabled = NO; + } + else + { + [self appendFrameFilterWithName:@"STKMeteringFilter" block:^(UInt32 channelsPerFrame, UInt32 bytesPerFrame, UInt32 frameCount, void* frames) + { + SInt16* samples16 = (SInt16*)frames; + SInt32* samples32 = (SInt32*)frames; + UInt32 countLeft = 0; + UInt32 countRight = 0; + Float32 decibelsLeft = STK_DBMIN; + Float32 peakValueLeft = STK_DBMIN; + Float64 totalValueLeft = 0; + Float32 previousFilteredValueOfSampleAmplitudeLeft = 0; + Float32 decibelsRight = STK_DBMIN; + Float32 peakValueRight = STK_DBMIN; + Float64 totalValueRight = 0; + Float32 previousFilteredValueOfSampleAmplitudeRight = 0; + + if (bytesPerFrame / channelsPerFrame == 2) + { + for (int i = 0; i < frameCount * channelsPerFrame; i += channelsPerFrame) + { + Float32 absoluteValueOfSampleAmplitudeLeft = abs(samples16[i]); + Float32 absoluteValueOfSampleAmplitudeRight = abs(samples16[i + 1]); + + CALCULATE_METER(Left); + CALCULATE_METER(Right); + } + } + else if (bytesPerFrame / channelsPerFrame == 4) + { + for (int i = 0; i < frameCount * channelsPerFrame; i += channelsPerFrame) + { + Float32 absoluteValueOfSampleAmplitudeLeft = abs(samples32[i]) / 32768.0; + Float32 absoluteValueOfSampleAmplitudeRight = abs(samples32[i + 1]) / 32768.0; + + CALCULATE_METER(Left); + CALCULATE_METER(Right); + } + } + else + { + return; + } + + peakPowerDb[0] = MIN(MAX(decibelsLeft, -60), 0); + peakPowerDb[1] = MIN(MAX(decibelsRight, -60), 0); + + if (countLeft > 0) + { + averagePowerDb[0] = MIN(MAX(totalValueLeft / frameCount, -60), 0); + } + + if (countRight != 0) + { + averagePowerDb[1] = MIN(MAX(totalValueRight / frameCount, -60), 0); + } + }]; + } +} + +#pragma mark Frame Filters + +-(NSArray*) frameFilters +{ + return frameFilters; +} + +-(void) appendFrameFilterWithName:(NSString*)name block:(STKFrameFilter)block +{ + [self addFrameFilterWithName:name afterFilterWithName:nil block:block]; +} + +-(void) removeFrameFilterWithName:(NSString*)name +{ + pthread_mutex_lock(&self->playerMutex); + + NSMutableArray* newFrameFilters = [[NSMutableArray alloc] initWithCapacity:frameFilters.count + 1]; + + for (STKFrameFilterEntry* filterEntry in frameFilters) + { + if (![filterEntry->name isEqualToString:name]) + { + [newFrameFilters addObject:filterEntry]; + } + } + + NSArray* replacement = [NSArray arrayWithArray:newFrameFilters]; + + OSSpinLockLock(&pcmBufferSpinLock); + if (newFrameFilters.count > 0) + { + frameFilters = replacement; + } + else + { + frameFilters = nil; + } + OSSpinLockUnlock(&pcmBufferSpinLock); + + pthread_mutex_unlock(&self->playerMutex); +} + +-(void) addFrameFilterWithName:(NSString*)name afterFilterWithName:(NSString*)afterFilterWithName block:(STKFrameFilter)block +{ + pthread_mutex_lock(&self->playerMutex); + + NSMutableArray* newFrameFilters = [[NSMutableArray alloc] initWithCapacity:frameFilters.count + 1]; + + if (afterFilterWithName == nil) + { + [newFrameFilters addObject:[[STKFrameFilterEntry alloc] initWithFilter:block andName:name]]; + [newFrameFilters addObjectsFromArray:frameFilters]; + } + else + { + for (STKFrameFilterEntry* filterEntry in frameFilters) + { + if (afterFilterWithName != nil && [filterEntry->name isEqualToString:afterFilterWithName]) + { + [newFrameFilters addObject:[[STKFrameFilterEntry alloc] initWithFilter:block andName:name]]; + } + + [newFrameFilters addObject:filterEntry]; + } + } + + NSArray* replacement = [NSArray arrayWithArray:newFrameFilters]; + + OSSpinLockLock(&pcmBufferSpinLock); + frameFilters = replacement; + OSSpinLockUnlock(&pcmBufferSpinLock); + + pthread_mutex_unlock(&self->playerMutex); +} + +-(void) addFrameFilter:(STKFrameFilter)frameFilter withName:(NSString*)name afterFilterWithName:(NSString*)afterFilterWithName +{ + pthread_mutex_lock(&self->playerMutex); + + NSMutableArray* newFrameFilters = [[NSMutableArray alloc] initWithCapacity:frameFilters.count + 1]; + + if (afterFilterWithName == nil) + { + [newFrameFilters addObjectsFromArray:frameFilters]; + [newFrameFilters addObject:[[STKFrameFilterEntry alloc] initWithFilter:frameFilter andName:name]]; + } + else + { + for (STKFrameFilterEntry* filterEntry in frameFilters) + { + [newFrameFilters addObject:filterEntry]; + + if (afterFilterWithName != nil && [filterEntry->name isEqualToString:afterFilterWithName]) + { + [newFrameFilters addObject:[[STKFrameFilterEntry alloc] initWithFilter:frameFilter andName:name]]; + } + } + } + + NSArray* replacement = [NSArray arrayWithArray:newFrameFilters]; + + OSSpinLockLock(&pcmBufferSpinLock); + frameFilters = replacement; + OSSpinLockUnlock(&pcmBufferSpinLock); + + pthread_mutex_unlock(&self->playerMutex); +} + +#pragma mark Volume + +-(void) setVolume:(Float32)value +{ + self->volume = value; + +#if (TARGET_OS_IPHONE) + if (self->mixerNode) + { + AudioUnitSetParameter(self->mixerUnit, kMultiChannelMixerParam_Volume, kAudioUnitScope_Output, 0, self->volume, 0); + } +#else + if (self->mixerNode) + { + AudioUnitSetParameter(self->mixerUnit, kMultiChannelMixerParam_Volume, kAudioUnitScope_Output, 0, self->volume, 0); + } + else + { + AudioUnitSetParameter(outputUnit, kHALOutputParam_Volume, kAudioUnitScope_Output, kOutputBus, self->volume, 0); + } +#endif +} + +-(Float32) volume +{ + return self->volume; +} + +-(BOOL) equalizerEnabled +{ + return self->equalizerEnabled; +} + +-(void) setEqualizerEnabled:(BOOL)value +{ + self->equalizerEnabled = value; +} + + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/STKAutoRecoveringHTTPDataSource.h b/submodules/LegacyComponents/LegacyComponents/STKAutoRecoveringHTTPDataSource.h new file mode 100755 index 0000000000..c55895f3b6 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/STKAutoRecoveringHTTPDataSource.h @@ -0,0 +1,52 @@ +/********************************************************************************** + AudioPlayer.m + + Created by Thong Nguyen on 16/10/2012. + https://github.com/tumtumtum/audjustable + + Copyright (c) 2012-2014 Thong Nguyen (tumtumtum@gmail.com). All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + 3. All advertising materials mentioning features or use of this software + must display the following acknowledgement: + This product includes software developed by Thong Nguyen (tumtumtum@gmail.com) + 4. Neither the name of Thong Nguyen nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY Thong Nguyen ''AS IS'' AND ANY + EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THONG NGUYEN BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **********************************************************************************/ + +#import "STKDataSource.h" +#import "STKHTTPDataSource.h" +#import "STKDataSourceWrapper.h" + +typedef struct +{ + int watchdogPeriodSeconds; + int inactivePeriodBeforeReconnectSeconds; +} +STKAutoRecoveringHTTPDataSourceOptions; + +@interface STKAutoRecoveringHTTPDataSource : STKDataSourceWrapper + +-(id) initWithHTTPDataSource:(STKHTTPDataSource*)innerDataSource; + +@property (readonly) STKHTTPDataSource* innerDataSource; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/STKAutoRecoveringHTTPDataSource.m b/submodules/LegacyComponents/LegacyComponents/STKAutoRecoveringHTTPDataSource.m new file mode 100755 index 0000000000..37fa5e5a93 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/STKAutoRecoveringHTTPDataSource.m @@ -0,0 +1,391 @@ +/********************************************************************************** + AudioPlayer.m + + Created by Thong Nguyen on 16/10/2012. + https://github.com/tumtumtum/audjustable + + Copyright (c) 2012-2014 Thong Nguyen (tumtumtum@gmail.com). All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + 3. All advertising materials mentioning features or use of this software + must display the following acknowledgement: + This product includes software developed by Thong Nguyen (tumtumtum@gmail.com) + 4. Neither the name of Thong Nguyen nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY Thong Nguyen''AS IS'' AND ANY + EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **********************************************************************************/ + +#import +#import +#import +#import +#import +#import +#import "mach/mach_time.h" +#import +#import +#import "STKAutoRecoveringHTTPDataSource.h" + +#define DEFAULT_WATCHDOG_PERIOD_SECONDS (8) +#define DEFAULT_INACTIVE_PERIOD_BEFORE_RECONNECT_SECONDS (15) + +static uint64_t GetTickCount(void) +{ + static mach_timebase_info_data_t sTimebaseInfo; + uint64_t machTime = mach_absolute_time(); + + if (sTimebaseInfo.denom == 0 ) + { + (void) mach_timebase_info(&sTimebaseInfo); + } + + uint64_t millis = ((machTime / 1000000) * sTimebaseInfo.numer) / sTimebaseInfo.denom; + + return millis; +} + +@interface STKAutoRecoveringHTTPDataSource() +{ + int serial; + int waitSeconds; + NSTimer* timeoutTimer; + BOOL waitingForNetwork; + uint64_t ticksWhenLastDataReceived; + SCNetworkReachabilityRef reachabilityRef; + STKAutoRecoveringHTTPDataSourceOptions options; +} + +-(void) reachabilityChanged; + +@end + +static void ReachabilityCallback(SCNetworkReachabilityRef target, SCNetworkReachabilityFlags flags, void* info) +{ + @autoreleasepool + { + STKAutoRecoveringHTTPDataSource* dataSource = (__bridge STKAutoRecoveringHTTPDataSource*)info; + + [dataSource reachabilityChanged]; + } +} + +static void PopulateOptionsWithDefault(STKAutoRecoveringHTTPDataSourceOptions* options) +{ + if (options->watchdogPeriodSeconds == 0) + { + options->watchdogPeriodSeconds = DEFAULT_WATCHDOG_PERIOD_SECONDS; + } + + if (options->inactivePeriodBeforeReconnectSeconds == 0) + { + options->inactivePeriodBeforeReconnectSeconds = DEFAULT_INACTIVE_PERIOD_BEFORE_RECONNECT_SECONDS; + } +} + +@implementation STKAutoRecoveringHTTPDataSource + +-(STKHTTPDataSource*) innerHTTPDataSource +{ + return (STKHTTPDataSource*)self.innerDataSource; +} + +-(id) initWithDataSource:(STKDataSource *)innerDataSource +{ + return [self initWithHTTPDataSource:(STKHTTPDataSource*)innerDataSource]; +} + +-(id) initWithHTTPDataSource:(STKHTTPDataSource*)innerDataSourceIn +{ + return [self initWithHTTPDataSource:innerDataSourceIn andOptions:(STKAutoRecoveringHTTPDataSourceOptions){}]; +} + +-(id) initWithHTTPDataSource:(STKHTTPDataSource*)innerDataSourceIn andOptions:(STKAutoRecoveringHTTPDataSourceOptions)optionsIn +{ + if (self = [super initWithDataSource:innerDataSourceIn]) + { + self.innerDataSource.delegate = self; + + struct sockaddr_in zeroAddress; + + bzero(&zeroAddress, sizeof(zeroAddress)); + zeroAddress.sin_len = sizeof(zeroAddress); + zeroAddress.sin_family = AF_INET; + + PopulateOptionsWithDefault(&optionsIn); + + self->options = optionsIn; + + reachabilityRef = SCNetworkReachabilityCreateWithAddress(kCFAllocatorDefault, (const struct sockaddr*)&zeroAddress); + } + + return self; +} + +-(BOOL) startNotifierOnRunLoop:(NSRunLoop*)runLoop +{ + BOOL retVal = NO; + SCNetworkReachabilityContext context = { 0, (__bridge void*)self, NULL, NULL, NULL }; + + if (SCNetworkReachabilitySetCallback(reachabilityRef, ReachabilityCallback, &context)) + { + if(SCNetworkReachabilityScheduleWithRunLoop(reachabilityRef, runLoop.getCFRunLoop, kCFRunLoopDefaultMode)) + { + retVal = YES; + } + } + + return retVal; +} + +-(BOOL) registerForEvents:(NSRunLoop*)runLoop +{ + [super registerForEvents:runLoop]; + [self startNotifierOnRunLoop:runLoop]; + + if (timeoutTimer) + { + [timeoutTimer invalidate]; + timeoutTimer = nil; + } + + ticksWhenLastDataReceived = GetTickCount(); + + [self createTimeoutTimer]; + + return YES; +} + +-(void) unregisterForEvents +{ + [super unregisterForEvents]; + [self stopNotifier]; + + [self destroyTimeoutTimer]; +} + +-(void) timeoutTimerTick:(NSTimer*)timer +{ + if (![self hasBytesAvailable]) + { + if ([self hasGotNetworkConnection]) + { + uint64_t currentTicks = GetTickCount(); + + if (((currentTicks - ticksWhenLastDataReceived) / 1000) >= options.inactivePeriodBeforeReconnectSeconds) + { + serial++; + + NSLog(@"timeoutTimerTick %lld/%lld", self.position, self.length); + + [self attemptReconnectWithSerial:@(serial)]; + } + } + } +} + +-(void) createTimeoutTimer +{ + [self destroyTimeoutTimer]; + + NSRunLoop* runLoop = self.innerDataSource.eventsRunLoop; + + if (runLoop == nil) + { + return; + } + + timeoutTimer = [NSTimer timerWithTimeInterval:options.watchdogPeriodSeconds target:self selector:@selector(timeoutTimerTick:) userInfo:@(serial) repeats:YES]; + + [runLoop addTimer:timeoutTimer forMode:NSRunLoopCommonModes]; +} + +-(void) destroyTimeoutTimer +{ + if (timeoutTimer) + { + [timeoutTimer invalidate]; + timeoutTimer = nil; + } +} + +-(void) stopNotifier +{ + if (reachabilityRef != NULL) + { + SCNetworkReachabilitySetCallback(reachabilityRef, NULL, NULL); + SCNetworkReachabilityUnscheduleFromRunLoop(reachabilityRef, [self.innerDataSource.eventsRunLoop getCFRunLoop], kCFRunLoopDefaultMode); + } +} + +-(BOOL) hasGotNetworkConnection +{ + SCNetworkReachabilityFlags flags; + + if (SCNetworkReachabilityGetFlags(reachabilityRef, &flags)) + { + return ((flags & kSCNetworkReachabilityFlagsReachable) != 0); + } + + return NO; +} + +-(void) seekToOffset:(int64_t)offset +{ + ticksWhenLastDataReceived = GetTickCount(); + + [super seekToOffset:offset]; +} + +-(void) close +{ + [self destroyTimeoutTimer]; + [super close]; +} + +-(void) dealloc +{ + NSLog(@"STKAutoRecoveringHTTPDataSource dealloc"); + + self.innerDataSource.delegate = nil; + + [self stopNotifier]; + [self destroyTimeoutTimer]; + + [NSObject cancelPreviousPerformRequestsWithTarget:self]; + + if (reachabilityRef!= NULL) + { + CFRelease(reachabilityRef); + } +} + +-(void) reachabilityChanged +{ + if (waitingForNetwork) + { + waitingForNetwork = NO; + + NSLog(@"reachabilityChanged %lld/%lld", self.position, self.length); + + serial++; + + [self attemptReconnectWithSerial:@(serial)]; + } +} + +-(void) dataSourceDataAvailable:(STKDataSource*)dataSource +{ + if (![self.innerDataSource hasBytesAvailable]) + { + return; + } + + serial++; + waitSeconds = 1; + ticksWhenLastDataReceived = GetTickCount(); + + [super dataSourceDataAvailable:dataSource]; +} + +-(void) attemptReconnectWithSerial:(NSNumber*)serialIn +{ + if (serialIn.intValue != self->serial) + { + return; + } + + NSLog(@"attemptReconnect %lld/%lld", self.position, self.length); + + if (self.innerDataSource.eventsRunLoop) + { + [self.innerDataSource reconnect]; + } +} + +-(void) attemptReconnectWithTimer:(NSTimer*)timer +{ + [self attemptReconnectWithSerial:(NSNumber*)timer.userInfo]; +} + +-(void) processRetryOnError +{ + if (![self hasGotNetworkConnection]) + { + waitingForNetwork = YES; + + return; + } + + waitingForNetwork = NO; + + NSRunLoop* runLoop = self.innerDataSource.eventsRunLoop; + + if (runLoop == nil) + { + // DataSource no longer used + + return; + } + else + { + serial++; + + NSTimer* timer = [NSTimer timerWithTimeInterval:waitSeconds target:self selector:@selector(attemptReconnectWithTimer:) userInfo:@(serial) repeats:NO]; + + [runLoop addTimer:timer forMode:NSRunLoopCommonModes]; + } + + waitSeconds = MIN(waitSeconds + 1, 5); +} + +-(void) dataSourceEof:(STKDataSource*)dataSource +{ + NSLog(@"dataSourceEof"); + + if ([self position] < [self length]) + { + [self processRetryOnError]; + + return; + } + + [self.delegate dataSourceEof:self]; +} + +-(void) dataSourceErrorOccured:(STKDataSource*)dataSource +{ + NSLog(@"dataSourceErrorOccured"); + + if (self.innerDataSource.httpStatusCode == 416 /* Range out of bounds */) + { + [super dataSourceEof:dataSource]; + } + else + { + [self processRetryOnError]; + } +} + +-(NSString*) description +{ + return [NSString stringWithFormat:@"HTTP data source with file length: %lld and position: %lld", self.length, self.position]; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/STKCoreFoundationDataSource.h b/submodules/LegacyComponents/LegacyComponents/STKCoreFoundationDataSource.h new file mode 100755 index 0000000000..731de5d703 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/STKCoreFoundationDataSource.h @@ -0,0 +1,63 @@ +/********************************************************************************** + AudioPlayer.m + + Created by Thong Nguyen on 14/05/2012. + https://github.com/tumtumtum/audjustable + + Copyright (c) 2012 Thong Nguyen (tumtumtum@gmail.com). All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + 3. All advertising materials mentioning features or use of this software + must display the following acknowledgement: + This product includes software developed by Thong Nguyen (tumtumtum@gmail.com) + 4. Neither the name of Thong Nguyen nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY Thong Nguyen ''AS IS'' AND ANY + EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THONG NGUYEN BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +**********************************************************************************/ + +#import "STKDataSource.h" + +@class STKCoreFoundationDataSource; + +@interface CoreFoundationDataSourceClientInfo : NSObject +@property (readwrite) CFReadStreamRef readStreamRef; +@property (readwrite, retain) STKCoreFoundationDataSource* datasource; +@end + +@interface STKCoreFoundationDataSource : STKDataSource +{ +@protected + BOOL isInErrorState; + CFReadStreamRef stream; + NSRunLoop* eventsRunLoop; +} + +@property (readonly) BOOL isInErrorState; + +-(BOOL) reregisterForEvents; + +-(void) open; +-(void) openCompleted; +-(void) dataAvailable; +-(void) eof; +-(void) errorOccured; +-(CFStreamStatus) status; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/STKCoreFoundationDataSource.m b/submodules/LegacyComponents/LegacyComponents/STKCoreFoundationDataSource.m new file mode 100755 index 0000000000..1763acd57a --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/STKCoreFoundationDataSource.m @@ -0,0 +1,201 @@ +/********************************************************************************** + AudioPlayer.m + + Created by Thong Nguyen on 14/05/2012. + https://github.com/tumtumtum/audjustable + + Copyright (c) 2012 Thong Nguyen (tumtumtum@gmail.com). All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + 3. All advertising materials mentioning features or use of this software + must display the following acknowledgement: + This product includes software developed by Thong Nguyen (tumtumtum@gmail.com) + 4. Neither the name of Thong Nguyen nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY Thong Nguyen ''AS IS'' AND ANY + EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THONG NGUYEN BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +**********************************************************************************/ + +#import "STKCoreFoundationDataSource.h" + +static void ReadStreamCallbackProc(CFReadStreamRef stream, CFStreamEventType eventType, void* inClientInfo) +{ + STKCoreFoundationDataSource* datasource = (__bridge STKCoreFoundationDataSource*)inClientInfo; + + switch (eventType) + { + case kCFStreamEventErrorOccurred: + [datasource errorOccured]; + break; + case kCFStreamEventEndEncountered: + [datasource eof]; + break; + case kCFStreamEventHasBytesAvailable: + [datasource dataAvailable]; + break; + case kCFStreamEventOpenCompleted: + [datasource openCompleted]; + break; + default: + break; + } +} + +@implementation CoreFoundationDataSourceClientInfo +@synthesize readStreamRef, datasource; +@end + +@implementation STKCoreFoundationDataSource + +-(BOOL) isInErrorState +{ + return self->isInErrorState; +} + +-(void) dataAvailable +{ + [self.delegate dataSourceDataAvailable:self]; +} + +-(void) eof +{ + [self.delegate dataSourceEof:self]; +} + +-(void) errorOccured +{ + self->isInErrorState = YES; + + [self.delegate dataSourceErrorOccured:self]; +} + +-(void) dealloc +{ + if (stream) + { + if (eventsRunLoop) + { + [self unregisterForEvents]; + } + + [self close]; + + stream = 0; + } +} + +-(void) close +{ + if (stream) + { + if (eventsRunLoop) + { + [self unregisterForEvents]; + } + + CFReadStreamClose(stream); + CFRelease(stream); + + stream = 0; + } +} + +-(void) open +{ +} + +-(void) seekToOffset:(SInt64)offset +{ +} + +-(int) readIntoBuffer:(UInt8*)buffer withSize:(int)size +{ + return (int)CFReadStreamRead(stream, buffer, size); +} + +-(void) unregisterForEvents +{ + if (stream) + { + CFReadStreamSetClient(stream, kCFStreamEventHasBytesAvailable | kCFStreamEventErrorOccurred | kCFStreamEventEndEncountered, NULL, NULL); + CFReadStreamUnscheduleFromRunLoop(stream, [eventsRunLoop getCFRunLoop], kCFRunLoopCommonModes); + + eventsRunLoop = nil; + } +} + +-(BOOL) reregisterForEvents +{ + if (eventsRunLoop && stream) + { + CFStreamClientContext context = {0, (__bridge void*)self, NULL, NULL, NULL}; + CFReadStreamSetClient(stream, kCFStreamEventHasBytesAvailable | kCFStreamEventErrorOccurred | kCFStreamEventEndEncountered, ReadStreamCallbackProc, &context); + CFReadStreamScheduleWithRunLoop(stream, [eventsRunLoop getCFRunLoop], kCFRunLoopCommonModes); + + return YES; + } + + return NO; +} + +-(BOOL) registerForEvents:(NSRunLoop*)runLoop +{ + eventsRunLoop = runLoop; + + if (!stream) + { + // Will register when they open or seek + + return YES; + } + + CFStreamClientContext context = {0, (__bridge void*)self, NULL, NULL, NULL}; + + CFReadStreamSetClient(stream, kCFStreamEventHasBytesAvailable | kCFStreamEventErrorOccurred | kCFStreamEventEndEncountered, ReadStreamCallbackProc, &context); + + CFReadStreamScheduleWithRunLoop(stream, [eventsRunLoop getCFRunLoop], kCFRunLoopCommonModes); + + return YES; +} + +-(BOOL) hasBytesAvailable +{ + if (!stream) + { + return NO; + } + + return CFReadStreamHasBytesAvailable(stream); +} + +-(CFStreamStatus) status +{ + if (stream) + { + return CFReadStreamGetStatus(stream); + } + + return 0; +} + +-(void) openCompleted +{ +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/STKDataSource.h b/submodules/LegacyComponents/LegacyComponents/STKDataSource.h new file mode 100755 index 0000000000..69c296196f --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/STKDataSource.h @@ -0,0 +1,61 @@ +/********************************************************************************** + AudioPlayer.m + + Created by Thong Nguyen on 14/05/2012. + https://github.com/tumtumtum/audjustable + + Copyright (c) 2012 Thong Nguyen (tumtumtum@gmail.com). All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + 3. All advertising materials mentioning features or use of this software + must display the following acknowledgement: + This product includes software developed by Thong Nguyen (tumtumtum@gmail.com) + 4. Neither the name of Thong Nguyen nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY Thong Nguyen ''AS IS'' AND ANY + EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THONG NGUYEN BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +**********************************************************************************/ + +#import +#include + +@class STKDataSource; + +@protocol STKDataSourceDelegate +-(void) dataSourceDataAvailable:(STKDataSource*)dataSource; +-(void) dataSourceErrorOccured:(STKDataSource*)dataSource; +-(void) dataSourceEof:(STKDataSource*)dataSource; +@end + +@interface STKDataSource : NSObject + +@property (readonly) SInt64 position; +@property (readonly) SInt64 length; +@property (readonly) BOOL hasBytesAvailable; +@property (readwrite, unsafe_unretained) id delegate; + +-(BOOL) registerForEvents:(NSRunLoop*)runLoop; +-(void) unregisterForEvents; +-(void) close; + +-(void) seekToOffset:(SInt64)offset; +-(int) readIntoBuffer:(UInt8*)buffer withSize:(int)size; +-(AudioFileTypeID) audioFileTypeHint; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/STKDataSource.m b/submodules/LegacyComponents/LegacyComponents/STKDataSource.m new file mode 100755 index 0000000000..9abd8215a3 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/STKDataSource.m @@ -0,0 +1,82 @@ +/********************************************************************************** + AudioPlayer.m + + Created by Thong Nguyen on 14/05/2012. + https://github.com/tumtumtum/audjustable + + Copyright (c) 2012 Thong Nguyen (tumtumtum@gmail.com). All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + 3. All advertising materials mentioning features or use of this software + must display the following acknowledgement: + This product includes software developed by Thong Nguyen (tumtumtum@gmail.com) + 4. Neither the name of Thong Nguyen nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY Thong Nguyen ''AS IS'' AND ANY + EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THONG NGUYEN BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +**********************************************************************************/ + +#import "STKDataSource.h" + +@implementation STKDataSource +@synthesize delegate; + +-(SInt64) length +{ + return 0; +} + +-(void) seekToOffset:(SInt64)offset +{ +} + +-(int) readIntoBuffer:(UInt8*)buffer withSize:(int)size +{ + return -1; +} + +-(SInt64) position +{ + return 0; +} + +-(BOOL) registerForEvents:(NSRunLoop*)runLoop +{ + return NO; +} + +-(void) unregisterForEvents +{ +} + +-(void) close +{ +} + +-(BOOL) hasBytesAvailable +{ + return NO; +} + +-(AudioFileTypeID) audioFileTypeHint +{ + return 0; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/STKDataSourceWrapper.h b/submodules/LegacyComponents/LegacyComponents/STKDataSourceWrapper.h new file mode 100755 index 0000000000..4d3e035d5e --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/STKDataSourceWrapper.h @@ -0,0 +1,43 @@ +/********************************************************************************** + AudioPlayer.m + + Created by Thong Nguyen on 16/10/2012. + https://github.com/tumtumtum/audjustable + + Copyright (c) 2012 Thong Nguyen (tumtumtum@gmail.com). All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + 3. All advertising materials mentioning features or use of this software + must display the following acknowledgement: + This product includes software developed by Thong Nguyen (tumtumtum@gmail.com) + 4. Neither the name of Thong Nguyen nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY Thong Nguyen''AS IS'' AND ANY + EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **********************************************************************************/ + +#import "STKDataSource.h" + +@interface STKDataSourceWrapper : STKDataSource + +-(id) initWithDataSource:(STKDataSource*)innerDataSource; + +@property (readonly) STKDataSource* innerDataSource; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/STKDataSourceWrapper.m b/submodules/LegacyComponents/LegacyComponents/STKDataSourceWrapper.m new file mode 100755 index 0000000000..76e1634d91 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/STKDataSourceWrapper.m @@ -0,0 +1,120 @@ +/********************************************************************************** + AudioPlayer.m + + Created by Thong Nguyen on 16/10/2012. + https://github.com/tumtumtum/audjustable + + Copyright (c) 2012 Thong Nguyen (tumtumtum@gmail.com). All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + 3. All advertising materials mentioning features or use of this software + must display the following acknowledgement: + This product includes software developed by Thong Nguyen (tumtumtum@gmail.com) + 4. Neither the name of Thong Nguyen nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY Thong Nguyen''AS IS'' AND ANY + EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **********************************************************************************/ + +#import "STKDataSourceWrapper.h" + +@interface STKDataSourceWrapper() +@property (readwrite) STKDataSource* innerDataSource; +@end + +@implementation STKDataSourceWrapper + +-(id) initWithDataSource:(STKDataSource*)innerDataSourceIn +{ + if (self = [super init]) + { + self.innerDataSource = innerDataSourceIn; + + self.innerDataSource.delegate = self; + } + + return self; +} + +-(AudioFileTypeID) audioFileTypeHint +{ + return self.innerDataSource.audioFileTypeHint; +} + +-(void) dealloc +{ + self.innerDataSource.delegate = nil; +} + +-(SInt64) length +{ + return self.innerDataSource.length; +} + +-(void) seekToOffset:(SInt64)offset +{ + return [self.innerDataSource seekToOffset:offset]; +} + +-(int) readIntoBuffer:(UInt8*)buffer withSize:(int)size +{ + return [self.innerDataSource readIntoBuffer:buffer withSize:size]; +} + +-(SInt64) position +{ + return self.innerDataSource.position; +} + +-(BOOL) registerForEvents:(NSRunLoop*)runLoop +{ + return [self.innerDataSource registerForEvents:runLoop]; +} + +-(void) unregisterForEvents +{ + [self.innerDataSource unregisterForEvents]; +} + +-(void) close +{ + [self.innerDataSource close]; +} + +-(BOOL) hasBytesAvailable +{ + return self.innerDataSource.hasBytesAvailable; +} + +-(void) dataSourceDataAvailable:(STKDataSource*)dataSource +{ + [self.delegate dataSourceDataAvailable:self]; +} + +-(void) dataSourceErrorOccured:(STKDataSource*)dataSource +{ + [self.delegate dataSourceErrorOccured:self]; +} + +-(void) dataSourceEof:(STKDataSource*)dataSource +{ + [self.delegate dataSourceEof:self]; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/STKHTTPDataSource.h b/submodules/LegacyComponents/LegacyComponents/STKHTTPDataSource.h new file mode 100755 index 0000000000..628a62f6d6 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/STKHTTPDataSource.h @@ -0,0 +1,55 @@ +/********************************************************************************** + AudioPlayer.m + + Created by Thong Nguyen on 14/05/2012. + https://github.com/tumtumtum/audjustable + + Copyright (c) 2012 Thong Nguyen (tumtumtum@gmail.com). All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + 3. All advertising materials mentioning features or use of this software + must display the following acknowledgement: + This product includes software developed by Thong Nguyen (tumtumtum@gmail.com) + 4. Neither the name of Thong Nguyen nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY Thong Nguyen ''AS IS'' AND ANY + EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THONG NGUYEN BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **********************************************************************************/ + +#import "STKCoreFoundationDataSource.h" + +@class STKHTTPDataSource; + +typedef void(^STKURLBlock)(NSURL* url); +typedef NSURL*(^STKURLProvider)(); +typedef void(^STKAsyncURLProvider)(STKHTTPDataSource* dataSource, BOOL forSeek, STKURLBlock callback); + +@interface STKHTTPDataSource : STKCoreFoundationDataSource + +@property (readonly, retain) NSURL* url; +@property (readonly) UInt32 httpStatusCode; + ++(AudioFileTypeID) audioFileTypeHintFromMimeType:(NSString*)fileExtension; +-(id) initWithURL:(NSURL*)url; +-(id) initWithURLProvider:(STKURLProvider)urlProvider; +-(id) initWithAsyncURLProvider:(STKAsyncURLProvider)asyncUrlProvider; +-(NSRunLoop*) eventsRunLoop; +-(void) reconnect; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/STKHTTPDataSource.m b/submodules/LegacyComponents/LegacyComponents/STKHTTPDataSource.m new file mode 100755 index 0000000000..8a5f4d3406 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/STKHTTPDataSource.m @@ -0,0 +1,383 @@ +/********************************************************************************** + AudioPlayer.m + + Created by Thong Nguyen on 14/05/2012. + https://github.com/tumtumtum/audjustable + + Copyright (c) 2012 Thong Nguyen (tumtumtum@gmail.com). All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + 3. All advertising materials mentioning features or use of this software + must display the following acknowledgement: + This product includes software developed by Thong Nguyen (tumtumtum@gmail.com) + 4. Neither the name of Thong Nguyen nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY Thong Nguyen ''AS IS'' AND ANY + EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THONG NGUYEN BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **********************************************************************************/ + +#import "STKHTTPDataSource.h" +#import "STKLocalFileDataSource.h" + +@interface STKHTTPDataSource() +{ +@private + UInt32 httpStatusCode; + SInt64 seekStart; + SInt64 relativePosition; + SInt64 fileLength; + int discontinuous; + int requestSerialNumber; + + NSURL* currentUrl; + STKAsyncURLProvider asyncUrlProvider; + NSDictionary* httpHeaders; + AudioFileTypeID audioFileTypeHint; +} +-(void) open; + +@end + +@implementation STKHTTPDataSource + +-(id) initWithURL:(NSURL*)urlIn +{ + return [self initWithURLProvider:^NSURL* { return urlIn; }]; +} + +-(id) initWithURLProvider:(STKURLProvider)urlProviderIn +{ + urlProviderIn = [urlProviderIn copy]; + + return [self initWithAsyncURLProvider:^(STKHTTPDataSource* dataSource, BOOL forSeek, STKURLBlock block) + { + block(urlProviderIn()); + }]; +} + +-(id) initWithAsyncURLProvider:(STKAsyncURLProvider)asyncUrlProviderIn +{ + if (self = [super init]) + { + seekStart = 0; + relativePosition = 0; + fileLength = -1; + + self->asyncUrlProvider = [asyncUrlProviderIn copy]; + + audioFileTypeHint = [STKLocalFileDataSource audioFileTypeHintFromFileExtension:self->currentUrl.pathExtension]; + } + + return self; +} + +-(void) dealloc +{ + NSLog(@"STKHTTPDataSource dealloc"); +} + +-(NSURL*) url +{ + return self->currentUrl; +} + ++(AudioFileTypeID) audioFileTypeHintFromMimeType:(NSString*)mimeType +{ + static dispatch_once_t onceToken; + static NSDictionary* fileTypesByMimeType; + + dispatch_once(&onceToken, ^ + { + fileTypesByMimeType = + @{ + @"audio/mp3": @(kAudioFileMP3Type), + @"audio/mpg": @(kAudioFileMP3Type), + @"audio/mpeg": @(kAudioFileMP3Type), + @"audio/wav": @(kAudioFileWAVEType), + @"audio/aifc": @(kAudioFileAIFCType), + @"audio/aiff": @(kAudioFileAIFFType), + @"audio/x-m4a": @(kAudioFileM4AType), + @"audio/x-mp4": @(kAudioFileMPEG4Type), + @"audio/aacp": @(kAudioFileAAC_ADTSType), + @"audio/m4a": @(kAudioFileM4AType), + @"audio/mp4": @(kAudioFileMPEG4Type), + @"audio/caf": @(kAudioFileCAFType), + @"audio/aac": @(kAudioFileAAC_ADTSType), + @"audio/ac3": @(kAudioFileAC3Type), + @"audio/3gp": @(kAudioFile3GPType) + }; + }); + + NSNumber* number = [fileTypesByMimeType objectForKey:mimeType]; + + if (!number) + { + return 0; + } + + return (AudioFileTypeID)number.intValue; +} + +-(AudioFileTypeID) audioFileTypeHint +{ + return audioFileTypeHint; +} + +-(void) dataAvailable +{ + if (stream == NULL) { + return; + } + + if (self.httpStatusCode == 0) + { + CFTypeRef response = CFReadStreamCopyProperty(stream, kCFStreamPropertyHTTPResponseHeader); + + if (response) + { + httpHeaders = (__bridge_transfer NSDictionary*)CFHTTPMessageCopyAllHeaderFields((CFHTTPMessageRef)response); + + self->httpStatusCode = (UInt32)CFHTTPMessageGetResponseStatusCode((CFHTTPMessageRef)response); + + CFRelease(response); + } + + if (self.httpStatusCode == 200) + { + if (seekStart == 0) + { + fileLength = (SInt64)[[httpHeaders objectForKey:@"Content-Length"] longLongValue]; + } + + NSString* contentType = [httpHeaders objectForKey:@"Content-Type"]; + AudioFileTypeID typeIdFromMimeType = [STKHTTPDataSource audioFileTypeHintFromMimeType:contentType]; + + if (typeIdFromMimeType != 0) + { + audioFileTypeHint = typeIdFromMimeType; + } + } + else if (self.httpStatusCode == 206) + { + NSString* contentRange = [httpHeaders objectForKey:@"Content-Range"]; + NSArray* components = [contentRange componentsSeparatedByString:@"/"]; + + if (components.count == 2) + { + fileLength = [[components objectAtIndex:1] integerValue]; + } + } + else if (self.httpStatusCode == 416) + { + if (self.length >= 0) + { + seekStart = self.length; + } + + [self eof]; + + return; + } + else if (self.httpStatusCode >= 300) + { + [self errorOccured]; + + return; + } + } + + [super dataAvailable]; +} + +-(SInt64) position +{ + return seekStart + relativePosition; +} + +-(SInt64) length +{ + return fileLength >= 0 ? fileLength : 0; +} + +-(void) reconnect +{ + NSRunLoop* savedEventsRunLoop = eventsRunLoop; + + [self close]; + + eventsRunLoop = savedEventsRunLoop; + + [self seekToOffset:self.position]; +} + +-(void) seekToOffset:(SInt64)offset +{ + NSRunLoop* savedEventsRunLoop = eventsRunLoop; + + [self close]; + + eventsRunLoop = savedEventsRunLoop; + + NSAssert([NSRunLoop currentRunLoop] == eventsRunLoop, @"Seek called on wrong thread"); + + stream = 0; + relativePosition = 0; + seekStart = offset; + + self->isInErrorState = NO; + + [self openForSeek:YES]; +} + +-(int) readIntoBuffer:(UInt8*)buffer withSize:(int)size +{ + if (size == 0) + { + return 0; + } + + int read = (int)CFReadStreamRead(stream, buffer, size); + + if (read < 0) + { + return read; + } + + relativePosition += read; + + return read; +} + +-(void) open +{ + return [self openForSeek:NO]; +} + +-(void) openForSeek:(BOOL)forSeek +{ + int localRequestSerialNumber; + + requestSerialNumber++; + localRequestSerialNumber = requestSerialNumber; + + asyncUrlProvider(self, forSeek, ^(NSURL* url) + { + if (localRequestSerialNumber != self->requestSerialNumber) + { + return; + } + + self->currentUrl = url; + + if (url == nil) + { + return; + } + + CFHTTPMessageRef message = CFHTTPMessageCreateRequest(NULL, (CFStringRef)@"GET", (__bridge CFURLRef)self->currentUrl, kCFHTTPVersion1_1); + + if (seekStart > 0) + { + CFHTTPMessageSetHeaderFieldValue(message, CFSTR("Range"), (__bridge CFStringRef)[NSString stringWithFormat:@"bytes=%lld-", seekStart]); + + discontinuous = YES; + } + + stream = CFReadStreamCreateForHTTPRequest(NULL, message); + + if (stream == nil) + { + CFRelease(message); + + [self errorOccured]; + + return; + } + + if (!CFReadStreamSetProperty(stream, kCFStreamPropertyHTTPShouldAutoredirect, kCFBooleanTrue)) + { + CFRelease(message); + + [self errorOccured]; + + return; + } + + // Proxy support + + CFDictionaryRef proxySettings = CFNetworkCopySystemProxySettings(); + CFReadStreamSetProperty(stream, kCFStreamPropertyHTTPProxy, proxySettings); + CFRelease(proxySettings); + + // SSL support + + if ([self->currentUrl.scheme caseInsensitiveCompare:@"https"] == NSOrderedSame) + { + NSDictionary* sslSettings = [NSDictionary dictionaryWithObjectsAndKeys: + (NSString*)kCFStreamSocketSecurityLevelNegotiatedSSL, kCFStreamSSLLevel, + [NSNumber numberWithBool:YES], kCFStreamSSLAllowsExpiredCertificates, + [NSNumber numberWithBool:YES], kCFStreamSSLAllowsExpiredRoots, + [NSNumber numberWithBool:YES], kCFStreamSSLAllowsAnyRoot, + [NSNumber numberWithBool:NO], kCFStreamSSLValidatesCertificateChain, + [NSNull null], kCFStreamSSLPeerName, + nil]; + + CFReadStreamSetProperty(stream, kCFStreamPropertySSLSettings, (__bridge CFTypeRef)sslSettings); + } + + [self reregisterForEvents]; + + self->httpStatusCode = 0; + + // Open + + if (!CFReadStreamOpen(stream)) + { + CFRelease(stream); + CFRelease(message); + + stream = 0; + + [self errorOccured]; + + return; + } + + self->isInErrorState = NO; + + CFRelease(message); + }); +} + +-(UInt32) httpStatusCode +{ + return self->httpStatusCode; +} + +-(NSRunLoop*) eventsRunLoop +{ + return self->eventsRunLoop; +} + +-(NSString*) description +{ + return [NSString stringWithFormat:@"HTTP data source with file length: %lld and position: %lld", self.length, self.position]; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/STKLocalFileDataSource.h b/submodules/LegacyComponents/LegacyComponents/STKLocalFileDataSource.h new file mode 100755 index 0000000000..777ee52f66 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/STKLocalFileDataSource.h @@ -0,0 +1,43 @@ +/********************************************************************************** + AudioPlayer.m + + Created by Thong Nguyen on 14/05/2012. + https://github.com/tumtumtum/audjustable + + Copyright (c) 2012 Thong Nguyen (tumtumtum@gmail.com). All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + 3. All advertising materials mentioning features or use of this software + must display the following acknowledgement: + This product includes software developed by Thong Nguyen (tumtumtum@gmail.com) + 4. Neither the name of Thong Nguyen nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY Thong Nguyen ''AS IS'' AND ANY + EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THONG NGUYEN BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +**********************************************************************************/ + +#import "STKCoreFoundationDataSource.h" + +@interface STKLocalFileDataSource : STKCoreFoundationDataSource + ++(AudioFileTypeID) audioFileTypeHintFromFileExtension:(NSString*)fileExtension; +@property (atomic, readonly, copy) NSString* filePath; +-(id) initWithFilePath:(NSString*)filePath; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/STKLocalFileDataSource.m b/submodules/LegacyComponents/LegacyComponents/STKLocalFileDataSource.m new file mode 100755 index 0000000000..2aded28dd9 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/STKLocalFileDataSource.m @@ -0,0 +1,243 @@ +/********************************************************************************** + AudioPlayer.m + + Created by Thong Nguyen on 14/05/2012. + https://github.com/tumtumtum/audjustable + + Copyright (c) 2012 Thong Nguyen (tumtumtum@gmail.com). All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + 3. All advertising materials mentioning features or use of this software + must display the following acknowledgement: + This product includes software developed by Thong Nguyen (tumtumtum@gmail.com) + 4. Neither the name of Thong Nguyen nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY Thong Nguyen ''AS IS'' AND ANY + EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THONG NGUYEN BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +**********************************************************************************/ + +#import "STKLocalFileDataSource.h" + +@interface STKLocalFileDataSource() +{ + SInt64 position; + SInt64 length; + AudioFileTypeID audioFileTypeHint; +} +@property (readwrite, copy) NSString* filePath; +-(void) open; +@end + +@implementation STKLocalFileDataSource +@synthesize filePath; + +-(id) initWithFilePath:(NSString*)filePathIn +{ + if (self = [super init]) + { + self.filePath = filePathIn; + + audioFileTypeHint = [STKLocalFileDataSource audioFileTypeHintFromFileExtension:filePathIn.pathExtension]; + } + + return self; +} + ++(AudioFileTypeID) audioFileTypeHintFromFileExtension:(NSString*)fileExtension +{ + static dispatch_once_t onceToken; + static NSDictionary* fileTypesByFileExtensions; + + dispatch_once(&onceToken, ^ + { + fileTypesByFileExtensions = + @{ + @"mp3": @(kAudioFileMP3Type), + @"wav": @(kAudioFileWAVEType), + @"aifc": @(kAudioFileAIFCType), + @"aiff": @(kAudioFileAIFFType), + @"m4a": @(kAudioFileM4AType), + @"mp4": @(kAudioFileMPEG4Type), + @"caf": @(kAudioFileCAFType), + @"aac": @(kAudioFileAAC_ADTSType), + @"ac3": @(kAudioFileAC3Type), + @"3gp": @(kAudioFile3GPType) + }; + }); + + NSNumber* number = [fileTypesByFileExtensions objectForKey:fileExtension]; + + if (!number) + { + return 0; + } + + return (AudioFileTypeID)number.intValue; +} + +-(AudioFileTypeID) audioFileTypeHint +{ + return audioFileTypeHint; +} + +-(void) dealloc +{ + [self close]; +} + +-(void) close +{ + if (stream) + { + [self unregisterForEvents]; + + CFReadStreamClose(stream); + + stream = 0; + } +} + +-(void) open +{ + if (stream) + { + [self unregisterForEvents]; + + CFReadStreamClose(stream); + CFRelease(stream); + + stream = 0; + } + + NSURL* url = [[NSURL alloc] initFileURLWithPath:self.filePath]; + + stream = CFReadStreamCreateWithFile(NULL, (__bridge CFURLRef)url); + + NSError* fileError; + NSFileManager* manager = [[NSFileManager alloc] init]; + NSDictionary* attributes = [manager attributesOfItemAtPath:filePath error:&fileError]; + + if (fileError) + { + CFReadStreamClose(stream); + CFRelease(stream); + stream = 0; + return; + } + + NSNumber* number = [attributes objectForKey:@"NSFileSize"]; + + if (number) + { + length = number.longLongValue; + } + + [self reregisterForEvents]; + + CFReadStreamOpen(stream); +} + +-(SInt64) position +{ + return position; +} + +-(SInt64) length +{ + return length; +} + +-(int) readIntoBuffer:(UInt8*)buffer withSize:(int)size +{ + int retval = (int)CFReadStreamRead(stream, buffer, size); + + if (retval > 0) + { + position += retval; + } + else + { + NSNumber* property = (__bridge_transfer NSNumber*)CFReadStreamCopyProperty(stream, kCFStreamPropertyFileCurrentOffset); + + position = property.longLongValue; + } + + return retval; +} + +-(void) seekToOffset:(SInt64)offset +{ + CFStreamStatus status = kCFStreamStatusClosed; + + if (stream != 0) + { + status = CFReadStreamGetStatus(stream); + } + + BOOL reopened = NO; + + if (status == kCFStreamStatusAtEnd || status == kCFStreamStatusClosed || status == kCFStreamStatusError) + { + reopened = YES; + + [self close]; + [self open]; + } + + if (stream == 0) + { + CFRunLoopPerformBlock(eventsRunLoop.getCFRunLoop, NSRunLoopCommonModes, ^ + { + [self errorOccured]; + }); + + CFRunLoopWakeUp(eventsRunLoop.getCFRunLoop); + + return; + } + + if (CFReadStreamSetProperty(stream, kCFStreamPropertyFileCurrentOffset, (__bridge CFTypeRef)[NSNumber numberWithLongLong:offset]) != TRUE) + { + position = 0; + } + else + { + position = offset; + } + + if (!reopened) + { + CFRunLoopPerformBlock(eventsRunLoop.getCFRunLoop, NSRunLoopCommonModes, ^ + { + if ([self hasBytesAvailable]) + { + [self dataAvailable]; + } + }); + + CFRunLoopWakeUp(eventsRunLoop.getCFRunLoop); + } +} + +-(NSString*) description +{ + return self->filePath; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/STKQueueEntry.h b/submodules/LegacyComponents/LegacyComponents/STKQueueEntry.h new file mode 100755 index 0000000000..78e3784276 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/STKQueueEntry.h @@ -0,0 +1,45 @@ +// +// STKQueueEntry.h +// StreamingKit +// +// Created by Thong Nguyen on 30/01/2014. +// Copyright (c) 2014 Thong Nguyen. All rights reserved. +// + +#import "STKDataSource.h" +#import "libkern/OSAtomic.h" +#import "AudioToolbox/AudioToolbox.h" + +@interface STKQueueEntry : NSObject +{ +@public + OSSpinLock spinLock; + + BOOL parsedHeader; + Float64 sampleRate; + double packetDuration; + UInt64 audioDataOffset; + UInt64 audioDataByteCount; + UInt32 packetBufferSize; + volatile Float64 seekTime; + volatile SInt64 framesQueued; + volatile SInt64 framesPlayed; + volatile SInt64 lastFrameQueued; + volatile int processedPacketsCount; + volatile int processedPacketsSizeTotal; + AudioStreamBasicDescription audioStreamBasicDescription; +} + +@property (readonly) UInt64 audioDataLengthInBytes; +@property (readwrite, retain) NSObject* queueItemId; +@property (readwrite, retain) STKDataSource* dataSource; + +-(id) initWithDataSource:(STKDataSource*)dataSource andQueueItemId:(NSObject*)queueItemId; + +-(void) reset; +-(double) duration; +-(Float64) progressInFrames; +-(double) calculatedBitRate; +-(BOOL) isDefinitelyCompatible:(AudioStreamBasicDescription*)basicDescription; + +@end \ No newline at end of file diff --git a/submodules/LegacyComponents/LegacyComponents/STKQueueEntry.m b/submodules/LegacyComponents/LegacyComponents/STKQueueEntry.m new file mode 100755 index 0000000000..2b6de04a3b --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/STKQueueEntry.m @@ -0,0 +1,121 @@ +// +// STKQueueEntry.m +// StreamingKit +// +// Created by Thong Nguyen on 30/01/2014. +// Copyright (c) 2014 Thong Nguyen. All rights reserved. +// + +#import "STKQueueEntry.h" +#import "STKDataSource.h" + +#define STK_BIT_RATE_ESTIMATION_MIN_PACKETS_MIN (2) +#define STK_BIT_RATE_ESTIMATION_MIN_PACKETS_PREFERRED (64) + +@implementation STKQueueEntry + +-(id) initWithDataSource:(STKDataSource*)dataSourceIn andQueueItemId:(NSObject*)queueItemIdIn +{ + if (self = [super init]) + { + self->spinLock = OS_SPINLOCK_INIT; + + self.dataSource = dataSourceIn; + self.queueItemId = queueItemIdIn; + self->lastFrameQueued = -1; + } + + return self; +} + +-(void) reset +{ + OSSpinLockLock(&self->spinLock); + self->framesQueued = 0; + self->framesPlayed = 0; + self->lastFrameQueued = -1; + OSSpinLockUnlock(&self->spinLock); +} + +-(double) calculatedBitRate +{ + double retval; + + if (packetDuration > 0) + { + if (processedPacketsCount > STK_BIT_RATE_ESTIMATION_MIN_PACKETS_PREFERRED || (audioStreamBasicDescription.mBytesPerFrame == 0 && processedPacketsCount > STK_BIT_RATE_ESTIMATION_MIN_PACKETS_MIN)) + { + double averagePacketByteSize = processedPacketsSizeTotal / processedPacketsCount; + + retval = averagePacketByteSize / packetDuration * 8; + + return retval; + } + } + + retval = (audioStreamBasicDescription.mBytesPerFrame * audioStreamBasicDescription.mSampleRate) * 8; + + return retval; +} + +-(double) duration +{ + if (self->sampleRate <= 0) + { + return 0; + } + + UInt64 audioDataLengthInBytes = [self audioDataLengthInBytes]; + + double calculatedBitRate = [self calculatedBitRate]; + + if (calculatedBitRate < 1.0 || self.dataSource.length == 0) + { + return 0; + } + + return audioDataLengthInBytes / (calculatedBitRate / 8); +} + +-(UInt64) audioDataLengthInBytes +{ + if (audioDataByteCount) + { + return audioDataByteCount; + } + else + { + if (!self.dataSource.length) + { + return 0; + } + + return self.dataSource.length - audioDataOffset; + } +} + +-(BOOL) isDefinitelyCompatible:(AudioStreamBasicDescription*)basicDescription +{ + if (self->audioStreamBasicDescription.mSampleRate == 0) + { + return NO; + } + + return (memcmp(&(self->audioStreamBasicDescription), basicDescription, sizeof(*basicDescription)) == 0); +} + +-(Float64) progressInFrames +{ + OSSpinLockLock(&self->spinLock); + Float64 retval = self->seekTime + self->framesPlayed; + OSSpinLockUnlock(&self->spinLock); + + return retval; +} + +-(NSString*) description +{ + return [[self queueItemId] description]; +} + +@end \ No newline at end of file diff --git a/submodules/LegacyComponents/LegacyComponents/TGActionMediaAttachment.h b/submodules/LegacyComponents/LegacyComponents/TGActionMediaAttachment.h new file mode 100644 index 0000000000..0143a2c40e --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGActionMediaAttachment.h @@ -0,0 +1,50 @@ + + +#import + +#define TGActionMediaAttachmentType 0x1167E28B + +typedef enum { + TGMessageActionNone = 0, + TGMessageActionChatEditTitle = 1, + TGMessageActionChatAddMember = 2, + TGMessageActionChatDeleteMember = 3, + TGMessageActionCreateChat = 4, + TGMessageActionChatEditPhoto = 5, + TGMessageActionContactRequest = 6, + TGMessageActionAcceptContactRequest = 7, + TGMessageActionContactRegistered = 8, + TGMessageActionUserChangedPhoto = 9, + TGMessageActionEncryptedChatRequest = 10, + TGMessageActionEncryptedChatAccept = 11, + TGMessageActionEncryptedChatDecline = 12, + TGMessageActionEncryptedChatMessageLifetime = 13, + TGMessageActionEncryptedChatScreenshot = 14, + TGMessageActionEncryptedChatMessageScreenshot = 15, + TGMessageActionCreateBroadcastList = 16, + TGMessageActionJoinedByLink = 17, + TGMessageActionChannelCreated = 18, + TGMessageActionChannelCommentsStatusChanged = 19, + TGMessageActionChannelInviter = 20, + TGMessageActionGroupMigratedTo = 21, + TGMessageActionGroupDeactivated = 22, + TGMessageActionGroupActivated = 23, + TGMessageActionChannelMigratedFrom = 24, + TGMessageActionPinnedMessage = 25, + TGMessageActionClearChat = 26, + TGMessageActionGameScore = 27, + TGMessageActionPhoneCall = 28, + TGMessageActionPaymentSent = 29, + TGMessageActionText = 30, + TGMessageActionGroupCall = 31, + TGMessageActionBotAllowed = 32, + TGMessageActionSecureValuesSent = 33, + TGMessageActionCustom = 1000 +} TGMessageAction; + +@interface TGActionMediaAttachment : TGMediaAttachment + +@property (nonatomic) TGMessageAction actionType; +@property (nonatomic, strong) NSDictionary *actionData; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGActionMediaAttachment.m b/submodules/LegacyComponents/LegacyComponents/TGActionMediaAttachment.m new file mode 100644 index 0000000000..038eb02e36 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGActionMediaAttachment.m @@ -0,0 +1,431 @@ +#import "TGActionMediaAttachment.h" + +#import "TGImageMediaAttachment.h" + +@implementation TGActionMediaAttachment + +- (id)init +{ + self = [super init]; + if (self != nil) + { + self.type = TGActionMediaAttachmentType; + } + return self; +} + +- (void)serialize:(NSMutableData *)data +{ + int dataLengthPtr = (int)data.length; + int zero = 0; + [data appendBytes:&zero length:4]; + + int actionType = _actionType; + [data appendBytes:&actionType length:4]; + + if (actionType == TGMessageActionChatAddMember || actionType == TGMessageActionChatDeleteMember) + { + int uid = [[_actionData objectForKey:@"uid"] intValue]; + [data appendBytes:&uid length:4]; + NSArray *uids = _actionData[@"uids"]; + int32_t uidsCount = (int32_t)uids.count; + [data appendBytes:&uidsCount length:4]; + for (NSNumber *nUid in uids) { + int32_t listUid = [nUid intValue]; + [data appendBytes:&listUid length:4]; + } + } + else if (actionType == TGMessageActionJoinedByLink) + { + int uid = [[_actionData objectForKey:@"inviterId"] intValue]; + [data appendBytes:&uid length:4]; + } + else if (actionType == TGMessageActionChatEditTitle) + { + NSString *title = [_actionData objectForKey:@"title"]; + NSData *titleData = [title dataUsingEncoding:NSUTF8StringEncoding]; + int length = (int)titleData.length; + [data appendBytes:&length length:4]; + [data appendData:titleData]; + } + else if (actionType == TGMessageActionCreateChat) + { + NSString *title = [_actionData objectForKey:@"title"]; + NSData *titleData = [title dataUsingEncoding:NSUTF8StringEncoding]; + int length = (int)titleData.length; + [data appendBytes:&length length:4]; + [data appendData:titleData]; + + NSArray *uids = [_actionData objectForKey:@"uids"]; + int count = (int)uids.count; + [data appendBytes:&count length:4]; + for (NSNumber *nUid in uids) + { + int uid = [nUid intValue]; + [data appendBytes:&uid length:4]; + } + } + else if (actionType == TGMessageActionCreateBroadcastList) + { + NSString *title = [_actionData objectForKey:@"title"]; + NSData *titleData = [title dataUsingEncoding:NSUTF8StringEncoding]; + int length = (int)titleData.length; + [data appendBytes:&length length:4]; + [data appendData:titleData]; + + NSArray *uids = [_actionData objectForKey:@"uids"]; + int count = (int)uids.count; + [data appendBytes:&count length:4]; + for (NSNumber *nUid in uids) + { + int uid = [nUid intValue]; + [data appendBytes:&uid length:4]; + } + } + else if (actionType == TGMessageActionChatEditPhoto) + { + TGImageMediaAttachment *photo = [_actionData objectForKey:@"photo"]; + if (photo != nil) + { + [photo serialize:data]; + } + } + else if (actionType == TGMessageActionContactRequest) + { + int hasPhone = [[_actionData objectForKey:@"hasPhone"] boolValue] ? 1 : 0; + [data appendBytes:&hasPhone length:4]; + } + else if (actionType == TGMessageActionAcceptContactRequest) + { + } + else if (actionType == TGMessageActionContactRegistered) + { + } + else if (actionType == TGMessageActionUserChangedPhoto) + { + TGImageMediaAttachment *photo = [_actionData objectForKey:@"photo"]; + if (photo != nil) + { + [photo serialize:data]; + } + } + else if (actionType == TGMessageActionEncryptedChatRequest) + { + } + else if (actionType == TGMessageActionEncryptedChatAccept) + { + + } + else if (actionType == TGMessageActionEncryptedChatDecline) + { + + } + else if (actionType == TGMessageActionEncryptedChatMessageLifetime) + { + int32_t messageLifetime = [_actionData[@"messageLifetime"] intValue]; + [data appendBytes:&messageLifetime length:4]; + } + else if (actionType == TGMessageActionEncryptedChatScreenshot) + { + } + else if (actionType == TGMessageActionChannelCreated) + { + NSString *title = [_actionData objectForKey:@"title"]; + NSData *titleData = [title dataUsingEncoding:NSUTF8StringEncoding]; + int length = (int)titleData.length; + [data appendBytes:&length length:4]; + [data appendData:titleData]; + } + else if (actionType == TGMessageActionChannelCommentsStatusChanged) { + uint8_t enabled = [_actionData[@"enabled"] boolValue]; + [data appendBytes:&enabled length:1]; + } else if (actionType == TGMessageActionChannelInviter) { + int32_t inviter = [_actionData[@"uid"] intValue]; + [data appendBytes:&inviter length:4]; + } else if (actionType == TGMessageActionGroupMigratedTo) { + int32_t channelId = [_actionData[@"channelId"] intValue]; + [data appendBytes:&channelId length:4]; + } else if (actionType == TGMessageActionGroupDeactivated) { + + } else if (actionType == TGMessageActionGroupActivated) { + + } else if (actionType == TGMessageActionChannelMigratedFrom) { + NSString *title = [_actionData objectForKey:@"title"]; + NSData *titleData = [title dataUsingEncoding:NSUTF8StringEncoding]; + int length = (int)titleData.length; + [data appendBytes:&length length:4]; + [data appendData:titleData]; + + int32_t channelId = [_actionData[@"groupId"] intValue]; + [data appendBytes:&channelId length:4]; + } else if (actionType == TGMessageActionPinnedMessage) { + } else if (actionType == TGMessageActionClearChat) { + } else if (actionType == TGMessageActionGameScore) { + int32_t gameId = [_actionData[@"gameId"] intValue]; + [data appendBytes:&gameId length:4]; + int32_t score = [_actionData[@"score"] intValue]; + [data appendBytes:&score length:4]; + } else if (actionType == TGMessageActionPhoneCall) { + int64_t callId = [_actionData[@"callId"] longLongValue]; + [data appendBytes:&callId length:8]; + int32_t reason = [_actionData[@"reason"] intValue]; + [data appendBytes:&reason length:4]; + int32_t duration = [_actionData[@"duration"] intValue]; + [data appendBytes:&duration length:4]; + } else if (actionType == TGMessageActionPaymentSent) { + NSString *curreny = _actionData[@"currency"]; + NSData *currencyBytes = [curreny dataUsingEncoding:NSUTF8StringEncoding]; + int32_t currencyLength = (int32_t)currencyBytes.length; + [data appendBytes:¤cyLength length:4]; + [data appendData:currencyBytes]; + int32_t totalAmount = [_actionData[@"totalAmount"] intValue]; + [data appendBytes:&totalAmount length:4]; + } else if (actionType == TGMessageActionText) { + NSString *text = _actionData[@"text"]; + NSData *textBytes = [text dataUsingEncoding:NSUTF8StringEncoding]; + int32_t textLength = (int32_t)textBytes.length; + [data appendBytes:&textLength length:4]; + [data appendData:textBytes]; + } else if (actionType == TGMessageActionBotAllowed) { + NSString *domain = _actionData[@"domain"]; + NSData *domainBytes = [domain dataUsingEncoding:NSUTF8StringEncoding]; + int32_t domainLength = (int32_t)domainBytes.length; + [data appendBytes:&domainLength length:4]; + [data appendData:domainBytes]; + } else if (actionType == TGMessageActionSecureValuesSent) { + NSString *values = _actionData[@"values"]; + NSData *valuesBytes = [values dataUsingEncoding:NSUTF8StringEncoding]; + int32_t valuesLength = (int32_t)valuesBytes.length; + [data appendBytes:&valuesLength length:4]; + [data appendData:valuesBytes]; + } + + int dataLength = (int)data.length - dataLengthPtr - 4; + [data replaceBytesInRange:NSMakeRange(dataLengthPtr, 4) withBytes:&dataLength]; +} + +- (TGMediaAttachment *)parseMediaAttachment:(NSInputStream *)is +{ + int dataLength = 0; + [is read:(uint8_t *)&dataLength maxLength:4]; + + TGActionMediaAttachment *actionAttachment = [[TGActionMediaAttachment alloc] init]; + + int actionType = 0; + [is read:(uint8_t *)&actionType maxLength:4]; + dataLength -= 4; + actionAttachment.actionType = (TGMessageAction)actionType; + + if (actionType == TGMessageActionChatAddMember || actionType == TGMessageActionChatDeleteMember) + { + int uid = 0; + [is read:(uint8_t *)&uid maxLength:4]; + dataLength -= 4; + + NSMutableArray *uids = [[NSMutableArray alloc] init]; + if (dataLength >= 4) { + int32_t uidsCount = 0; + [is read:(uint8_t *)&uidsCount maxLength:4]; + dataLength -= 4; + + for (int32_t i = 0; dataLength > 0 && i < uidsCount; i++) { + int32_t listUid = 0; + [is read:(uint8_t *)&listUid maxLength:4]; + [uids addObject:@(listUid)]; + dataLength -= 4; + } + } + + if (uids.count != 0) { + actionAttachment.actionData = @{@"uid": @(uid), @"uids": uids}; + } else { + actionAttachment.actionData = @{@"uid": @(uid)}; + } + } + else if (actionType == TGMessageActionJoinedByLink) + { + int uid = 0; + [is read:(uint8_t *)&uid maxLength:4]; + actionAttachment.actionData = [NSDictionary dictionaryWithObject:[NSNumber numberWithInt:uid] forKey:@"inviterId"]; + } + else if (actionType == TGMessageActionChatEditTitle) + { + int length = 0; + [is read:(uint8_t *)&length maxLength:4]; + uint8_t *titleBytes = malloc(length); + [is read:titleBytes maxLength:length]; + NSString *title = [[NSString alloc] initWithBytesNoCopy:titleBytes length:length encoding:NSUTF8StringEncoding freeWhenDone:true]; + actionAttachment.actionData = [NSDictionary dictionaryWithObject:(title == nil ? @"" : title) forKey:@"title"]; + } + else if (actionType == TGMessageActionCreateChat) + { + int length = 0; + [is read:(uint8_t *)&length maxLength:4]; + uint8_t *titleBytes = malloc(length); + [is read:titleBytes maxLength:length]; + NSString *title = [[NSString alloc] initWithBytesNoCopy:titleBytes length:length encoding:NSUTF8StringEncoding freeWhenDone:true]; + + int count = 0; + [is read:(uint8_t *)&count maxLength:4]; + NSMutableArray *uids = [[NSMutableArray alloc] initWithCapacity:count]; + for (int i = 0; i < count; i++) + { + int uid = 0; + [is read:(uint8_t *)&uid maxLength:4]; + if (uid != 0) + [uids addObject:[[NSNumber alloc] initWithInt:uid]]; + } + + actionAttachment.actionData = [[NSDictionary alloc] initWithObjectsAndKeys:(title == nil ? @"" : title), @"title", uids, @"uids", nil]; + } + else if (actionType == TGMessageActionCreateBroadcastList) + { + int length = 0; + [is read:(uint8_t *)&length maxLength:4]; + uint8_t *titleBytes = malloc(length); + [is read:titleBytes maxLength:length]; + NSString *title = [[NSString alloc] initWithBytesNoCopy:titleBytes length:length encoding:NSUTF8StringEncoding freeWhenDone:true]; + + int count = 0; + [is read:(uint8_t *)&count maxLength:4]; + NSMutableArray *uids = [[NSMutableArray alloc] initWithCapacity:count]; + for (int i = 0; i < count; i++) + { + int uid = 0; + [is read:(uint8_t *)&uid maxLength:4]; + if (uid != 0) + [uids addObject:[[NSNumber alloc] initWithInt:uid]]; + } + + actionAttachment.actionData = [[NSDictionary alloc] initWithObjectsAndKeys:(title == nil ? @"" : title), @"title", uids, @"uids", nil]; + } + else if (actionType == TGMessageActionChatEditPhoto) + { + TGImageMediaAttachment *photo = (TGImageMediaAttachment *)[[[TGImageMediaAttachment alloc] init] parseMediaAttachment:is]; + if (photo != nil) + actionAttachment.actionData = [[NSDictionary alloc] initWithObjectsAndKeys:photo, @"photo", nil]; + } + else if (actionType == TGMessageActionContactRequest) + { + int hasPhone = 0; + [is read:(uint8_t *)&hasPhone maxLength:4]; + actionAttachment.actionData = [[NSDictionary alloc] initWithObjectsAndKeys:[[NSNumber alloc] initWithBool:hasPhone != 0], @"hasPhone", nil]; + } + else if (actionType == TGMessageActionAcceptContactRequest) + { + } + else if (actionType == TGMessageActionContactRegistered) + { + } + else if (actionType == TGMessageActionUserChangedPhoto) + { + TGImageMediaAttachment *photo = (TGImageMediaAttachment *)[[[TGImageMediaAttachment alloc] init] parseMediaAttachment:is]; + if (photo != nil) + actionAttachment.actionData = [[NSDictionary alloc] initWithObjectsAndKeys:photo, @"photo", nil]; + } + else if (actionType == TGMessageActionEncryptedChatRequest) + { + } + else if (actionType == TGMessageActionEncryptedChatAccept) + { + } + else if (actionType == TGMessageActionEncryptedChatDecline) + { + } + else if (actionType == TGMessageActionEncryptedChatMessageLifetime) + { + int32_t messageLifetime = 0; + [is read:(uint8_t *)&messageLifetime maxLength:4]; + actionAttachment.actionData = [[NSDictionary alloc] initWithObjectsAndKeys:[[NSNumber alloc] initWithInt:messageLifetime], @"messageLifetime", nil]; + } + else if (actionType == TGMessageActionEncryptedChatScreenshot) + { + } + else if (actionType == TGMessageActionChannelCreated) + { + int length = 0; + [is read:(uint8_t *)&length maxLength:4]; + uint8_t *titleBytes = malloc(length); + [is read:titleBytes maxLength:length]; + NSString *title = [[NSString alloc] initWithBytesNoCopy:titleBytes length:length encoding:NSUTF8StringEncoding freeWhenDone:true]; + actionAttachment.actionData = [NSDictionary dictionaryWithObject:(title == nil ? @"" : title) forKey:@"title"]; + } + else if (actionType == TGMessageActionChannelCommentsStatusChanged) { + uint8_t enabled = 0; + [is read:(uint8_t *)&enabled maxLength:1]; + actionAttachment.actionData = @{@"enabled": @(enabled != 0)}; + } else if (actionType == TGMessageActionChannelInviter) { + int32_t uid = 0; + [is read:(uint8_t *)&uid maxLength:4]; + actionAttachment.actionData = @{@"uid": @(uid)}; + } else if (actionType == TGMessageActionGroupMigratedTo) { + int32_t channelId = 0; + [is read:(uint8_t *)&channelId maxLength:4]; + actionAttachment.actionData = @{@"channelId": @(channelId)}; + } else if (actionType == TGMessageActionChannelMigratedFrom) { + int length = 0; + [is read:(uint8_t *)&length maxLength:4]; + uint8_t *titleBytes = malloc(length); + [is read:titleBytes maxLength:length]; + NSString *title = [[NSString alloc] initWithBytesNoCopy:titleBytes length:length encoding:NSUTF8StringEncoding freeWhenDone:true]; + + int32_t groupId = 0; + [is read:(uint8_t *)&groupId maxLength:4]; + + actionAttachment.actionData = @{@"groupId": @(groupId), @"title": title}; + } else if (actionType == TGMessageActionPinnedMessage) { + } else if (actionType == TGMessageActionClearChat) { + } else if (actionType == TGMessageActionGameScore) { + int gameId = 0; + [is read:(uint8_t *)&gameId maxLength:4]; + int score = 0; + [is read:(uint8_t *)&score maxLength:4]; + actionAttachment.actionData = @{@"gameId": @(gameId), @"score": @(score)}; + } else if (actionType == TGMessageActionPhoneCall) { + int64_t callId = 0; + [is read:(uint8_t *)&callId maxLength:8]; + int32_t reason = 0; + [is read:(uint8_t *)&reason maxLength:4]; + int32_t duration = 0; + [is read:(uint8_t *)&duration maxLength:4]; + actionAttachment.actionData = @{@"callId": @(callId), @"reason": @(reason), @"duration": @(duration)}; + } else if (actionType == TGMessageActionPaymentSent) { + int32_t currencyLength = 0; + [is read:(uint8_t *)¤cyLength maxLength:4]; + uint8_t *titleBytes = malloc(currencyLength); + [is read:titleBytes maxLength:currencyLength]; + NSString *title = [[NSString alloc] initWithBytesNoCopy:titleBytes length:currencyLength encoding:NSUTF8StringEncoding freeWhenDone:true]; + int32_t totalAmount = 0; + [is read:(uint8_t *)&totalAmount maxLength:4]; + actionAttachment.actionData = @{@"currency": title, @"totalAmount": @(totalAmount)}; + } else if (actionType == TGMessageActionText) { + int32_t textLength = 0; + [is read:(uint8_t *)&textLength maxLength:4]; + uint8_t *titleBytes = malloc(textLength); + [is read:titleBytes maxLength:textLength]; + NSString *title = [[NSString alloc] initWithBytesNoCopy:titleBytes length:textLength encoding:NSUTF8StringEncoding freeWhenDone:true]; + actionAttachment.actionData = @{@"text": title}; + } else if (actionType == TGMessageActionBotAllowed) { + int32_t domainLength = 0; + [is read:(uint8_t *)&domainLength maxLength:4]; + uint8_t *domainBytes = malloc(domainLength); + [is read:domainBytes maxLength:domainLength]; + NSString *domain = [[NSString alloc] initWithBytesNoCopy:domainBytes length:domainLength encoding:NSUTF8StringEncoding freeWhenDone:true]; + if (domain == nil) + domain = @""; + actionAttachment.actionData = @{@"domain": domain}; + } else if (actionType == TGMessageActionSecureValuesSent) { + int32_t valuesLength = 0; + [is read:(uint8_t *)&valuesLength maxLength:4]; + uint8_t *valuesBytes = malloc(valuesLength); + [is read:valuesBytes maxLength:valuesLength]; + NSString *values = [[NSString alloc] initWithBytesNoCopy:valuesBytes length:valuesLength encoding:NSUTF8StringEncoding freeWhenDone:true]; + if (values == nil) + values = @""; + actionAttachment.actionData = @{@"values": values}; + } + + return actionAttachment; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGAlphacode.h b/submodules/LegacyComponents/LegacyComponents/TGAlphacode.h new file mode 100644 index 0000000000..2b02e50306 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGAlphacode.h @@ -0,0 +1,12 @@ +#import + +@class SSignal; + +@interface TGAlphacodeEntry : NSObject + +@property (nonatomic, strong, readonly) NSString *emoji; +@property (nonatomic, strong, readonly) NSString *code; + +- (instancetype)initWithEmoji:(NSString *)emoji code:(NSString *)code; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGAlphacode.m b/submodules/LegacyComponents/LegacyComponents/TGAlphacode.m new file mode 100644 index 0000000000..18e4db65dc --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGAlphacode.m @@ -0,0 +1,14 @@ +#import "TGAlphacode.h" + +@implementation TGAlphacodeEntry + +- (instancetype)initWithEmoji:(NSString *)emoji code:(NSString *)code { + self = [super init]; + if (self != nil) { + _emoji = emoji; + _code = code; + } + return self; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGAlphacodePanelCell.h b/submodules/LegacyComponents/LegacyComponents/TGAlphacodePanelCell.h new file mode 100644 index 0000000000..9c169a2221 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGAlphacodePanelCell.h @@ -0,0 +1,14 @@ +#import + +extern NSString *const TGAlphacodePanelCellKind; + +@class TGConversationAssociatedInputPanelPallete; + +@interface TGAlphacodePanelCell : UITableViewCell + +@property (nonatomic, strong) TGConversationAssociatedInputPanelPallete *pallete; + +- (instancetype)initWithStyle:(TGModernConversationAssociatedInputPanelStyle)style; +- (void)setEmoji:(NSString *)emoji label:(NSString *)label; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGAlphacodePanelCell.m b/submodules/LegacyComponents/LegacyComponents/TGAlphacodePanelCell.m new file mode 100644 index 0000000000..7845f7c863 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGAlphacodePanelCell.m @@ -0,0 +1,110 @@ +#import "TGAlphacodePanelCell.h" + +#import "LegacyComponentsInternal.h" +#import "TGColor.h" +#import "TGFont.h" + +#import "TGModernConversationAssociatedInputPanel.h" + +NSString *const TGAlphacodePanelCellKind = @"TGAlphacodePanelCell"; + +@interface TGAlphacodePanelCell () { + UILabel *_emojiLabel; + UILabel *_descriptionLabel; +} + +@end + +@implementation TGAlphacodePanelCell + +- (instancetype)initWithStyle:(TGModernConversationAssociatedInputPanelStyle)style { + self = [super initWithStyle:UITableViewCellStyleDefault reuseIdentifier:TGAlphacodePanelCellKind]; + if (self != nil) { + UIColor *backgroundColor = [UIColor whiteColor]; + UIColor *nameColor = [UIColor blackColor]; + UIColor *usernameColor = [UIColor blackColor]; + UIColor *selectionColor = TGSelectionColor(); + + if (style == TGModernConversationAssociatedInputPanelDarkStyle) + { + backgroundColor = UIColorRGB(0x171717); + nameColor = [UIColor whiteColor]; + usernameColor = UIColorRGB(0x828282); + selectionColor = UIColorRGB(0x292929); + } + else if (style == TGModernConversationAssociatedInputPanelDarkBlurredStyle) + { + backgroundColor = [UIColor clearColor]; + nameColor = [UIColor whiteColor]; + usernameColor = UIColorRGB(0x828282); + selectionColor = UIColorRGB(0x3d3d3d); + } + + self.backgroundColor = backgroundColor; + self.backgroundView = [[UIView alloc] init]; + self.backgroundView.backgroundColor = backgroundColor; + self.backgroundView.opaque = false; + + self.selectedBackgroundView = [[UIView alloc] init]; + self.selectedBackgroundView.backgroundColor = selectionColor; + + _emojiLabel = [[UILabel alloc] init]; + _emojiLabel.backgroundColor = [UIColor clearColor]; + _emojiLabel.textColor = nameColor; + _emojiLabel.font = TGSystemFontOfSize(14.0f); + _emojiLabel.lineBreakMode = NSLineBreakByTruncatingTail; + [self.contentView addSubview:_emojiLabel]; + + _descriptionLabel = [[UILabel alloc] init]; + _descriptionLabel.backgroundColor = [UIColor clearColor]; + _descriptionLabel.textColor = usernameColor; + _descriptionLabel.font = TGSystemFontOfSize(14.0f); + _descriptionLabel.lineBreakMode = NSLineBreakByTruncatingTail; + [self.contentView addSubview:_descriptionLabel]; + } + return self; +} + +- (void)setPallete:(TGConversationAssociatedInputPanelPallete *)pallete +{ + if (pallete == nil || _pallete == pallete) + return; + + _pallete = pallete; + + _emojiLabel.textColor = pallete.textColor; + _descriptionLabel.textColor = pallete.textColor; + + self.backgroundColor = pallete.backgroundColor; + self.backgroundView.backgroundColor = self.backgroundColor; + self.selectedBackgroundView.backgroundColor = pallete.selectionColor; +} + +- (void)setEmoji:(NSString *)emoji label:(NSString *)label { + _emojiLabel.text = emoji; + _descriptionLabel.text = label; + + [self setNeedsLayout]; +} + +- (void)layoutSubviews +{ + [super layoutSubviews]; + + CGSize boundsSize = self.bounds.size; + + CGFloat leftInset = 11.0f; + CGFloat rightInset = 6.0f; + + CGSize titleSize = [_emojiLabel.text sizeWithFont:_emojiLabel.font]; + titleSize.width = CGCeil(MIN((boundsSize.width - leftInset - rightInset) * 3.0f / 4.0f, titleSize.width)); + titleSize.height = CGCeil(titleSize.height); + + CGSize descriptionSize = [_descriptionLabel.text sizeWithFont:_descriptionLabel.font]; + descriptionSize.width = CGCeil(MIN(boundsSize.width - leftInset - 40.0f, descriptionSize.width)); + + _emojiLabel.frame = CGRectMake(leftInset, CGFloor((boundsSize.height - titleSize.height) / 2.0f), titleSize.width, titleSize.height); + _descriptionLabel.frame = CGRectMake(40.0f, CGFloor((boundsSize.height - descriptionSize.height) / 2.0f), descriptionSize.width, descriptionSize.height); +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGAnimationBlockDelegate.h b/submodules/LegacyComponents/LegacyComponents/TGAnimationBlockDelegate.h new file mode 100644 index 0000000000..606fde92ef --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGAnimationBlockDelegate.h @@ -0,0 +1,12 @@ +#import + +@interface TGAnimationBlockDelegate : NSObject + +@property (nonatomic) bool removeLayerOnCompletion; +@property (nonatomic) NSNumber *opacityOnCompletion; +@property (nonatomic, weak) CALayer *layer; +@property (nonatomic, copy) void (^completion)(BOOL finished); + +- (instancetype)initWithLayer:(CALayer *)layer; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGAnimationBlockDelegate.m b/submodules/LegacyComponents/LegacyComponents/TGAnimationBlockDelegate.m new file mode 100644 index 0000000000..770238e192 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGAnimationBlockDelegate.m @@ -0,0 +1,37 @@ +#import "TGAnimationBlockDelegate.h" + +@implementation TGAnimationBlockDelegate + +- (instancetype)initWithLayer:(CALayer *)layer +{ + self = [super init]; + if (self != nil) + { + _layer = layer; + } + return self; +} + +- (void)animationDidStart:(CAAnimation *)__unused anim +{ +} + +- (void)animationDidStop:(CAAnimation *)__unused anim finished:(BOOL)flag +{ + CALayer *layer = _layer; + + if (flag) + { + if (_opacityOnCompletion != nil) + layer.opacity = [_opacityOnCompletion floatValue]; + } + if (_removeLayerOnCompletion) + [layer removeFromSuperlayer]; + + if (_completion) + _completion(flag); + + _completion = nil; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGAnimationUtils.h b/submodules/LegacyComponents/LegacyComponents/TGAnimationUtils.h new file mode 100644 index 0000000000..c220a6f334 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGAnimationUtils.h @@ -0,0 +1,22 @@ +#import + +#ifdef __cplusplus +extern "C" { +#endif + +extern NSString *kCAMediaTimingFunctionSpring; + +#ifdef __cplusplus +} +#endif + +@interface CALayer (AnimationUtils) + +- (void)animateAlphaFrom:(CGFloat)from to:(CGFloat)to duration:(NSTimeInterval)duration timingFunction:(NSString *)timingFunction removeOnCompletion:(bool)removeOnCompletion completion:(void (^)(bool))completion; + +- (void)animateScaleFrom:(CGFloat)from to:(CGFloat)to duration:(NSTimeInterval)duration timingFunction:(NSString *)timingFunction removeOnCompletion:(bool)removeOnCompletion completion:(void (^)(bool))completion; +- (void)animateSpringScaleFrom:(CGFloat)from to:(CGFloat)to duration:(NSTimeInterval)duration removeOnCompletion:(bool)removeOnCompletion completion:(void (^)(bool))completion; + +- (void)animatePositionFrom:(CGPoint)from to:(CGPoint)to duration:(NSTimeInterval)duration timingFunction:(NSString *)timingFunction removeOnCompletion:(bool)removeOnCompletion completion:(void (^)(bool))completion; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGAnimationUtils.m b/submodules/LegacyComponents/LegacyComponents/TGAnimationUtils.m new file mode 100644 index 0000000000..ddae4ce14c --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGAnimationUtils.m @@ -0,0 +1,146 @@ +#import "TGAnimationUtils.h" + +#import + +NSString *kCAMediaTimingFunctionSpring = @"kCAMediaTimingFunctionSpring"; + +@interface TGLayerAnimationDelegate : NSObject { + void (^_completion)(bool); +} + +@end + +@implementation TGLayerAnimationDelegate + +- (instancetype)initWithCompletion:(void (^)(bool))completion { + self = [super init]; + if (self != nil) { + _completion = [completion copy]; + } + return self; +} + +- (void)animationDidStop:(__unused CAAnimation *)anim finished:(BOOL)flag { + if (_completion) { + _completion(flag); + } +} + +@end + +@interface CAAnimation (AnimationUtils) + +@end + +@implementation CAAnimation (AnimationUtils) + +- (void)setCompletionBlock:(void (^)(bool))block { + self.delegate = [[TGLayerAnimationDelegate alloc] initWithCompletion:block]; +} + +@end + + +static CABasicAnimation * _Nonnull makeSpringAnimation(NSString * _Nonnull keyPath) { + CASpringAnimation *springAnimation = [CASpringAnimation animationWithKeyPath:keyPath]; + springAnimation.mass = 3.0f; + springAnimation.stiffness = 1000.0f; + springAnimation.damping = 500.0f; + springAnimation.duration = 0.5;//springAnimation.settlingDuration; + springAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear]; + return springAnimation; +} + +static CABasicAnimation * _Nonnull makeExtendedSpringAnimation(NSString * _Nonnull keyPath, CGFloat damping) { + CASpringAnimation *springAnimation = [CASpringAnimation animationWithKeyPath:keyPath]; + springAnimation.mass = 3.0f; + springAnimation.stiffness = 1000.0f; + springAnimation.damping = damping; + springAnimation.duration = 0.5;//springAnimation.settlingDuration; + springAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear]; + return springAnimation; +} + +@implementation CALayer (AnimationUtils) + +- (void)animateFrom:(id)from to:(id)to keyPath:(NSString *)keyPath timingFunction:(NSString *)timingFunction duration:(NSTimeInterval)duration removeOnCompletion:(bool)removeOnCompletion completion:(void (^)(bool))completion { + if ([timingFunction isEqualToString:kCAMediaTimingFunctionSpring]) { + CABasicAnimation *animation = makeSpringAnimation(keyPath); + animation.fromValue = from; + animation.toValue = to; + animation.removedOnCompletion = removeOnCompletion; + animation.fillMode = kCAFillModeForwards; + if (completion != nil) { + [animation setCompletionBlock:completion]; + } + + float k = (float)TGAnimationSpeedFactor(); + float speed = 1.0f; + if (k != 0 && k != 1) { + speed = 1.0f / k; + } + + animation.speed = speed * (float)(animation.duration / duration); + + [self addAnimation:animation forKey:keyPath]; + } else { + float k = (float)TGAnimationSpeedFactor(); + float speed = 1.0f; + if (k != 0 && k != 1) { + speed = 1.0f / k; + } + + CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:keyPath]; + animation.fromValue = from; + animation.toValue = to; + animation.duration = duration; + animation.timingFunction = [CAMediaTimingFunction functionWithName: timingFunction]; + animation.removedOnCompletion = removeOnCompletion; + animation.fillMode = kCAFillModeForwards; + animation.speed = speed; + if (completion != nil) { + [animation setCompletionBlock:completion]; + } + + [self addAnimation:animation forKey:keyPath]; + } +} + +- (void)animateSpringFrom:(id)from to:(id)to keyPath:(NSString *)keyPath duration:(NSTimeInterval)duration removeOnCompletion:(bool)removeOnCompletion completion:(void (^)(bool))completion { + CABasicAnimation *animation = makeExtendedSpringAnimation(keyPath, 75.0f); + animation.fromValue = from; + animation.toValue = to; + animation.removedOnCompletion = removeOnCompletion; + animation.fillMode = kCAFillModeForwards; + if (completion != nil) { + [animation setCompletionBlock:completion]; + } + + float k = (float)TGAnimationSpeedFactor(); + float speed = 1.0f; + if (k != 0 && k != 1) { + speed = 1.0f / k; + } + + animation.speed = speed * (float)(animation.duration / duration); + + [self addAnimation:animation forKey:keyPath]; +} + +- (void)animateAlphaFrom:(CGFloat)from to:(CGFloat)to duration:(NSTimeInterval)duration timingFunction:(NSString *)timingFunction removeOnCompletion:(bool)removeOnCompletion completion:(void (^)(bool))completion { + [self animateFrom:@(from) to:@(to) keyPath:@"opacity" timingFunction:timingFunction duration:duration removeOnCompletion:removeOnCompletion completion:completion]; +} + +- (void)animateScaleFrom:(CGFloat)from to:(CGFloat)to duration:(NSTimeInterval)duration timingFunction:(NSString *)timingFunction removeOnCompletion:(bool)removeOnCompletion completion:(void (^)(bool))completion { + [self animateFrom:@(from) to:@(to) keyPath:@"transform.scale" timingFunction:timingFunction duration:duration removeOnCompletion:removeOnCompletion completion:completion]; +} + +- (void)animateSpringScaleFrom:(CGFloat)from to:(CGFloat)to duration:(NSTimeInterval)duration removeOnCompletion:(bool)removeOnCompletion completion:(void (^)(bool))completion { + [self animateSpringFrom:@(from) to:@(to) keyPath:@"transform.scale" duration:duration removeOnCompletion:removeOnCompletion completion:completion]; +} + +- (void)animatePositionFrom:(CGPoint)from to:(CGPoint)to duration:(NSTimeInterval)duration timingFunction:(NSString *)timingFunction removeOnCompletion:(bool)removeOnCompletion completion:(void (^)(bool))completion { + [self animateFrom:[NSValue valueWithCGPoint:from] to:[NSValue valueWithCGPoint:to] keyPath:@"position" timingFunction:timingFunction duration:duration removeOnCompletion:removeOnCompletion completion:completion]; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGAttachmentAssetCell.h b/submodules/LegacyComponents/LegacyComponents/TGAttachmentAssetCell.h new file mode 100644 index 0000000000..57417f4511 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGAttachmentAssetCell.h @@ -0,0 +1,28 @@ +#import "TGAttachmentMenuCell.h" +#import +#import + +@class TGMediaAsset; +@class TGMediaSelectionContext; +@class TGMediaEditingContext; + +@interface TGAttachmentAssetCell : TGAttachmentMenuCell +{ + UIImageView *_iconView; + UIImageView *_gradientView; +} + +@property (nonatomic, readonly) TGImageView *imageView; +@property (nonatomic, readonly) TGCheckButtonView *checkButton; +- (void)setHidden:(bool)hidden animated:(bool)animated; + +@property (nonatomic, readonly) TGMediaAsset *asset; +- (void)setAsset:(TGMediaAsset *)asset signal:(SSignal *)signal; +- (void)setSignal:(SSignal *)signal; + +@property (nonatomic, assign) bool isZoomed; + +@property (nonatomic, strong) TGMediaSelectionContext *selectionContext; +@property (nonatomic, strong) TGMediaEditingContext *editingContext; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGAttachmentAssetCell.m b/submodules/LegacyComponents/LegacyComponents/TGAttachmentAssetCell.m new file mode 100644 index 0000000000..3f9b5eb3a8 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGAttachmentAssetCell.m @@ -0,0 +1,210 @@ +#import "TGAttachmentAssetCell.h" +#import + +#import "LegacyComponentsInternal.h" + +@interface TGAttachmentAssetCell () +{ + SMetaDisposable *_itemSelectedDisposable; +} +@end + +@implementation TGAttachmentAssetCell + +- (instancetype)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + if (self != nil) + { + _imageView = [[TGImageView alloc] initWithFrame:self.bounds]; + _imageView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + _imageView.contentMode = UIViewContentModeScaleAspectFill; + [self addSubview:_imageView]; + + static dispatch_once_t onceToken; + static UIImage *gradientImage; + dispatch_once(&onceToken, ^ + { + UIGraphicsBeginImageContextWithOptions(CGSizeMake(1.0f, 20.0f), false, 0.0f); + CGContextRef context = UIGraphicsGetCurrentContext(); + + CGColorRef colors[2] = { + CGColorRetain(UIColorRGBA(0x000000, 0.0f).CGColor), + CGColorRetain(UIColorRGBA(0x000000, 0.8f).CGColor) + }; + + CFArrayRef colorsArray = CFArrayCreate(kCFAllocatorDefault, (const void **)&colors, 2, NULL); + CGFloat locations[2] = {0.0f, 1.0f}; + + CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); + CGGradientRef gradient = CGGradientCreateWithColors(colorSpace, colorsArray, (CGFloat const *)&locations); + + CFRelease(colorsArray); + CFRelease(colors[0]); + CFRelease(colors[1]); + + CGColorSpaceRelease(colorSpace); + + CGContextDrawLinearGradient(context, gradient, CGPointMake(0.0f, 0.0f), CGPointMake(0.0f, 20.0f), 0); + + CFRelease(gradient); + + gradientImage = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + }); + + _gradientView = [[UIImageView alloc] initWithFrame:CGRectZero]; + _gradientView.image = gradientImage; + _gradientView.hidden = true; + [self addSubview:_gradientView]; + + if (iosMajorVersion() >= 11) + { + _imageView.accessibilityIgnoresInvertColors = true; + _gradientView.accessibilityIgnoresInvertColors = true; + } + + [self bringSubviewToFront:_cornersView]; + } + return self; +} + +- (void)dealloc +{ + [_itemSelectedDisposable dispose]; +} + +- (void)setAsset:(TGMediaAsset *)asset signal:(SSignal *)signal +{ + _asset = asset; + + if (self.selectionContext != nil) + { + if (_checkButton == nil) + { + _checkButton = [[TGCheckButtonView alloc] initWithStyle:TGCheckButtonStyleMedia]; + [_checkButton addTarget:self action:@selector(checkButtonPressed) forControlEvents:UIControlEventTouchUpInside]; + [self addSubview:_checkButton]; + + if (iosMajorVersion() >= 11) + _checkButton.accessibilityIgnoresInvertColors = true; + } + + if (_itemSelectedDisposable == nil) + _itemSelectedDisposable = [[SMetaDisposable alloc] init]; + + [self setChecked:[self.selectionContext isItemSelected:(id)asset] animated:false]; + __weak TGAttachmentAssetCell *weakSelf = self; + [_itemSelectedDisposable setDisposable:[[self.selectionContext itemInformativeSelectedSignal:(id)asset] startWithNext:^(TGMediaSelectionChange *next) + { + __strong TGAttachmentAssetCell *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + if (next.sender != strongSelf->_checkButton) + [strongSelf setChecked:next.selected animated:next.animated]; + }]]; + } + + if (_asset == nil) + { + [_imageView reset]; + return; + } + + [self setSignal:signal]; +} + + +- (void)setSignal:(SSignal *)signal +{ + if (signal != nil) + [_imageView setSignal:signal]; + else + [_imageView reset]; +} + +- (void)checkButtonPressed +{ + [_checkButton setSelected:!_checkButton.selected animated:true]; + + [self.selectionContext setItem:(id)self.asset selected:_checkButton.selected animated:false sender:_checkButton]; +} + +- (void)setChecked:(bool)checked animated:(bool)animated +{ + [_checkButton setSelected:checked animated:animated]; +} + +- (void)prepareForReuse +{ + [super prepareForReuse]; + [_imageView reset]; + _asset = nil; +} + +- (void)setHidden:(bool)hidden animated:(bool)animated +{ + if (hidden != self.imageView.hidden) + { + self.imageView.hidden = hidden; + + if (animated) + { + if (!hidden) + { + for (UIView *view in self.subviews) + { + if (view != self.imageView && view != _cornersView) + view.alpha = 0.0f; + } + } + + [UIView animateWithDuration:0.2 animations:^ + { + if (!hidden) + { + for (UIView *view in self.subviews) + { + if (view != self.imageView && view != _cornersView) + view.alpha = 1.0f; + } + } + }]; + } + else + { + for (UIView *view in self.subviews) + { + if (view != self.imageView && view != _cornersView) + view.alpha = hidden ? 0.0f : 1.0f; + } + } + } +} + +- (void)layoutSubviews +{ + [super layoutSubviews]; + + if (_checkButton != nil) + { + CGFloat offset = 0.0f; + if (self.superview != nil) + { + CGRect rect = [self.superview convertRect:self.frame toView:self.superview.superview]; + if (rect.origin.x < 0) + offset = rect.origin.x * -1; + else if (CGRectGetMaxX(rect) > self.superview.frame.size.width) + offset = self.superview.frame.size.width - CGRectGetMaxX(rect); + } + + CGFloat x = MAX(0, MIN(self.bounds.size.width - _checkButton.frame.size.width, self.bounds.size.width - _checkButton.frame.size.width + offset)); + _checkButton.frame = CGRectMake(x, 0, _checkButton.frame.size.width, _checkButton.frame.size.height); + } + + if (!_gradientView.hidden) + _gradientView.frame = CGRectMake(0, self.frame.size.height - 20.0f, self.frame.size.width, 20.0f); +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGAttachmentCameraCell.h b/submodules/LegacyComponents/LegacyComponents/TGAttachmentCameraCell.h new file mode 100644 index 0000000000..08524a26dc --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGAttachmentCameraCell.h @@ -0,0 +1,12 @@ +#import "TGAttachmentMenuCell.h" +#import "TGAttachmentCameraView.h" + +@interface TGAttachmentCameraCell : TGAttachmentMenuCell + +@property (nonatomic, readonly) TGAttachmentCameraView *cameraView; + +- (void)attachCameraViewIfNeeded:(TGAttachmentCameraView *)cameraView; + +@end + +extern NSString *const TGAttachmentCameraCellIdentifier; diff --git a/submodules/LegacyComponents/LegacyComponents/TGAttachmentCameraCell.m b/submodules/LegacyComponents/LegacyComponents/TGAttachmentCameraCell.m new file mode 100644 index 0000000000..49a6cc94da --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGAttachmentCameraCell.m @@ -0,0 +1,18 @@ +#import "TGAttachmentCameraCell.h" + +NSString *const TGAttachmentCameraCellIdentifier = @"AttachmentCameraCell"; + +@implementation TGAttachmentCameraCell + +- (void)attachCameraViewIfNeeded:(TGAttachmentCameraView *)cameraView +{ + if (_cameraView == cameraView) + return; + + _cameraView = cameraView; + _cameraView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + _cameraView.frame = self.bounds; + [self addSubview:cameraView]; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGAttachmentCameraView.h b/submodules/LegacyComponents/LegacyComponents/TGAttachmentCameraView.h new file mode 100644 index 0000000000..5d2fa718d3 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGAttachmentCameraView.h @@ -0,0 +1,27 @@ +#import + +@class TGCameraPreviewView; +@class TGMenuSheetPallete; + +@interface TGAttachmentCameraView : UIView + +@property (nonatomic, copy) void (^pressed)(void); +@property (nonatomic, strong) TGMenuSheetPallete *pallete; + +- (instancetype)initForSelfPortrait:(bool)selfPortrait; + +@property (nonatomic, readonly) bool previewViewAttached; +- (void)detachPreviewView; +- (void)attachPreviewViewAnimated:(bool)animated; +- (void)willAttachPreviewView; + +- (void)startPreview; +- (void)stopPreview; +- (void)resumePreview; +- (void)pausePreview; + +- (void)setZoomedProgress:(CGFloat)progress; + +- (TGCameraPreviewView *)previewView; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGAttachmentCameraView.m b/submodules/LegacyComponents/LegacyComponents/TGAttachmentCameraView.m new file mode 100644 index 0000000000..f28d784bd1 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGAttachmentCameraView.m @@ -0,0 +1,248 @@ +#import "TGAttachmentCameraView.h" + +#import "LegacyComponentsInternal.h" + +#import +#import "TGAttachmentMenuCell.h" + +#import +#import +#import + +#import + +#import + +@interface TGAttachmentCameraView () +{ + UIView *_wrapperView; + UIView *_fadeView; + UIImageView *_iconView; + UIImageView *_cornersView; + UIView *_zoomedView; + + TGCameraPreviewView *_previewView; + __weak PGCamera *_camera; +} +@end + +@implementation TGAttachmentCameraView + +- (instancetype)initForSelfPortrait:(bool)selfPortrait +{ + self = [super initWithFrame:CGRectZero]; + if (self != nil) + { + self.backgroundColor = [UIColor clearColor]; + + _wrapperView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 84.0f, 84.0f)]; + [self addSubview:_wrapperView]; + + PGCamera *camera = nil; + if ([PGCamera cameraAvailable]) + { + camera = [[PGCamera alloc] initWithMode:PGCameraModePhoto position:selfPortrait ? PGCameraPositionFront : PGCameraPositionUndefined]; + } + _camera = camera; + + _previewView = [[TGCameraPreviewView alloc] initWithFrame:CGRectMake(0, 0, 84.0f, 84.0f)]; + [_wrapperView addSubview:_previewView]; + [camera attachPreviewView:_previewView]; + + _iconView = [[UIImageView alloc] initWithImage:TGComponentsImageNamed(@"AttachmentMenuInteractiveCameraIcon")]; + [self addSubview:_iconView]; + + [self addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapGesture:)]]; + + [self setInterfaceOrientation:[[LegacyComponentsGlobals provider] applicationStatusBarOrientation] animated:false]; + + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleOrientationChange:) name:UIApplicationDidChangeStatusBarOrientationNotification object:nil]; + + _fadeView = [[UIView alloc] initWithFrame:self.bounds]; + _fadeView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + _fadeView.backgroundColor = [UIColor blackColor]; + _fadeView.hidden = true; + [self addSubview:_fadeView]; + + if (!TGMenuSheetUseEffectView) + { + static dispatch_once_t onceToken; + static UIImage *cornersImage; + dispatch_once(&onceToken, ^ + { + CGRect rect = CGRectMake(0, 0, TGAttachmentMenuCellCornerRadius * 2 + 1.0f, TGAttachmentMenuCellCornerRadius * 2 + 1.0f); + + UIGraphicsBeginImageContextWithOptions(rect.size, false, 0); + CGContextRef context = UIGraphicsGetCurrentContext(); + + CGContextSetFillColorWithColor(context, [UIColor whiteColor].CGColor); + CGContextFillRect(context, rect); + + CGContextSetBlendMode(context, kCGBlendModeClear); + + CGContextSetFillColorWithColor(context, [UIColor clearColor].CGColor); + CGContextFillEllipseInRect(context, rect); + + cornersImage = [UIGraphicsGetImageFromCurrentImageContext() resizableImageWithCapInsets:UIEdgeInsetsMake(TGAttachmentMenuCellCornerRadius, TGAttachmentMenuCellCornerRadius, TGAttachmentMenuCellCornerRadius, TGAttachmentMenuCellCornerRadius)]; + + UIGraphicsEndImageContext(); + }); + + _cornersView = [[UIImageView alloc] initWithImage:cornersImage]; + _cornersView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + _cornersView.frame = _previewView.bounds; + [_previewView addSubview:_cornersView]; + } + + _zoomedView = [[UIView alloc] initWithFrame:self.bounds]; + _zoomedView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + _zoomedView.backgroundColor = [UIColor whiteColor]; + _zoomedView.alpha = 0.0f; + _zoomedView.userInteractionEnabled = false; + [self addSubview:_zoomedView]; + + if (iosMajorVersion() >= 11) + { + _fadeView.accessibilityIgnoresInvertColors = true; + _iconView.accessibilityIgnoresInvertColors = true; + } + } + return self; +} + +- (void)dealloc +{ + TGCameraPreviewView *previewView = _previewView; + if (previewView.superview == _wrapperView && _camera != nil) + [self stopPreview]; + + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidChangeStatusBarOrientationNotification object:nil]; +} + +- (void)setPallete:(TGMenuSheetPallete *)pallete +{ + _pallete = pallete; + + _zoomedView.backgroundColor = pallete.backgroundColor; + _cornersView.image = pallete.cornersImage; +} + +- (void)setZoomedProgress:(CGFloat)progress +{ + _zoomedView.alpha = progress; +} + +- (TGCameraPreviewView *)previewView +{ + return _previewView; +} + +- (bool)previewViewAttached +{ + return _previewView.superview == _wrapperView; +} + +- (void)detachPreviewView +{ + [UIView animateWithDuration:0.1f animations:^ + { + _cornersView.alpha = 0.0f; + }]; + _iconView.alpha = 0.0f; +} + +- (void)attachPreviewViewAnimated:(bool)animated +{ + [_wrapperView addSubview:_previewView]; + [self setNeedsLayout]; + + if (animated) + { + _iconView.alpha = 0.0f; + [UIView animateWithDuration:0.2 animations:^ + { + _iconView.alpha = 1.0f; + }]; + } +} + +- (void)willAttachPreviewView +{ + [UIView animateWithDuration:0.1f delay:0.1f options:kNilOptions animations:^ + { + _cornersView.alpha = 1.0f; + } completion:nil]; +} + +- (void)tapGesture:(UITapGestureRecognizer *)recognizer +{ + if (recognizer.state == UIGestureRecognizerStateRecognized) + { + if (_pressed) + _pressed(); + } +} + +- (void)startPreview +{ + PGCamera *camera = _camera; + [camera startCaptureForResume:false completion:nil]; +} + +- (void)stopPreview +{ + PGCamera *camera = _camera; + [camera stopCaptureForPause:false completion:nil]; + _camera = nil; +} + +- (void)pausePreview +{ + TGCameraPreviewView *previewView = _previewView; + if (previewView.superview != _wrapperView) + return; + + PGCamera *camera = _camera; + [camera stopCaptureForPause:true completion:nil]; +} + +- (void)resumePreview +{ + TGCameraPreviewView *previewView = _previewView; + if (previewView.superview != _wrapperView) + return; + + PGCamera *camera = _camera; + [camera startCaptureForResume:true completion:nil]; +} + +- (void)handleOrientationChange:(NSNotification *)__unused notification +{ + [self setInterfaceOrientation:[[LegacyComponentsGlobals provider] applicationStatusBarOrientation] animated:true]; +} + +- (void)setInterfaceOrientation:(UIInterfaceOrientation)orientation animated:(bool)animated +{ + void(^block)(void) = ^ + { + _wrapperView.transform = CGAffineTransformMakeRotation(-1 * TGRotationForInterfaceOrientation(orientation)); + }; + + if (animated) + [UIView animateWithDuration:0.3f animations:block]; + else + block(); +} + +- (void)layoutSubviews +{ + [super layoutSubviews]; + + TGCameraPreviewView *previewView = _previewView; + if (previewView.superview == _wrapperView) + previewView.frame = self.bounds; + + _iconView.frame = CGRectMake((self.frame.size.width - _iconView.frame.size.width) / 2, (self.frame.size.height - _iconView.frame.size.height) / 2, _iconView.frame.size.width, _iconView.frame.size.height); +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGAttachmentCarouselItemView.h b/submodules/LegacyComponents/LegacyComponents/TGAttachmentCarouselItemView.h new file mode 100644 index 0000000000..360eb2de36 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGAttachmentCarouselItemView.h @@ -0,0 +1,52 @@ +#import +#import +#import + +@class TGMediaSelectionContext; +@class TGMediaEditingContext; +@class TGSuggestionContext; +@class TGViewController; +@class TGAttachmentCameraView; +@protocol TGModernGalleryTransitionHostScrollView; + +@interface TGAttachmentCarouselCollectionView : UICollectionView + +@end + +@interface TGAttachmentCarouselItemView : TGMenuSheetItemView + +@property (nonatomic, weak) TGViewController *parentController; + +@property (nonatomic, readonly) TGMediaSelectionContext *selectionContext; +@property (nonatomic, readonly) TGMediaEditingContext *editingContext; +@property (nonatomic, strong) TGSuggestionContext *suggestionContext; +@property (nonatomic) bool allowCaptions; +@property (nonatomic) bool allowCaptionEntities; +@property (nonatomic) bool inhibitDocumentCaptions; +@property (nonatomic) bool hasTimer; +@property (nonatomic) bool onlyCrop; +@property (nonatomic) bool asFile; +@property (nonatomic) bool inhibitMute; +@property (nonatomic) bool disableStickers; + +@property (nonatomic, strong) NSArray *underlyingViews; +@property (nonatomic, assign) bool openEditor; + +@property (nonatomic, copy) void (^cameraPressed)(TGAttachmentCameraView *cameraView); +@property (nonatomic, copy) void (^sendPressed)(TGMediaAsset *currentItem, bool asFiles); +@property (nonatomic, copy) void (^avatarCompletionBlock)(UIImage *image); + +@property (nonatomic, copy) void (^editorOpened)(void); +@property (nonatomic, copy) void (^editorClosed)(void); + +@property (nonatomic, assign) CGFloat remainingHeight; +@property (nonatomic, assign) bool condensed; +@property (nonatomic, assign) bool collapsed; + +@property (nonatomic, strong) NSString *recipientName; + +- (instancetype)initWithContext:(id)context camera:(bool)hasCamera selfPortrait:(bool)selfPortrait forProfilePhoto:(bool)forProfilePhoto assetType:(TGMediaAssetType)assetType saveEditedPhotos:(bool)saveEditedPhotos allowGrouping:(bool)allowGrouping; + +- (instancetype)initWithContext:(id)context camera:(bool)hasCamera selfPortrait:(bool)selfPortrait forProfilePhoto:(bool)forProfilePhoto assetType:(TGMediaAssetType)assetType saveEditedPhotos:(bool)saveEditedPhotos allowGrouping:(bool)allowGrouping allowSelection:(bool)allowSelection allowEditing:(bool)allowEditing document:(bool)document; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGAttachmentCarouselItemView.m b/submodules/LegacyComponents/LegacyComponents/TGAttachmentCarouselItemView.m new file mode 100644 index 0000000000..a8119d2466 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGAttachmentCarouselItemView.m @@ -0,0 +1,1244 @@ +#import "TGAttachmentCarouselItemView.h" + +#import "LegacyComponentsInternal.h" + +#import +#import + +#import +#import + +#import +#import + +#import "TGTransitionLayout.h" + +#import "TGAttachmentCameraView.h" + +#import "TGAttachmentPhotoCell.h" +#import "TGAttachmentVideoCell.h" +#import "TGAttachmentGifCell.h" + +#import +#import + +#import + +#import +#import +#import + +#import "TGMediaAvatarEditorTransition.h" +#import +#import +#import + +const CGSize TGAttachmentCellSize = { 84.0f, 84.0f }; +const CGFloat TGAttachmentEdgeInset = 8.0f; + +const CGFloat TGAttachmentZoomedPhotoRemainer = 32.0f; + +const CGFloat TGAttachmentZoomedPhotoHeight = 198.0f; +const CGFloat TGAttachmentZoomedPhotoMaxWidth = 250.0f; + +const CGFloat TGAttachmentZoomedPhotoCondensedHeight = 141.0f; +const CGFloat TGAttachmentZoomedPhotoCondensedMaxWidth = 178.0f; + +const CGFloat TGAttachmentZoomedPhotoAspectRatio = 1.2626f; + +const NSUInteger TGAttachmentDisplayedAssetLimit = 500; + +@interface TGAttachmentCarouselCollectionView () + +@end + +@implementation TGAttachmentCarouselCollectionView + +- (bool)disableGalleryTransitionOffsetFix { + return true; +} + +@end + +@interface TGAttachmentCarouselItemView () +{ + TGMediaAssetsLibrary *_assetsLibrary; + SMetaDisposable *_assetsDisposable; + TGMediaAssetFetchResult *_fetchResult; + + bool _document; + bool _forProfilePhoto; + + SMetaDisposable *_selectionChangedDisposable; + SMetaDisposable *_itemsSizeChangedDisposable; + + UICollectionViewFlowLayout *_smallLayout; + UICollectionViewFlowLayout *_largeLayout; + UICollectionView *_collectionView; + TGMediaAssetsPreheatMixin *_preheatMixin; + + TGAttachmentCameraView *_cameraView; + + TGMenuSheetButtonItemView *_sendMediaItemView; + TGMenuSheetButtonItemView *_sendFileItemView; + + TGMediaPickerModernGalleryMixin *_galleryMixin; + TGMediaPickerModernGalleryMixin *_previewGalleryMixin; + TGMediaAsset *_hiddenItem; + + bool _zoomedIn; + bool _zoomingIn; + CGFloat _zoomingProgress; + + NSInteger _pivotInItemIndex; + NSInteger _pivotOutItemIndex; + + CGSize _imageSize; + + CGSize _maxPhotoSize; + + CGFloat _smallActivationHeight; + bool _smallActivated; + CGSize _smallMaxPhotoSize; + + CGFloat _carouselCorrection; + + id _context; + bool _saveEditedPhotos; + + TGMenuSheetPallete *_pallete; +} +@end + +@implementation TGAttachmentCarouselItemView + +- (bool)disableGalleryTransitionOffsetFix { + return true; +} + +- (instancetype)initWithContext:(id)context camera:(bool)hasCamera selfPortrait:(bool)selfPortrait forProfilePhoto:(bool)forProfilePhoto assetType:(TGMediaAssetType)assetType saveEditedPhotos:(bool)saveEditedPhotos allowGrouping:(bool)allowGrouping +{ + return [self initWithContext:context camera:hasCamera selfPortrait:selfPortrait forProfilePhoto:forProfilePhoto assetType:assetType saveEditedPhotos:saveEditedPhotos allowGrouping:allowGrouping allowSelection:true allowEditing:!selfPortrait document:false]; +} + +- (instancetype)initWithContext:(id)context camera:(bool)hasCamera selfPortrait:(bool)selfPortrait forProfilePhoto:(bool)forProfilePhoto assetType:(TGMediaAssetType)assetType saveEditedPhotos:(bool)saveEditedPhotos allowGrouping:(bool)allowGrouping allowSelection:(bool)allowSelection allowEditing:(bool)allowEditing document:(bool)document +{ + self = [super initWithType:TGMenuSheetItemTypeDefault]; + if (self != nil) + { + _context = context; + _saveEditedPhotos = saveEditedPhotos; + +#if TARGET_IPHONE_SIMULATOR + hasCamera = true; +#endif + + __weak TGAttachmentCarouselItemView *weakSelf = self; + _document = document; + _forProfilePhoto = forProfilePhoto; + + _assetsLibrary = [TGMediaAssetsLibrary libraryForAssetType:assetType]; + _assetsDisposable = [[SMetaDisposable alloc] init]; + + if (!forProfilePhoto && allowSelection) + { + _selectionContext = [[TGMediaSelectionContext alloc] initWithGroupingAllowed:allowGrouping]; + if (allowGrouping) + _selectionContext.grouping = ![[[NSUserDefaults standardUserDefaults] objectForKey:@"TG_mediaGroupingDisabled_v0"] boolValue]; + [_selectionContext setItemSourceUpdatedSignal:[_assetsLibrary libraryChanged]]; + _selectionContext.updatedItemsSignal = ^SSignal *(NSArray *items) + { + __strong TGAttachmentCarouselItemView *strongSelf = weakSelf; + if (strongSelf == nil) + return nil; + + return [strongSelf->_assetsLibrary updatedAssetsForAssets:items]; + }; + + _selectionChangedDisposable = [[SMetaDisposable alloc] init]; + [_selectionChangedDisposable setDisposable:[[[_selectionContext selectionChangedSignal] mapToSignal:^SSignal *(id value) + { + __strong TGAttachmentCarouselItemView *strongSelf = weakSelf; + if (strongSelf == nil) + return [SSignal complete]; + + return [[strongSelf->_collectionView noOngoingTransitionSignal] then:[SSignal single:value]]; + }] startWithNext:^(__unused TGMediaSelectionChange *change) + { + __strong TGAttachmentCarouselItemView *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + NSInteger index = [strongSelf->_fetchResult indexOfAsset:(TGMediaAsset *)change.item]; + [strongSelf updateSendButtonsFromIndex:index]; + + [strongSelf updateSelectionIndexes]; + }]]; + } + + if (allowEditing) + { + _editingContext = [[TGMediaEditingContext alloc] init]; + + _itemsSizeChangedDisposable = [[SMetaDisposable alloc] init]; + [_itemsSizeChangedDisposable setDisposable:[[[_editingContext cropAdjustmentsUpdatedSignal] deliverOn:[SQueue mainQueue]] startWithNext:^(__unused id next) + { + __strong TGAttachmentCarouselItemView *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + if (strongSelf->_zoomedIn) + { + [strongSelf->_largeLayout invalidateLayout]; + [strongSelf->_collectionView layoutSubviews]; + + UICollectionViewCell *pivotCell = (UICollectionViewCell *)[strongSelf->_galleryMixin currentReferenceView]; + if (pivotCell != nil) + { + NSIndexPath *indexPath = [strongSelf->_collectionView indexPathForCell:pivotCell]; + if (indexPath != nil) + [strongSelf centerOnItemWithIndex:indexPath.row animated:false]; + } + } + }]]; + } + + _smallLayout = [[UICollectionViewFlowLayout alloc] init]; + _smallLayout.scrollDirection = UICollectionViewScrollDirectionHorizontal; + _smallLayout.minimumLineSpacing = TGAttachmentEdgeInset; + + _largeLayout = [[UICollectionViewFlowLayout alloc] init]; + _largeLayout.scrollDirection = _smallLayout.scrollDirection; + _largeLayout.minimumLineSpacing = _smallLayout.minimumLineSpacing; + + if (hasCamera) + { + _cameraView = [[TGAttachmentCameraView alloc] initForSelfPortrait:selfPortrait]; + _cameraView.frame = CGRectMake(_smallLayout.minimumLineSpacing, 0, TGAttachmentCellSize.width, TGAttachmentCellSize.height); + [_cameraView startPreview]; + + _cameraView.pressed = ^ + { + __strong TGAttachmentCarouselItemView *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [strongSelf.superview bringSubviewToFront:strongSelf]; + + if (strongSelf.cameraPressed != nil) + strongSelf.cameraPressed(strongSelf->_cameraView); + }; + } + + _collectionView = [[TGAttachmentCarouselCollectionView alloc] initWithFrame:CGRectMake(0, 0, self.bounds.size.width, TGAttachmentZoomedPhotoHeight + TGAttachmentEdgeInset * 2) collectionViewLayout:_smallLayout]; + if (iosMajorVersion() >= 11) + _collectionView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; + _collectionView.backgroundColor = [UIColor clearColor]; + _collectionView.dataSource = self; + _collectionView.delegate = self; + _collectionView.showsHorizontalScrollIndicator = false; + _collectionView.showsVerticalScrollIndicator = false; + [_collectionView registerClass:[TGAttachmentPhotoCell class] forCellWithReuseIdentifier:TGAttachmentPhotoCellIdentifier]; + [_collectionView registerClass:[TGAttachmentVideoCell class] forCellWithReuseIdentifier:TGAttachmentVideoCellIdentifier]; + [_collectionView registerClass:[TGAttachmentGifCell class] forCellWithReuseIdentifier:TGAttachmentGifCellIdentifier]; + if (iosMajorVersion() >= 9) + _collectionView.semanticContentAttribute = UISemanticContentAttributeForceLeftToRight; + [self addSubview:_collectionView]; + + if (_cameraView) + [_collectionView addSubview:_cameraView]; + + _sendMediaItemView = [[TGMenuSheetButtonItemView alloc] initWithTitle:nil type:TGMenuSheetButtonTypeSend action:^ + { + __strong TGAttachmentCarouselItemView *strongSelf = weakSelf; + if (strongSelf != nil && strongSelf.sendPressed != nil) + { + if (strongSelf->_selectionContext.allowGrouping) + [[NSUserDefaults standardUserDefaults] setObject:@(!strongSelf->_selectionContext.grouping) forKey:@"TG_mediaGroupingDisabled_v0"]; + strongSelf.sendPressed(nil, false); + } + }]; + [_sendMediaItemView setHidden:true animated:false]; + [self addSubview:_sendMediaItemView]; + + if (!_document) + { + _sendFileItemView = [[TGMenuSheetButtonItemView alloc] initWithTitle:nil type:TGMenuSheetButtonTypeDefault action:^ + { + __strong TGAttachmentCarouselItemView *strongSelf = weakSelf; + if (strongSelf != nil && strongSelf.sendPressed != nil) + strongSelf.sendPressed(nil, true); + }]; + _sendFileItemView.requiresDivider = false; + [_sendFileItemView setHidden:true animated:false]; + [self addSubview:_sendFileItemView]; + } + + [self setSignal:[[TGMediaAssetsLibrary authorizationStatusSignal] mapToSignal:^SSignal *(NSNumber *statusValue) + { + __strong TGAttachmentCarouselItemView *strongSelf = weakSelf; + if (strongSelf == nil) + return [SSignal complete]; + + TGMediaLibraryAuthorizationStatus status = [statusValue intValue]; + if (status == TGMediaLibraryAuthorizationStatusAuthorized) + { + return [[strongSelf->_assetsLibrary cameraRollGroup] mapToSignal:^SSignal *(TGMediaAssetGroup *cameraRollGroup) + { + return [strongSelf->_assetsLibrary assetsOfAssetGroup:cameraRollGroup reversed:true]; + }]; + } + else + { + return [SSignal fail:nil]; + } + }]]; + + _preheatMixin = [[TGMediaAssetsPreheatMixin alloc] initWithCollectionView:_collectionView scrollDirection:UICollectionViewScrollDirectionHorizontal]; + _preheatMixin.imageType = TGMediaAssetImageTypeThumbnail; + _preheatMixin.assetCount = ^NSInteger + { + __strong TGAttachmentCarouselItemView *strongSelf = weakSelf; + if (strongSelf == nil) + return 0; + + return [strongSelf collectionView:strongSelf->_collectionView numberOfItemsInSection:0]; + }; + _preheatMixin.assetAtIndexPath = ^TGMediaAsset *(NSIndexPath *indexPath) + { + __strong TGAttachmentCarouselItemView *strongSelf = weakSelf; + if (strongSelf == nil) + return nil; + + return [strongSelf->_fetchResult assetAtIndex:indexPath.row]; + }; + + [self _updateImageSize]; + _preheatMixin.imageSize = _imageSize; + + [self setCondensed:false]; + + _pivotInItemIndex = NSNotFound; + _pivotOutItemIndex = NSNotFound; + } + return self; +} + +- (void)dealloc +{ + [_assetsDisposable dispose]; + [_selectionChangedDisposable dispose]; + [_itemsSizeChangedDisposable dispose]; +} + +- (void)setPallete:(TGMenuSheetPallete *)pallete +{ + _pallete = pallete; + + _cameraView.pallete = pallete; + [_sendMediaItemView setPallete:pallete]; + [_sendFileItemView setPallete:pallete]; +} + +- (void)setRemainingHeight:(CGFloat)remainingHeight +{ + _remainingHeight = remainingHeight; + [self setCondensed:_condensed]; +} + +- (void)updateSelectionIndexes +{ + for (TGAttachmentAssetCell *cell in _collectionView.visibleCells) + { + NSUInteger index = [self.selectionContext indexOfItem:cell.asset]; + [cell.checkButton setNumber:index]; + } +} + +- (void)setCondensed:(bool)condensed +{ + _condensed = condensed; + + if (condensed) + _maxPhotoSize = CGSizeMake(TGAttachmentZoomedPhotoCondensedMaxWidth, TGAttachmentZoomedPhotoCondensedHeight); + else + _maxPhotoSize = CGSizeMake(TGAttachmentZoomedPhotoMaxWidth, TGAttachmentZoomedPhotoHeight); + + if (_remainingHeight > TGMenuSheetButtonItemViewHeight * (condensed ? 3 : 4)) + _maxPhotoSize.height += TGAttachmentZoomedPhotoRemainer; + + CGSize screenSize = TGScreenSize(); + _smallActivationHeight = screenSize.width; + + CGFloat smallHeight = MAX(95, screenSize.width - 225); + _smallMaxPhotoSize = CGSizeMake(ceil(smallHeight * TGAttachmentZoomedPhotoAspectRatio), smallHeight); + + CGRect frame = _collectionView.frame; + frame.size.height = _maxPhotoSize.height + TGAttachmentEdgeInset * 2; + _collectionView.frame = frame; +} + +- (void)setSignal:(SSignal *)signal +{ + __weak TGAttachmentCarouselItemView *weakSelf = self; + [_assetsDisposable setDisposable:[[[signal mapToSignal:^SSignal *(id value) + { + __strong TGAttachmentCarouselItemView *strongSelf = weakSelf; + if (strongSelf == nil) + return [SSignal complete]; + + return [[strongSelf->_collectionView noOngoingTransitionSignal] then:[SSignal single:value]]; + }] deliverOn:[SQueue mainQueue]] startWithNext:^(id next) + { + __strong TGAttachmentCarouselItemView *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + if ([next isKindOfClass:[TGMediaAssetFetchResult class]]) + { + TGMediaAssetFetchResult *fetchResult = (TGMediaAssetFetchResult *)next; + strongSelf->_fetchResult = fetchResult; + [strongSelf->_collectionView reloadData]; + } + else if ([next isKindOfClass:[TGMediaAssetFetchResultChange class]]) + { + TGMediaAssetFetchResultChange *change = (TGMediaAssetFetchResultChange *)next; + strongSelf->_fetchResult = change.fetchResultAfterChanges; + [TGMediaAssetsCollectionViewIncrementalUpdater updateCollectionView:strongSelf->_collectionView withChange:change completion:nil]; + + dispatch_async(dispatch_get_main_queue(), ^ + { + [strongSelf scrollViewDidScroll:strongSelf->_collectionView]; + }); + } + + if (strongSelf->_galleryMixin != nil && strongSelf->_fetchResult != nil) + [strongSelf->_galleryMixin updateWithFetchResult:strongSelf->_fetchResult]; + }]]; +} + +- (SSignal *)_signalForItem:(TGMediaAsset *)asset +{ + return [self _signalForItem:asset refresh:false onlyThumbnail:false]; +} + +- (SSignal *)_signalForItem:(TGMediaAsset *)asset refresh:(bool)refresh onlyThumbnail:(bool)onlyThumbnail +{ + bool thumbnail = onlyThumbnail || !_zoomedIn; + CGSize imageSize = onlyThumbnail ? [self imageSizeForThumbnail:true] : _imageSize; + + TGMediaAssetImageType screenImageType = refresh ? TGMediaAssetImageTypeLargeThumbnail : TGMediaAssetImageTypeFastLargeThumbnail; + TGMediaAssetImageType imageType = thumbnail ? TGMediaAssetImageTypeAspectRatioThumbnail : screenImageType; + + SSignal *assetSignal = [TGMediaAssetImageSignals imageForAsset:asset imageType:imageType size:imageSize]; + if (_editingContext == nil) + return assetSignal; + + SSignal *editedSignal = thumbnail ? [_editingContext thumbnailImageSignalForItem:asset] : [_editingContext fastImageSignalForItem:asset withUpdates:true]; + return [editedSignal mapToSignal:^SSignal *(id result) + { + if (result != nil) + return [SSignal single:result]; + else + return assetSignal; + }]; +} + +#pragma mark - + +- (void)setCameraZoomedIn:(bool)zoomedIn progress:(CGFloat)progress +{ + if (_cameraView == nil) + return; + + CGFloat size = TGAttachmentCellSize.height; + progress = zoomedIn ? progress : 1.0f - progress; + _cameraView.frame = CGRectMake(_smallLayout.minimumLineSpacing - (size + _smallLayout.minimumLineSpacing) * progress, 0, TGAttachmentCellSize.width + (size - TGAttachmentCellSize.width) * progress, TGAttachmentCellSize.height + (size - TGAttachmentCellSize.height) * progress); + [_cameraView setZoomedProgress:progress]; +} + +- (void)setZoomedMode:(bool)zoomed animated:(bool)animated index:(NSInteger)index +{ + if (zoomed == _zoomedIn) + { + if (_zoomedIn) + [self centerOnItemWithIndex:index animated:animated]; + + return; + } + + _zoomedIn = zoomed; + _zoomingIn = true; + _collectionView.userInteractionEnabled = false; + + if (zoomed) + _pivotInItemIndex = index; + else + _pivotOutItemIndex = index; + + UICollectionViewFlowLayout *toLayout = _zoomedIn ? _largeLayout : _smallLayout; + + [self _updateImageSize]; + + __weak TGAttachmentCarouselItemView *weakSelf = self; + TGTransitionLayout *layout = (TGTransitionLayout *)[_collectionView transitionToCollectionViewLayout:toLayout duration:0.3f completion:^(__unused BOOL completed, __unused BOOL finished) + { + __strong TGAttachmentCarouselItemView *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + strongSelf->_zoomingIn = false; + strongSelf->_collectionView.userInteractionEnabled = true; + [strongSelf centerOnItemWithIndex:index animated:false]; + }]; + layout.progressChanged = ^(CGFloat progress) + { + __strong TGAttachmentCarouselItemView *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + strongSelf->_zoomingProgress = progress; + [strongSelf requestMenuLayoutUpdate]; + [strongSelf _layoutButtonItemViews]; + [strongSelf setCameraZoomedIn:strongSelf->_zoomedIn progress:progress]; + }; + layout.transitionAlmostFinished = ^ + { + __strong TGAttachmentCarouselItemView *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + strongSelf->_pivotInItemIndex = NSNotFound; + strongSelf->_pivotOutItemIndex = NSNotFound; + }; + + CGPoint toOffset = [_collectionView toContentOffsetForLayout:layout indexPath:[NSIndexPath indexPathForRow:index inSection:0] toSize:_collectionView.bounds.size toContentInset:[self collectionView:_collectionView layout:toLayout insetForSectionAtIndex:0]]; + toOffset.y = 0; + layout.toContentOffset = toOffset; + + for (TGMenuSheetItemView *itemView in self.underlyingViews) + [itemView setHidden:zoomed animated:animated]; + + [_sendMediaItemView setHidden:!zoomed animated:animated]; + [_sendFileItemView setHidden:!zoomed animated:animated]; + + [self _updateVisibleItems]; +} + +- (void)updateSendButtonsFromIndex:(NSInteger)index +{ + __block NSInteger photosCount = 0; + __block NSInteger videosCount = 0; + __block NSInteger gifsCount = 0; + + [_selectionContext enumerateSelectedItems:^(id item) + { + TGMediaAsset *asset = (TGMediaAsset *)item; + if (![asset isKindOfClass:[TGMediaAsset class]]) + return; + + switch (asset.type) + { + case TGMediaAssetVideoType: + videosCount++; + break; + + case TGMediaAssetGifType: + gifsCount++; + break; + + default: + photosCount++; + break; + } + }]; + + NSInteger totalCount = photosCount + videosCount + gifsCount; + bool activated = (totalCount > 0); + if ([self zoomedModeSupported]) + [self setZoomedMode:activated animated:true index:index]; + else + [self setSelectedMode:activated animated:true]; + + if (_document) + { + _sendMediaItemView.title = TGLocalized(@"Common.Done"); + return; + } + + if (totalCount == 0) + return; + + if (photosCount > 0 && videosCount == 0 && gifsCount == 0) + { + NSString *format = TGLocalized([TGStringUtils integerValueFormat:@"AttachmentMenu.SendPhoto_" value:photosCount]); + _sendMediaItemView.title = [NSString stringWithFormat:format, [NSString stringWithFormat:@"%ld", photosCount]]; + } + else if (videosCount > 0 && photosCount == 0 && gifsCount == 0) + { + NSString *format = TGLocalized([TGStringUtils integerValueFormat:@"AttachmentMenu.SendVideo_" value:videosCount]); + _sendMediaItemView.title = [NSString stringWithFormat:format, [NSString stringWithFormat:@"%ld", videosCount]]; + } + else if (gifsCount > 0 && photosCount == 0 && videosCount == 0) + { + NSString *format = TGLocalized([TGStringUtils integerValueFormat:@"AttachmentMenu.SendGif_" value:gifsCount]); + _sendMediaItemView.title = [NSString stringWithFormat:format, [NSString stringWithFormat:@"%ld", gifsCount]]; + } + else + { + NSString *format = TGLocalized([TGStringUtils integerValueFormat:@"AttachmentMenu.SendItem_" value:totalCount]); + _sendMediaItemView.title = [NSString stringWithFormat:format, [NSString stringWithFormat:@"%ld", totalCount]]; + } + + if (totalCount == 1) + _sendFileItemView.title = TGLocalized(@"AttachmentMenu.SendAsFile"); + else + _sendFileItemView.title = TGLocalized(@"AttachmentMenu.SendAsFiles"); +} + +- (void)setSelectedMode:(bool)selected animated:(bool)animated +{ + [self.underlyingViews.firstObject setHidden:selected animated:animated]; + [_sendMediaItemView setHidden:!selected animated:animated]; +} + +- (bool)zoomedModeSupported +{ + return [TGMediaAssetsLibrary usesPhotoFramework]; +} + +- (CGPoint)contentOffsetForItemAtIndex:(NSInteger)index +{ + CGRect cellFrame = [_collectionView.collectionViewLayout layoutAttributesForItemAtIndexPath:[NSIndexPath indexPathForRow:index inSection:0]].frame; + + CGFloat x = cellFrame.origin.x - (_collectionView.frame.size.width - cellFrame.size.width) / 2.0f; + CGFloat contentOffset = MAX(0.0f, MIN(x, _collectionView.contentSize.width - _collectionView.frame.size.width)); + + return CGPointMake(contentOffset, 0); +} + +- (void)centerOnItemWithIndex:(NSInteger)index animated:(bool)animated +{ + [_collectionView setContentOffset:[self contentOffsetForItemAtIndex:index] animated:animated]; +} + +#pragma mark - + +- (CGFloat)_preferredHeightForZoomedIn:(bool)zoomedIn progress:(CGFloat)progress screenHeight:(CGFloat)__unused screenHeight +{ + progress = zoomedIn ? progress : 1.0f - progress; + + CGFloat inset = TGAttachmentEdgeInset * 2; + CGFloat cellHeight = TGAttachmentCellSize.height; + CGFloat targetCellHeight = _smallActivated ? _smallMaxPhotoSize.height : _maxPhotoSize.height; + + cellHeight = cellHeight + (targetCellHeight - cellHeight) * progress; + + return cellHeight + inset; +} + +- (CGFloat)_heightCorrectionForZoomedIn:(bool)zoomedIn progress:(CGFloat)progress +{ + progress = zoomedIn ? progress : 1.0f - progress; + + CGFloat correction = self.remainingHeight - (_document ? 1 : 2) * TGMenuSheetButtonItemViewHeight; + return -(correction * progress); +} + +- (CGFloat)preferredHeightForWidth:(CGFloat)__unused width screenHeight:(CGFloat)screenHeight +{ + if (_collapsed) { + self.alpha = 0.0f; + return 0.0f; + } + + CGFloat progress = _zoomingIn ? _zoomingProgress : 1.0f; + return [self _preferredHeightForZoomedIn:_zoomedIn progress:progress screenHeight:screenHeight]; +} + +- (CGFloat)contentHeightCorrection +{ + CGFloat progress = _zoomingIn ? _zoomingProgress : 1.0f; + return [self _heightCorrectionForZoomedIn:_zoomedIn progress:progress]; +} + +#pragma mark - + +- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event +{ + if (!_sendMediaItemView.userInteractionEnabled) + return [super pointInside:point withEvent:event]; + + return CGRectContainsPoint(self.bounds, point) || CGRectContainsPoint(_sendMediaItemView.frame, point) || CGRectContainsPoint(_sendFileItemView.frame, point); +} + +#pragma mark - + +- (void)_updateVisibleItems +{ + for (NSIndexPath *indexPath in _collectionView.indexPathsForVisibleItems) + { + TGMediaAsset *asset = [_fetchResult assetAtIndex:indexPath.row]; + TGAttachmentAssetCell *cell = (TGAttachmentAssetCell *)[_collectionView cellForItemAtIndexPath:indexPath]; + if (cell.isZoomed != _zoomedIn) + { + cell.isZoomed = _zoomedIn; + if (cell.asset == nil) + [cell setAsset:asset signal:[self _signalForItem:asset refresh:true onlyThumbnail:false]]; + else + [cell setSignal:[self _signalForItem:asset refresh:true onlyThumbnail:false]]; + } + } +} + +- (void)_updateImageSize +{ + _imageSize = [self imageSizeForThumbnail:!_zoomedIn]; +} + +- (CGSize)imageSizeForThumbnail:(bool)forThumbnail +{ + CGFloat scale = MIN(2.0f, TGScreenScaling()); + if (forThumbnail) + return CGSizeMake(TGAttachmentCellSize.width * scale, TGAttachmentCellSize.height * scale); + else + return CGSizeMake(floor(TGAttachmentZoomedPhotoMaxWidth * scale), floor(TGAttachmentZoomedPhotoMaxWidth * scale)); +} + +- (bool)hasCameraInCurrentMode +{ + return (!_zoomedIn && _cameraView != nil); +} + +#pragma mark - + +- (void)_setupGalleryMixin:(TGMediaPickerModernGalleryMixin *)mixin +{ + __weak TGAttachmentCarouselItemView *weakSelf = self; + mixin.referenceViewForItem = ^UIView *(TGMediaPickerGalleryItem *item) + { + __strong TGAttachmentCarouselItemView *strongSelf = weakSelf; + if (strongSelf != nil) + return [strongSelf referenceViewForAsset:item.asset]; + + return nil; + }; + + mixin.itemFocused = ^(TGMediaPickerGalleryItem *item) + { + __strong TGAttachmentCarouselItemView *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + strongSelf->_hiddenItem = item.asset; + [strongSelf updateHiddenCellAnimated:false]; + }; + + mixin.willTransitionIn = ^ + { + __strong TGAttachmentCarouselItemView *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [strongSelf.superview bringSubviewToFront:strongSelf]; + [strongSelf->_cameraView pausePreview]; + }; + + mixin.willTransitionOut = ^ + { + __strong TGAttachmentCarouselItemView *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [strongSelf->_cameraView resumePreview]; + }; + + mixin.didTransitionOut = ^ + { + __strong TGAttachmentCarouselItemView *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + strongSelf->_hiddenItem = nil; + [strongSelf updateHiddenCellAnimated:true]; + + strongSelf->_galleryMixin = nil; + }; + + mixin.completeWithItem = ^(TGMediaPickerGalleryItem *item) + { + __strong TGAttachmentCarouselItemView *strongSelf = weakSelf; + if (strongSelf != nil && strongSelf.sendPressed != nil) + { + if (strongSelf->_selectionContext.allowGrouping) + [[NSUserDefaults standardUserDefaults] setObject:@(!strongSelf->_selectionContext.grouping) forKey:@"TG_mediaGroupingDisabled_v0"]; + strongSelf.sendPressed(item.asset, strongSelf.asFile); + } + }; + + mixin.editorOpened = self.editorOpened; + mixin.editorClosed = self.editorClosed; +} + +- (TGMediaPickerModernGalleryMixin *)galleryMixinForIndexPath:(NSIndexPath *)indexPath previewMode:(bool)previewMode outAsset:(TGMediaAsset **)outAsset +{ + TGMediaAsset *asset = [_fetchResult assetAtIndex:indexPath.row]; + if (outAsset != NULL) + *outAsset = asset; + + UIImage *thumbnailImage = nil; + + TGAttachmentAssetCell *cell = (TGAttachmentAssetCell *)[_collectionView cellForItemAtIndexPath:indexPath]; + if ([cell isKindOfClass:[TGAttachmentAssetCell class]]) + thumbnailImage = cell.imageView.image; + + TGMediaPickerModernGalleryMixin *mixin = [[TGMediaPickerModernGalleryMixin alloc] initWithContext:_context item:asset fetchResult:_fetchResult parentController:self.parentController thumbnailImage:thumbnailImage selectionContext:_selectionContext editingContext:_editingContext suggestionContext:self.suggestionContext hasCaptions:(_allowCaptions && !_forProfilePhoto) allowCaptionEntities:self.allowCaptionEntities hasTimer:self.hasTimer onlyCrop:self.onlyCrop inhibitDocumentCaptions:_inhibitDocumentCaptions inhibitMute:self.inhibitMute asFile:self.asFile itemsLimit:TGAttachmentDisplayedAssetLimit recipientName:self.recipientName]; + + __weak TGAttachmentCarouselItemView *weakSelf = self; + mixin.thumbnailSignalForItem = ^SSignal *(id item) + { + __strong TGAttachmentCarouselItemView *strongSelf = weakSelf; + if (strongSelf == nil) + return nil; + + return [strongSelf _signalForItem:item refresh:false onlyThumbnail:true]; + }; + + if (!previewMode) + [self _setupGalleryMixin:mixin]; + + return mixin; +} + +- (UIView *)referenceViewForAsset:(TGMediaAsset *)asset +{ + for (TGAttachmentAssetCell *cell in [_collectionView visibleCells]) + { + if ([cell.asset isEqual:asset]) + return cell; + } + + return nil; +} + +- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath +{ + NSInteger index = indexPath.row; + TGMediaAsset *asset = [_fetchResult assetAtIndex:index]; + + __block UIImage *thumbnailImage = nil; + if ([TGMediaAssetsLibrary usesPhotoFramework]) + { + TGAttachmentAssetCell *cell = (TGAttachmentAssetCell *)[collectionView cellForItemAtIndexPath:indexPath]; + if ([cell isKindOfClass:[TGAttachmentAssetCell class]]) + thumbnailImage = cell.imageView.image; + } + else + { + [[TGMediaAssetImageSignals imageForAsset:asset imageType:TGMediaAssetImageTypeAspectRatioThumbnail size:CGSizeZero] startWithNext:^(UIImage *next) + { + thumbnailImage = next; + }]; + } + + __weak TGAttachmentCarouselItemView *weakSelf = self; + UIView *(^referenceViewForAsset)(TGMediaAsset *) = ^UIView *(TGMediaAsset *asset) + { + __strong TGAttachmentCarouselItemView *strongSelf = weakSelf; + if (strongSelf != nil) + return [strongSelf referenceViewForAsset:asset]; + + return nil; + }; + + if (self.openEditor) + { + id windowManager = [_context makeOverlayWindowManager]; + + TGPhotoEditorController *controller = [[TGPhotoEditorController alloc] initWithContext:[windowManager context] item:asset intent:_disableStickers ? TGPhotoEditorControllerSignupAvatarIntent : TGPhotoEditorControllerAvatarIntent adjustments:nil caption:nil screenImage:thumbnailImage availableTabs:[TGPhotoEditorController defaultTabsForAvatarIntent] selectedTab:TGPhotoEditorCropTab]; + controller.editingContext = _editingContext; + controller.dontHideStatusBar = true; + + TGMediaAvatarEditorTransition *transition = [[TGMediaAvatarEditorTransition alloc] initWithController:controller fromView:referenceViewForAsset(asset)]; + + controller.didFinishRenderingFullSizeImage = ^(UIImage *resultImage) + { + __strong TGAttachmentCarouselItemView *strongSelf = weakSelf; + if (strongSelf == nil || !strongSelf->_saveEditedPhotos) + return; + + [[strongSelf->_assetsLibrary saveAssetWithImage:resultImage] startWithNext:nil]; + }; + + __weak TGPhotoEditorController *weakController = controller; + controller.didFinishEditing = ^(__unused id adjustments, UIImage *resultImage, __unused UIImage *thumbnailImage, __unused bool hasChanges) + { + if (!hasChanges) + return; + + __strong TGAttachmentCarouselItemView *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + __strong TGPhotoEditorController *strongController = weakController; + if (strongController == nil) + return; + + if (strongSelf.avatarCompletionBlock != nil) + strongSelf.avatarCompletionBlock(resultImage); + + [strongController dismissAnimated:true]; + }; + + controller.requestThumbnailImage = ^(id editableItem) + { + return [editableItem thumbnailImageSignal]; + }; + + controller.requestOriginalScreenSizeImage = ^(id editableItem, NSTimeInterval position) + { + return [editableItem screenImageSignal:position]; + }; + + controller.requestOriginalFullSizeImage = ^(id editableItem, NSTimeInterval position) + { + return [editableItem originalImageSignal:position]; + }; + + TGOverlayControllerWindow *controllerWindow = [[TGOverlayControllerWindow alloc] initWithManager:windowManager parentController:_parentController contentController:controller]; + controllerWindow.hidden = false; + controller.view.clipsToBounds = true; + + transition.referenceFrame = ^CGRect + { + UIView *referenceView = referenceViewForAsset(asset); + return [referenceView.superview convertRect:referenceView.frame toView:nil]; + }; + transition.referenceImageSize = ^CGSize + { + return asset.dimensions; + }; + transition.referenceScreenImageSignal = ^SSignal * + { + return [TGMediaAssetImageSignals imageForAsset:asset imageType:TGMediaAssetImageTypeFastScreen size:CGSizeMake(640, 640)]; + }; + [transition presentAnimated:true]; + + controller.beginCustomTransitionOut = ^(CGRect outReferenceFrame, UIView *repView, void (^completion)(void)) + { + __strong TGAttachmentCarouselItemView *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + transition.outReferenceFrame = outReferenceFrame; + transition.repView = repView; + [transition dismissAnimated:true completion:^ + { + strongSelf->_hiddenItem = nil; + [strongSelf updateHiddenCellAnimated:false]; + + dispatch_async(dispatch_get_main_queue(), ^ + { + if (completion != nil) + completion(); + }); + }]; + }; + + _hiddenItem = asset; + [self updateHiddenCellAnimated:false]; + } + else + { + _galleryMixin = [self galleryMixinForIndexPath:indexPath previewMode:false outAsset:NULL]; + [_galleryMixin present]; + } +} + +- (NSInteger)collectionView:(UICollectionView *)__unused collectionView numberOfItemsInSection:(NSInteger)__unused section +{ + return MIN(_fetchResult.count, TGAttachmentDisplayedAssetLimit); +} + +- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath +{ + NSInteger index = indexPath.row; + + TGMediaAsset *asset = [_fetchResult assetAtIndex:index]; + NSString *cellIdentifier = nil; + switch (asset.type) + { + case TGMediaAssetVideoType: + cellIdentifier = TGAttachmentVideoCellIdentifier; + break; + + case TGMediaAssetGifType: + if (_forProfilePhoto) + cellIdentifier = TGAttachmentPhotoCellIdentifier; + else + cellIdentifier = TGAttachmentGifCellIdentifier; + break; + + default: + cellIdentifier = TGAttachmentPhotoCellIdentifier; + break; + } + + TGAttachmentAssetCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:cellIdentifier forIndexPath:indexPath]; + cell.pallete = _pallete; + NSInteger pivotIndex = NSNotFound; + NSInteger limit = 0; + if (_pivotInItemIndex != NSNotFound) + { + if (self.frame.size.width <= 320) + limit = 2; + else + limit = 3; + + pivotIndex = _pivotInItemIndex; + } + else if (_pivotOutItemIndex != NSNotFound) + { + pivotIndex = _pivotOutItemIndex; + + if (self.frame.size.width <= 320) + limit = 4; + else + limit = 6; + } + + cell.selectionContext = _selectionContext; + cell.editingContext = _editingContext; + + if (!(pivotIndex != NSNotFound && (indexPath.row < pivotIndex - limit || indexPath.row > pivotIndex + limit))) + { + if (![asset isEqual:cell.asset] || cell.isZoomed != _zoomedIn) + { + cell.isZoomed = _zoomedIn; + [cell setAsset:asset signal:[self _signalForItem:asset refresh:[cell.asset isEqual:asset] onlyThumbnail:false]]; + } + } + + if (self.selectionContext != nil) + [cell.checkButton setNumber:[self.selectionContext indexOfItem:asset]]; + + return cell; +} + +- (void)updateHiddenCellAnimated:(bool)animated +{ + for (TGAttachmentAssetCell *cell in [_collectionView visibleCells]) + [cell setHidden:([cell.asset isEqual:_hiddenItem]) animated:animated]; +} + +#pragma mark - + +- (CGSize)collectionView:(UICollectionView *)__unused collectionView layout:(UICollectionViewLayout *)__unused collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath +{ + if (_zoomedIn) + { + CGSize maxPhotoSize = _maxPhotoSize; + if (_smallActivated) + maxPhotoSize = _smallMaxPhotoSize; + + if (_pivotInItemIndex != NSNotFound && (indexPath.row < _pivotInItemIndex - 2 || indexPath.row > _pivotInItemIndex + 2)) + return CGSizeMake(maxPhotoSize.height, maxPhotoSize.height); + + TGMediaAsset *asset = [_fetchResult assetAtIndex:indexPath.row]; + if (asset != nil) + { + CGSize dimensions = asset.dimensions; + if (dimensions.width < 1.0f) + dimensions.width = 1.0f; + if (dimensions.height < 1.0f) + dimensions.height = 1.0f; + + id adjustments = [_editingContext adjustmentsForItem:asset]; + if ([adjustments cropAppliedForAvatar:false]) + { + dimensions = adjustments.cropRect.size; + + bool sideward = TGOrientationIsSideward(adjustments.cropOrientation, NULL); + if (sideward) + dimensions = CGSizeMake(dimensions.height, dimensions.width); + } + + CGFloat width = MIN(maxPhotoSize.width, ceil(dimensions.width * maxPhotoSize.height / dimensions.height)); + return CGSizeMake(width, maxPhotoSize.height); + } + + return CGSizeMake(maxPhotoSize.height, maxPhotoSize.height); + } + + return TGAttachmentCellSize; +} + +- (UIEdgeInsets)collectionView:(UICollectionView *)__unused collectionView layout:(UICollectionViewLayout *)collectionViewLayout insetForSectionAtIndex:(NSInteger)__unused section +{ + CGFloat edgeInset = TGAttachmentEdgeInset; + CGFloat leftInset = [self hasCameraInCurrentMode] ? 2 * edgeInset + 84.0f : edgeInset; + + CGFloat height = self.frame.size.height; + + if (collectionViewLayout == _smallLayout) + height = [self _preferredHeightForZoomedIn:false progress:1.0f screenHeight:self.screenHeight]; + else if (collectionViewLayout == _largeLayout) + height = [self _preferredHeightForZoomedIn:true progress:1.0f screenHeight:self.screenHeight]; + + CGFloat cellHeight = height - 2 * edgeInset; + CGFloat topInset = _collectionView.frame.size.height - cellHeight - edgeInset; + CGFloat bottomInset = edgeInset; + + return UIEdgeInsetsMake(topInset, leftInset, bottomInset, edgeInset); +} + +- (CGFloat)collectionView:(UICollectionView *)__unused collectionView layout:(UICollectionViewLayout *)__unused collectionViewLayout minimumLineSpacingForSectionAtIndex:(NSInteger)__unused section +{ + return _smallLayout.minimumLineSpacing; +} + +- (UICollectionViewTransitionLayout *)collectionView:(UICollectionView *)__unused collectionView transitionLayoutForOldLayout:(UICollectionViewLayout *)fromLayout newLayout:(UICollectionViewLayout *)toLayout +{ + return [[TGTransitionLayout alloc] initWithCurrentLayout:fromLayout nextLayout:toLayout]; +} + +#pragma mark - + +- (void)scrollViewDidScroll:(UIScrollView *)__unused scrollView +{ + if (_zoomingIn) + return; + + if (!_zoomedIn) + [_preheatMixin update]; + + for (UICollectionViewCell *cell in _collectionView.visibleCells) + { + if ([cell isKindOfClass:[TGAttachmentAssetCell class]]) + [(TGAttachmentAssetCell *)cell setNeedsLayout]; + } +} + +#pragma mark - + +- (void)menuView:(TGMenuSheetView *)menuView willAppearAnimated:(bool)__unused animated +{ + __weak TGAttachmentCarouselItemView *weakSelf = self; + menuView.tapDismissalAllowed = ^bool + { + __strong TGAttachmentCarouselItemView *strongSelf = weakSelf; + if (strongSelf == nil) + return true; + + return !strongSelf->_collectionView.isDecelerating && !strongSelf->_collectionView.isTracking; + }; +} + +- (void)menuView:(TGMenuSheetView *)menuView willDisappearAnimated:(bool)animated +{ + [super menuView:menuView didDisappearAnimated:animated]; + menuView.tapDismissalAllowed = nil; + [_cameraView stopPreview]; +} + +#pragma mark - + +- (void)setScreenHeight:(CGFloat)screenHeight +{ + _screenHeight = screenHeight; + [self _updateSmallActivated]; + +} + +- (void)setSizeClass:(UIUserInterfaceSizeClass)sizeClass +{ + _sizeClass = sizeClass; + [self _updateSmallActivated]; +} + +- (void)_updateSmallActivated +{ + _smallActivated = (fabs(_screenHeight - _smallActivationHeight) < FLT_EPSILON && _sizeClass == UIUserInterfaceSizeClassCompact); +} + +- (void)layoutSubviews +{ + [super layoutSubviews]; + + CGRect frame = _collectionView.frame; + frame.size.width = self.frame.size.width; + + frame.size.height = (_smallActivated ? _smallMaxPhotoSize.height : _maxPhotoSize.height) + TGAttachmentEdgeInset * 2; + frame.origin.y = self.frame.size.height - frame.size.height; + + if (!CGRectEqualToRect(frame, _collectionView.frame)) + { + bool invalidate = fabs(_collectionView.frame.size.height - frame.size.height) > FLT_EPSILON; + + _collectionView.frame = frame; + + if (invalidate) + { + [_smallLayout invalidateLayout]; + [_largeLayout invalidateLayout]; + [_collectionView layoutSubviews]; + } + } + + CGFloat height = self.frame.size.height; + CGFloat cellHeight = height - 2 * TGAttachmentEdgeInset; + CGFloat topInset = _collectionView.frame.size.height - cellHeight - TGAttachmentEdgeInset; + + frame = _cameraView.frame; + frame.origin.y = topInset; + _cameraView.frame = frame; + + [self _layoutButtonItemViews]; +} + +- (void)_layoutButtonItemViews +{ + _sendMediaItemView.frame = CGRectMake(0, [self preferredHeightForWidth:self.frame.size.width screenHeight:self.screenHeight], self.frame.size.width, [_sendMediaItemView preferredHeightForWidth:self.frame.size.width screenHeight:self.screenHeight]); + _sendFileItemView.frame = CGRectMake(0, CGRectGetMaxY(_sendMediaItemView.frame), self.frame.size.width, [_sendFileItemView preferredHeightForWidth:self.frame.size.width screenHeight:self.screenHeight]); +} + +#pragma mark - + +- (UIView *)previewSourceView +{ + return _collectionView; +} + +- (UIViewController *)previewingContext:(id)previewingContext viewControllerForLocation:(CGPoint)location +{ + if (self.openEditor) { + return nil; + } + + NSIndexPath *indexPath = [_collectionView indexPathForItemAtPoint:location]; + if (indexPath == nil) + return nil; + + CGRect cellFrame = [_collectionView.collectionViewLayout layoutAttributesForItemAtIndexPath:indexPath].frame; + previewingContext.sourceRect = cellFrame; + + TGMediaAsset *asset = nil; + _previewGalleryMixin = [self galleryMixinForIndexPath:indexPath previewMode:true outAsset:&asset]; + UIViewController *controller = [_previewGalleryMixin galleryController]; + + CGSize screenSize = TGScreenSize(); + controller.preferredContentSize = TGFitSize(asset.dimensions, screenSize); + [_previewGalleryMixin setPreviewMode]; + return controller; +} + +- (void)previewingContext:(id)__unused previewingContext commitViewController:(UIViewController *)__unused viewControllerToCommit +{ + _galleryMixin = _previewGalleryMixin; + _previewGalleryMixin = nil; + + [self _setupGalleryMixin:_galleryMixin]; + [_galleryMixin present]; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGAttachmentGifCell.h b/submodules/LegacyComponents/LegacyComponents/TGAttachmentGifCell.h new file mode 100644 index 0000000000..856ec84c95 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGAttachmentGifCell.h @@ -0,0 +1,9 @@ +#import "TGAttachmentPhotoCell.h" + +@interface TGAttachmentGifCell : TGAttachmentPhotoCell +{ + UILabel *_typeLabel; +} +@end + +extern NSString *const TGAttachmentGifCellIdentifier; diff --git a/submodules/LegacyComponents/LegacyComponents/TGAttachmentGifCell.m b/submodules/LegacyComponents/LegacyComponents/TGAttachmentGifCell.m new file mode 100644 index 0000000000..ce4d9d7dd3 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGAttachmentGifCell.m @@ -0,0 +1,39 @@ +#import "TGAttachmentGifCell.h" + +#import + +NSString *const TGAttachmentGifCellIdentifier = @"AttachmentGifCell"; + +@implementation TGAttachmentGifCell + +- (instancetype)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + if (self != nil) + { + _typeLabel = [[UILabel alloc] initWithFrame:CGRectZero]; + _typeLabel.backgroundColor = [UIColor clearColor]; + _typeLabel.font = TGSystemFontOfSize(12); + _typeLabel.textColor = [UIColor whiteColor]; + _typeLabel.text = @"GIF"; + [self addSubview:_typeLabel]; + + [_typeLabel sizeToFit]; + + CGSize typeSize = CGSizeMake(ceil(_typeLabel.frame.size.width), ceil(_typeLabel.frame.size.height)); + _typeLabel.frame = CGRectMake(4, self.frame.size.height - typeSize.height - 2, typeSize.width, typeSize.height); + + _gradientView.hidden = false; + + [self bringSubviewToFront:_cornersView]; + } + return self; +} + +- (void)layoutSubviews +{ + [super layoutSubviews]; + _typeLabel.frame = CGRectMake(4, self.frame.size.height - _typeLabel.frame.size.height - 2, _typeLabel.frame.size.width, _typeLabel.frame.size.height); +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGAttachmentMenuCell.h b/submodules/LegacyComponents/LegacyComponents/TGAttachmentMenuCell.h new file mode 100644 index 0000000000..dcb1550409 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGAttachmentMenuCell.h @@ -0,0 +1,14 @@ +#import + +@class TGMenuSheetPallete; + +@interface TGAttachmentMenuCell : UICollectionViewCell +{ + UIImageView *_cornersView; +} + +@property (nonatomic, strong) TGMenuSheetPallete *pallete; + +@end + +extern const CGFloat TGAttachmentMenuCellCornerRadius; diff --git a/submodules/LegacyComponents/LegacyComponents/TGAttachmentMenuCell.m b/submodules/LegacyComponents/LegacyComponents/TGAttachmentMenuCell.m new file mode 100644 index 0000000000..8368ec3742 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGAttachmentMenuCell.m @@ -0,0 +1,65 @@ +#import "TGAttachmentMenuCell.h" +#import +#import +#import "LegacyComponentsInternal.h" + +const CGFloat TGAttachmentMenuCellCornerRadius = 5.5f; + +@implementation TGAttachmentMenuCell + +- (instancetype)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + if (self != nil) + { + self.clipsToBounds = true; + + if (TGMenuSheetUseEffectView) + { + self.backgroundColor = [UIColor clearColor]; + self.layer.cornerRadius = TGAttachmentMenuCellCornerRadius; + } + else + { + self.backgroundColor = [UIColor whiteColor]; + + static dispatch_once_t onceToken; + static UIImage *cornersImage; + dispatch_once(&onceToken, ^ + { + CGRect rect = CGRectMake(0, 0, TGAttachmentMenuCellCornerRadius * 2 + 1.0f, TGAttachmentMenuCellCornerRadius * 2 + 1.0f); + + UIGraphicsBeginImageContextWithOptions(rect.size, false, 0); + CGContextRef context = UIGraphicsGetCurrentContext(); + + CGContextSetFillColorWithColor(context, UIColorRGB(0x222f40).CGColor); + CGContextFillRect(context, rect); + + CGContextSetBlendMode(context, kCGBlendModeClear); + + CGContextSetFillColorWithColor(context, [UIColor clearColor].CGColor); + CGContextFillEllipseInRect(context, rect); + + cornersImage = [UIGraphicsGetImageFromCurrentImageContext() resizableImageWithCapInsets:UIEdgeInsetsMake(TGAttachmentMenuCellCornerRadius, TGAttachmentMenuCellCornerRadius, TGAttachmentMenuCellCornerRadius, TGAttachmentMenuCellCornerRadius)]; + + UIGraphicsEndImageContext(); + }); + + _cornersView = [[UIImageView alloc] initWithImage:cornersImage]; + _cornersView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + _cornersView.frame = self.bounds; + [self addSubview:_cornersView]; + } + } + return self; +} + +- (void)setPallete:(TGMenuSheetPallete *)pallete +{ + _pallete = pallete; + + self.backgroundColor = pallete.backgroundColor; + _cornersView.image = pallete.cornersImage; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGAttachmentPhotoCell.h b/submodules/LegacyComponents/LegacyComponents/TGAttachmentPhotoCell.h new file mode 100644 index 0000000000..fc611985a7 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGAttachmentPhotoCell.h @@ -0,0 +1,7 @@ +#import "TGAttachmentAssetCell.h" + +@interface TGAttachmentPhotoCell : TGAttachmentAssetCell + +@end + +extern NSString *const TGAttachmentPhotoCellIdentifier; diff --git a/submodules/LegacyComponents/LegacyComponents/TGAttachmentPhotoCell.m b/submodules/LegacyComponents/LegacyComponents/TGAttachmentPhotoCell.m new file mode 100644 index 0000000000..91e93af96f --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGAttachmentPhotoCell.m @@ -0,0 +1,63 @@ +#import "TGAttachmentPhotoCell.h" +#import + +NSString *const TGAttachmentPhotoCellIdentifier = @"AttachmentPhotoCell"; + +@interface TGAttachmentPhotoCell () + +@end + +@implementation TGAttachmentPhotoCell + +- (UIImage *)transitionImage +{ + CGFloat scale = 1.0f; + CGSize scaledBoundsSize = CGSizeZero; + CGSize scaledImageSize = CGSizeZero; + + if (self.imageView.image.size.width > self.imageView.image.size.height) + { + scale = self.frame.size.height / self.imageView.image.size.height; + scaledBoundsSize = CGSizeMake(self.frame.size.width / scale, self.imageView.image.size.height); + + scaledImageSize = CGSizeMake(self.imageView.image.size.width * scale, self.imageView.image.size.height * scale); + + if (scaledImageSize.width < self.frame.size.width) + { + scale = self.frame.size.width / self.imageView.image.size.width; + scaledBoundsSize = CGSizeMake(self.imageView.image.size.width, self.frame.size.height / scale); + } + } + else + { + scale = self.frame.size.width / self.imageView.image.size.width; + scaledBoundsSize = CGSizeMake(self.imageView.image.size.width, self.frame.size.height / scale); + + scaledImageSize = CGSizeMake(self.imageView.image.size.width * scale, self.imageView.image.size.height * scale); + + if (scaledImageSize.width < self.frame.size.width) + { + scale = self.frame.size.height / self.imageView.image.size.height; + scaledBoundsSize = CGSizeMake(self.frame.size.width / scale, self.imageView.image.size.height); + } + } + + CGRect rect = self.bounds; + UIGraphicsBeginImageContextWithOptions(rect.size, false, 0.0f); + + CGContextRef context = UIGraphicsGetCurrentContext(); + [[UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, self.frame.size.width, self.frame.size.height) cornerRadius:TGAttachmentMenuCellCornerRadius] addClip]; + + CGContextScaleCTM(context, scale, scale); + [self.imageView.image drawInRect:CGRectMake((scaledBoundsSize.width - self.imageView.image.size.width) / 2, + (scaledBoundsSize.height - self.imageView.image.size.height) / 2, + self.imageView.image.size.width, + self.imageView.image.size.height)]; + + UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + + return image; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGAttachmentVideoCell.h b/submodules/LegacyComponents/LegacyComponents/TGAttachmentVideoCell.h new file mode 100644 index 0000000000..2b14488f92 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGAttachmentVideoCell.h @@ -0,0 +1,9 @@ +#import "TGAttachmentAssetCell.h" + +@interface TGAttachmentVideoCell : TGAttachmentAssetCell +{ + UILabel *_durationLabel; +} +@end + +extern NSString *const TGAttachmentVideoCellIdentifier; diff --git a/submodules/LegacyComponents/LegacyComponents/TGAttachmentVideoCell.m b/submodules/LegacyComponents/LegacyComponents/TGAttachmentVideoCell.m new file mode 100644 index 0000000000..30cabb5435 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGAttachmentVideoCell.m @@ -0,0 +1,301 @@ +#import "TGAttachmentVideoCell.h" + +#import "LegacyComponentsInternal.h" +#import "TGFont.h" + +#import + +#import +#import + +#import +#import +#import + +NSString *const TGAttachmentVideoCellIdentifier = @"AttachmentVideoCell"; + +@interface TGAttachmentVideoCell () +{ + SMetaDisposable *_adjustmentsDisposable; +} +@end + +@implementation TGAttachmentVideoCell + +- (instancetype)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + if (self != nil) + { + _iconView = [[UIImageView alloc] init]; + _iconView.contentMode = UIViewContentModeCenter; + [self addSubview:_iconView]; + + _durationLabel = [[UILabel alloc] initWithFrame:CGRectZero]; + _durationLabel.backgroundColor = [UIColor clearColor]; + _durationLabel.font = TGSystemFontOfSize(12); + _durationLabel.textColor = [UIColor whiteColor]; + [self addSubview:_durationLabel]; + + _gradientView.hidden = false; + + [self bringSubviewToFront:_cornersView]; + + _adjustmentsDisposable = [[SMetaDisposable alloc] init]; + + if (iosMajorVersion() >= 11) + { + _iconView.accessibilityIgnoresInvertColors = true; + _durationLabel.accessibilityIgnoresInvertColors = true; + } + } + return self; +} + +- (void)dealloc +{ + [_adjustmentsDisposable dispose]; +} + +- (void)setAsset:(TGMediaAsset *)asset signal:(SSignal *)signal +{ + [super setAsset:asset signal:signal]; + + _durationLabel.text = [NSString stringWithFormat:@"%d:%02d", (int)ceil(asset.videoDuration) / 60, (int)ceil(asset.videoDuration) % 60]; + [_durationLabel sizeToFit]; + CGRect durationFrame = _durationLabel.frame; + durationFrame.size = CGSizeMake(ceil(_durationLabel.frame.size.width), ceil(_durationLabel.frame.size.height)); + _durationLabel.frame = durationFrame; + + if (asset.subtypes & TGMediaAssetSubtypeVideoTimelapse) + _iconView.image = TGComponentsImageNamed(@"ModernMediaItemTimelapseIcon"); + else if (asset.subtypes & TGMediaAssetSubtypeVideoHighFrameRate) + _iconView.image = TGComponentsImageNamed(@"ModernMediaItemSloMoIcon"); + else + _iconView.image = TGComponentsImageNamed(@"ModernMediaItemVideoIcon"); + + SSignal *adjustmentsSignal = [self.editingContext adjustmentsSignalForItem:self.asset]; + + __weak TGAttachmentVideoCell *weakSelf = self; + [_adjustmentsDisposable setDisposable:[adjustmentsSignal startWithNext:^(TGVideoEditAdjustments *next) + { + __strong TGAttachmentVideoCell *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + if ([next isKindOfClass:[TGVideoEditAdjustments class]]) + [strongSelf _layoutImageForOriginalSize:next.originalSize cropRect:next.cropRect cropOrientation:next.cropOrientation]; + else + [strongSelf _layoutImageWithoutAdjustments]; + }]]; +} + +- (void)_transformLayoutForOrientation:(UIImageOrientation)orientation originalSize:(CGSize *)inOriginalSize cropRect:(CGRect *)inCropRect +{ + if (inOriginalSize == NULL || inCropRect == NULL) + return; + + CGSize originalSize = *inOriginalSize; + CGRect cropRect = *inCropRect; + + if (orientation == UIImageOrientationLeft) + { + cropRect = CGRectMake(cropRect.origin.y, originalSize.width - cropRect.size.width - cropRect.origin.x, cropRect.size.height, cropRect.size.width); + originalSize = CGSizeMake(originalSize.height, originalSize.width); + } + else if (orientation == UIImageOrientationRight) + { + cropRect = CGRectMake(originalSize.height - cropRect.size.height - cropRect.origin.y, cropRect.origin.x, cropRect.size.height, cropRect.size.width); + originalSize = CGSizeMake(originalSize.height, originalSize.width); + } + else if (orientation == UIImageOrientationDown) + { + cropRect = CGRectMake(originalSize.width - cropRect.size.width - cropRect.origin.x, originalSize.height - cropRect.size.height - cropRect.origin.y, cropRect.size.width, cropRect.size.height); + } + + *inOriginalSize = originalSize; + *inCropRect = cropRect; +} + +- (CGPoint)fittedCropCenterRect:(CGRect)cropRect scale:(CGFloat)scale +{ + CGSize size = CGSizeMake(cropRect.size.width * scale, cropRect.size.height * scale); + CGRect rect = CGRectMake(cropRect.origin.x * scale, cropRect.origin.y * scale, size.width, size.height); + + return CGPointMake(CGRectGetMidX(rect), CGRectGetMidY(rect)); +} + +- (void)_layoutImageForOriginalSize:(CGSize)originalSize cropRect:(CGRect)cropRect cropOrientation:(UIImageOrientation)cropOrientation +{ + self.imageView.transform = CGAffineTransformMakeRotation(TGRotationForOrientation(cropOrientation)); + + [self _transformLayoutForOrientation:cropOrientation originalSize:&originalSize cropRect:&cropRect]; + + CGSize scaledSize = TGScaleToFillSize(cropRect.size, self.frame.size); + CGFloat ratio = cropRect.size.width > cropRect.size.height ? scaledSize.width / cropRect.size.width : scaledSize.height / cropRect.size.height; + + CGSize fittedOriginalSize = CGSizeMake(originalSize.width * ratio, originalSize.height * ratio); + CGPoint centerPoint = CGPointMake(fittedOriginalSize.width / 2.0f, fittedOriginalSize.height / 2.0f); + + CGFloat scale = fittedOriginalSize.width / originalSize.width; + CGPoint centerOffset = TGPaintSubtractPoints(centerPoint, [self fittedCropCenterRect:cropRect scale:scale]); + + self.imageView.bounds = CGRectMake(0, 0, fittedOriginalSize.width, fittedOriginalSize.height); + self.imageView.center = TGPaintAddPoints(TGPaintCenterOfRect(self.bounds), centerOffset); + + //self.imageView.frame = CGRectMake(-cropRect.origin.x * ratio + (self.frame.size.width - fillSize.width) / 2, -cropRect.origin.y * ratio + (self.frame.size.height - fillSize.height) / 2, ); +} + +- (void)_layoutImageWithoutAdjustments +{ + self.imageView.transform = CGAffineTransformIdentity; + self.imageView.frame = self.bounds; +} + +- (void)layoutSubviews +{ + [super layoutSubviews]; + + _iconView.frame = CGRectMake(0, self.frame.size.height - 19, 19, 19); + + CGSize durationSize = _durationLabel.frame.size; + _durationLabel.frame = CGRectMake(self.frame.size.width - durationSize.width - 4, self.frame.size.height - durationSize.height - 2, durationSize.width, durationSize.height); +} + +- (UIImage *)transitionImageSquared +{ + UIGraphicsBeginImageContextWithOptions(self.bounds.size, false, 0.0f); + [[UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, self.frame.size.width, self.frame.size.height) cornerRadius:TGAttachmentMenuCellCornerRadius] addClip]; + + UIImage *image = self.imageView.image; + + CGSize originalSize = CGSizeZero; + CGRect cropRect = CGRectZero; + UIImageOrientation cropOrientation = UIImageOrientationUp; + + TGVideoEditAdjustments *adjustments = (TGVideoEditAdjustments *)[self.editingContext adjustmentsForItem:self.asset]; + if ([adjustments isKindOfClass:[TGVideoEditAdjustments class]]) + { + originalSize = adjustments.originalSize; + cropRect = adjustments.cropRect; + cropOrientation = adjustments.cropOrientation; + + __block UIImage *editedImage = nil; + [[self.editingContext thumbnailImageSignalForItem:self.asset withUpdates:false synchronous:true] startWithNext:^(UIImage *next) + { + editedImage = next; + }]; + + if (editedImage != nil) + image = editedImage; + } + + CGContextRef context = UIGraphicsGetCurrentContext(); + CGContextSaveGState(context); + + CGSize fillSize = TGScaleToFillSize(image.size, self.bounds.size); + if (CGRectEqualToRect(cropRect, CGRectZero)) + { + [image drawInRect:CGRectMake((self.bounds.size.width - fillSize.width) / 2, (self.bounds.size.height - fillSize.height) / 2, fillSize.width, fillSize.height)]; + } + else + { + CGContextConcatCTM(context, TGVideoCropTransformForOrientation(cropOrientation, self.frame.size, false)); + + CGFloat ratio = (cropRect.size.width > cropRect.size.height) ? self.frame.size.height / cropRect.size.height : self.frame.size.width / cropRect.size.width; + CGSize fillSize = CGSizeMake(cropRect.size.width * ratio, cropRect.size.height * ratio); + + [image drawInRect:CGRectMake(-cropRect.origin.x * ratio + (self.frame.size.width - fillSize.width) / 2, -cropRect.origin.y * ratio + (self.frame.size.height - fillSize.height) / 2, originalSize.width * ratio, originalSize.height * ratio)]; + } + + CGContextRestoreGState(context); + + UIImage *transitionImage = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + + return transitionImage; +} + +- (UIImage *)transitionImage +{ + if (fabs(self.frame.size.width - self.frame.size.height) < FLT_EPSILON) + return [self transitionImageSquared]; + + UIImage *sourceImage = self.imageView.image; + + CGSize originalSize = self.asset.dimensions; + CGRect cropRect = CGRectMake(0, 0, originalSize.width, originalSize.height); + UIImageOrientation cropOrientation = UIImageOrientationUp; + + TGVideoEditAdjustments *adjustments = (TGVideoEditAdjustments *)[self.editingContext adjustmentsForItem:self.asset]; + if ([adjustments isKindOfClass:[TGVideoEditAdjustments class]]) + { + originalSize = adjustments.originalSize; + cropRect = adjustments.cropRect; + cropOrientation = adjustments.cropOrientation; + + __block UIImage *editedImage = nil; + [[self.editingContext thumbnailImageSignalForItem:self.asset withUpdates:false synchronous:true] startWithNext:^(UIImage *next) + { + editedImage = next; + }]; + + if (editedImage != nil) + sourceImage = editedImage; + } + + CGSize fillSize = TGScaleToFillSize(cropRect.size, self.bounds.size); + UIImage *croppedImage = TGPhotoEditorVideoCrop(sourceImage, nil, cropOrientation, 0, cropRect, false, fillSize, originalSize, true, true); + + UIGraphicsBeginImageContextWithOptions(self.bounds.size, false, 0.0f); + + CGFloat scale = 1.0f; + CGSize scaledBoundsSize = CGSizeZero; + CGSize scaledImageSize = CGSizeZero; + + if (croppedImage.size.width > croppedImage.size.height) + { + scale = self.frame.size.height / croppedImage.size.height; + scaledBoundsSize = CGSizeMake(self.frame.size.width / scale, croppedImage.size.height); + + scaledImageSize = CGSizeMake(croppedImage.size.width * scale, croppedImage.size.height * scale); + + if (scaledImageSize.width < self.frame.size.width) + { + scale = self.frame.size.width / croppedImage.size.width; + scaledBoundsSize = CGSizeMake(croppedImage.size.width, self.frame.size.height / scale); + } + } + else + { + scale = self.frame.size.width / croppedImage.size.width; + scaledBoundsSize = CGSizeMake(croppedImage.size.width, self.frame.size.height / scale); + + scaledImageSize = CGSizeMake(croppedImage.size.width * scale, croppedImage.size.height * scale); + + if (scaledImageSize.width < self.frame.size.width) + { + scale = self.frame.size.height / croppedImage.size.height; + scaledBoundsSize = CGSizeMake(self.frame.size.width / scale, croppedImage.size.height); + } + } + + CGRect rect = self.bounds; + UIGraphicsBeginImageContextWithOptions(rect.size, false, 0.0f); + + CGContextRef context = UIGraphicsGetCurrentContext(); + [[UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, self.frame.size.width, self.frame.size.height) cornerRadius:TGAttachmentMenuCellCornerRadius] addClip]; + + CGContextScaleCTM(context, scale, scale); + [croppedImage drawInRect:CGRectMake((scaledBoundsSize.width - croppedImage.size.width) / 2, + (scaledBoundsSize.height - croppedImage.size.height) / 2, + croppedImage.size.width, + croppedImage.size.height)]; + + UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + + return image; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGAudioMediaAttachment.h b/submodules/LegacyComponents/LegacyComponents/TGAudioMediaAttachment.h new file mode 100644 index 0000000000..3861cc6cc2 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGAudioMediaAttachment.h @@ -0,0 +1,25 @@ +#import + +#define TGAudioMediaAttachmentType 0x3A0E7A32 + +@interface TGAudioMediaAttachment : TGMediaAttachment + +@property (nonatomic) int64_t audioId; +@property (nonatomic) int64_t accessHash; +@property (nonatomic) int32_t datacenterId; + +@property (nonatomic) int64_t localAudioId; + +@property (nonatomic) int32_t duration; +@property (nonatomic) int32_t fileSize; + +@property (nonatomic, strong) NSString *audioUri; + +/*- (NSString *)localFilePath; + ++ (NSString *)localAudioFileDirectoryForLocalAudioId:(int64_t)audioId; ++ (NSString *)localAudioFileDirectoryForRemoteAudioId:(int64_t)audioId; ++ (NSString *)localAudioFilePathForLocalAudioId:(int64_t)audioId; ++ (NSString *)localAudioFilePathForRemoteAudioId:(int64_t)audioId;*/ + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGAudioMediaAttachment.m b/submodules/LegacyComponents/LegacyComponents/TGAudioMediaAttachment.m new file mode 100644 index 0000000000..ba6bb7dc32 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGAudioMediaAttachment.m @@ -0,0 +1,71 @@ +#import "TGAudioMediaAttachment.h" + +@implementation TGAudioMediaAttachment + +- (instancetype)init +{ + self = [super init]; + if (self != nil) + { + self.type = TGAudioMediaAttachmentType; + } + return self; +} + +- (void)serialize:(NSMutableData *)data +{ + int dataLengthPtr = (int)data.length; + int32_t zero = 0; + [data appendBytes:&zero length:4]; + + [data appendBytes:&_audioId length:8]; + [data appendBytes:&_accessHash length:8]; + [data appendBytes:&_datacenterId length:4]; + + [data appendBytes:&_localAudioId length:8]; + + [data appendBytes:&_duration length:4]; + [data appendBytes:&_fileSize length:4]; + + NSData *audioUriData = [_audioUri dataUsingEncoding:NSUTF8StringEncoding]; + int32_t audioUriLength = (int32_t)audioUriData.length; + [data appendBytes:&audioUriLength length:4]; + if (audioUriLength != 0) + [data appendData:audioUriData]; + + int dataLength = (int)(data.length - dataLengthPtr - 4); + [data replaceBytesInRange:NSMakeRange(dataLengthPtr, 4) withBytes:&dataLength]; +} + +- (TGMediaAttachment *)parseMediaAttachment:(NSInputStream *)is +{ + int dataLength = 0; + [is read:(uint8_t *)&dataLength maxLength:4]; + + TGAudioMediaAttachment *attachment = [[TGAudioMediaAttachment alloc] init]; + + if (attachment != nil) + { + [is read:(uint8_t *)&attachment->_audioId maxLength:8]; + [is read:(uint8_t *)&attachment->_accessHash maxLength:8]; + [is read:(uint8_t *)&attachment->_datacenterId maxLength:4]; + + [is read:(uint8_t *)&attachment->_localAudioId maxLength:8]; + + [is read:(uint8_t *)&attachment->_duration maxLength:4]; + [is read:(uint8_t *)&attachment->_fileSize maxLength:4]; + + int32_t audioUriLength = 0; + [is read:(uint8_t *)&audioUriLength maxLength:4]; + if (audioUriLength != 0) + { + uint8_t *audioUriBytes = malloc(audioUriLength); + [is read:audioUriBytes maxLength:audioUriLength]; + attachment.audioUri = [[NSString alloc] initWithBytesNoCopy:audioUriBytes length:audioUriLength encoding:NSUTF8StringEncoding freeWhenDone:true]; + } + } + + return attachment; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGAudioWaveform.h b/submodules/LegacyComponents/LegacyComponents/TGAudioWaveform.h new file mode 100644 index 0000000000..9deed08ce3 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGAudioWaveform.h @@ -0,0 +1,15 @@ +#import + +#import + +@interface TGAudioWaveform : NSObject + +@property (nonatomic, strong, readonly) NSData *samples; +@property (nonatomic, readonly) int32_t peak; + +- (instancetype)initWithSamples:(NSData *)samples peak:(int32_t)peak; +- (instancetype)initWithBitstream:(NSData *)bitstream bitsPerSample:(NSUInteger)bitsPerSample; + +- (NSData *)bitstream; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGAudioWaveform.m b/submodules/LegacyComponents/LegacyComponents/TGAudioWaveform.m new file mode 100644 index 0000000000..0802460a06 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGAudioWaveform.m @@ -0,0 +1,85 @@ +#import "TGAudioWaveform.h" + +#import "LegacyComponentsInternal.h" + +#import "PSKeyValueCoder.h" + +static int32_t get_bits(uint8_t const *bytes, unsigned int bitOffset, unsigned int numBits) +{ + uint8_t const *data = bytes; + numBits = (unsigned int)pow(2, numBits) - 1; //this will only work up to 32 bits, of course + data += bitOffset / 8; + bitOffset %= 8; + return (*((int*)data) >> bitOffset) & numBits; +} + +static void set_bits(uint8_t *bytes, int32_t bitOffset, int32_t numBits, int32_t value) { + numBits = (unsigned int)pow(2, numBits) - 1; //this will only work up to 32 bits, of course + uint8_t *data = bytes; + data += bitOffset / 8; + bitOffset %= 8; + *((int32_t *)data) |= ((value) << bitOffset); +} + +@implementation TGAudioWaveform + +- (instancetype)initWithSamples:(NSData *)samples peak:(int32_t)peak { + self = [super init]; + if (self != nil) { + _samples = samples; + _peak = peak; + } + return self; +} + +- (instancetype)initWithCoder:(NSCoder *)aDecoder { + return [self initWithSamples:[aDecoder decodeObjectForKey:@"samples"] peak:[aDecoder decodeInt32ForKey:@"peak"]]; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder { + [aCoder encodeObject:_samples forKey:@"samples"]; + [aCoder encodeInt32:_peak forKey:@"peak"]; +} + +- (instancetype)initWithKeyValueCoder:(PSKeyValueCoder *)coder { + return [self initWithSamples:[coder decodeDataCorCKey:"samples"] peak:[coder decodeInt32ForCKey:"peak"]]; +} + +- (void)encodeWithKeyValueCoder:(PSKeyValueCoder *)coder { + [coder encodeData:_samples forCKey:"samples"]; + [coder encodeInt32:_peak forCKey:"peak"]; +} + +- (instancetype)initWithBitstream:(NSData *)bitstream bitsPerSample:(NSUInteger)bitsPerSample { + int numSamples = (int)(bitstream.length * 8 / bitsPerSample); + uint8_t const *bytes = bitstream.bytes; + int32_t maxSample = (1 << bitsPerSample) - 1; + NSMutableData *result = [[NSMutableData alloc] initWithLength:numSamples * 2]; + int16_t *samples = result.mutableBytes; + int32_t norm = (1 << bitsPerSample) - 1; + for (int i = 0; i < numSamples; i++) { + samples[i] = (int16_t)(((int64_t)get_bits(bytes, i * 5, 5) * maxSample) / norm); + } + return [self initWithSamples:result peak:31]; +} + +- (NSData *)bitstream { + int numSamples = (int)(_samples.length / 2); + int bitstreamLength = (numSamples * 5) / 8 + (((numSamples * 5) % 8) == 0 ? 0 : 1); + NSMutableData *result = [[NSMutableData alloc] initWithLength:bitstreamLength]; + int32_t maxSample = _peak; + uint16_t const *samples = _samples.bytes; + uint8_t *bytes = result.mutableBytes; + + for (int i = 0; i < numSamples; i++) { + int32_t value = MIN(31, ABS((int32_t)samples[i]) * 31 / maxSample); + set_bits(bytes, i * 5, 5, value & 31); + } + return result; +} + +- (BOOL)isEqual:(id)object { + return [object isKindOfClass:[TGAudioWaveform class]] && TGObjectCompare(_samples, ((TGAudioWaveform *)object)->_samples) && _peak == ((TGAudioWaveform *)object)->_peak; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGAuthorSignatureMediaAttachment.h b/submodules/LegacyComponents/LegacyComponents/TGAuthorSignatureMediaAttachment.h new file mode 100644 index 0000000000..dd55422c1d --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGAuthorSignatureMediaAttachment.h @@ -0,0 +1,15 @@ +#import + +@class TGImageMediaAttachment; +@class TGDocumentMediaAttachment; +@class TGWebPageMediaAttachment; + +#define TGAuthorSignatureMediaAttachmentType ((int)0x157b8516) + +@interface TGAuthorSignatureMediaAttachment : TGMediaAttachment + +@property (nonatomic, strong, readonly) NSString *signature; + +- (instancetype)initWithSignature:(NSString *)signature; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGAuthorSignatureMediaAttachment.m b/submodules/LegacyComponents/LegacyComponents/TGAuthorSignatureMediaAttachment.m new file mode 100644 index 0000000000..cc1f3a4920 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGAuthorSignatureMediaAttachment.m @@ -0,0 +1,46 @@ +#import "TGAuthorSignatureMediaAttachment.h" + +#import "LegacyComponentsInternal.h" + +#import "NSInputStream+TL.h" + +@implementation TGAuthorSignatureMediaAttachment + +- (instancetype)initWithSignature:(NSString *)signature { + self = [super init]; + if (self != nil) { + self.type = TGAuthorSignatureMediaAttachmentType; + + _signature = signature; + } + return self; +} + +- (instancetype)initWithCoder:(NSCoder *)aDecoder { + return [self initWithSignature:[aDecoder decodeObjectForKey:@"signature"]]; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder { + [aCoder encodeObject:_signature forKey:@"signature"]; +} + +- (void)serialize:(NSMutableData *)data +{ + NSData *serializedData = [NSKeyedArchiver archivedDataWithRootObject:self]; + int32_t length = (int32_t)serializedData.length; + [data appendBytes:&length length:4]; + [data appendData:serializedData]; +} + +- (TGMediaAttachment *)parseMediaAttachment:(NSInputStream *)is +{ + int32_t length = [is readInt32]; + NSData *data = [is readData:length]; + return [NSKeyedUnarchiver unarchiveObjectWithData:data]; +} + +- (BOOL)isEqual:(id)object { + return [object isKindOfClass:[TGAuthorSignatureMediaAttachment class]] && TGStringCompare(((TGAuthorSignatureMediaAttachment *)object)->_signature, _signature); +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGBackdropView.h b/submodules/LegacyComponents/LegacyComponents/TGBackdropView.h new file mode 100644 index 0000000000..9c1c39548b --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGBackdropView.h @@ -0,0 +1,7 @@ +#import + +@interface TGBackdropView : UIView + ++ (TGBackdropView *)viewWithLightNavigationBarStyle; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGBackdropView.m b/submodules/LegacyComponents/LegacyComponents/TGBackdropView.m new file mode 100644 index 0000000000..ad9a8b8066 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGBackdropView.m @@ -0,0 +1,14 @@ +#import "TGBackdropView.h" + +#import "LegacyComponentsInternal.h" + +@implementation TGBackdropView + ++ (TGBackdropView *)viewWithLightNavigationBarStyle +{ + TGBackdropView *view = [[TGBackdropView alloc] init]; + view.backgroundColor = UIColorRGBA(0xf7f7f7, 1.0f); + return view; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGBotComandInfo.h b/submodules/LegacyComponents/LegacyComponents/TGBotComandInfo.h new file mode 100644 index 0000000000..23be3747d7 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGBotComandInfo.h @@ -0,0 +1,12 @@ +#import + +#import + +@interface TGBotComandInfo : NSObject + +@property (nonatomic, strong, readonly) NSString *command; +@property (nonatomic, strong, readonly) NSString *commandDescription; + +- (instancetype)initWithCommand:(NSString *)command commandDescription:(NSString *)commandDescription; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGBotComandInfo.m b/submodules/LegacyComponents/LegacyComponents/TGBotComandInfo.m new file mode 100644 index 0000000000..9e18f66869 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGBotComandInfo.m @@ -0,0 +1,29 @@ +#import "TGBotComandInfo.h" + +#import "PSKeyValueCoder.h" + +@implementation TGBotComandInfo + +- (instancetype)initWithCommand:(NSString *)command commandDescription:(NSString *)commandDescription +{ + self = [super init]; + if (self != nil) + { + _command = command; + _commandDescription = commandDescription; + } + return self; +} + +- (instancetype)initWithKeyValueCoder:(PSKeyValueCoder *)coder +{ + return [self initWithCommand:[coder decodeStringForCKey:"command"] commandDescription:[coder decodeStringForCKey:"commandDescription"]]; +} + +- (void)encodeWithKeyValueCoder:(PSKeyValueCoder *)coder +{ + [coder encodeString:_command forCKey:"command"]; + [coder encodeString:_commandDescription forCKey:"commandDescription"]; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGBotContextResultAttachment.h b/submodules/LegacyComponents/LegacyComponents/TGBotContextResultAttachment.h new file mode 100644 index 0000000000..a2095ac83b --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGBotContextResultAttachment.h @@ -0,0 +1,16 @@ +#import + +#import +#import + +#define TGBotContextResultAttachmentType ((int)0x1718023f) + +@interface TGBotContextResultAttachment : TGMediaAttachment + +@property (nonatomic, readonly) int32_t userId; +@property (nonatomic, strong, readonly) NSString *resultId; +@property (nonatomic, readonly) int64_t queryId; + +- (instancetype)initWithUserId:(int32_t)userId resultId:(NSString *)resultId queryId:(int64_t)queryId; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGBotContextResultAttachment.m b/submodules/LegacyComponents/LegacyComponents/TGBotContextResultAttachment.m new file mode 100644 index 0000000000..862c6de745 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGBotContextResultAttachment.m @@ -0,0 +1,44 @@ +#import "TGBotContextResultAttachment.h" + +#import "NSInputStream+TL.h" + +@implementation TGBotContextResultAttachment + +- (instancetype)initWithUserId:(int32_t)userId resultId:(NSString *)resultId queryId:(int64_t)queryId { + self = [super init]; + if (self != nil) { + self.type = TGBotContextResultAttachmentType; + _userId = userId; + _resultId = resultId; + _queryId = queryId; + } + return self; +} + +- (instancetype)initWithCoder:(NSCoder *)aDecoder { + return [self initWithUserId:[aDecoder decodeInt32ForKey:@"userId"] resultId:[aDecoder decodeObjectForKey:@"resultId"] queryId:[aDecoder decodeInt64ForKey:@"queryId"]]; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder { + [aCoder encodeInt32:_userId forKey:@"userId"]; + [aCoder encodeObject:_resultId forKey:@"resultId"]; + [aCoder encodeInt64:_queryId forKey:@"queryId"]; +} + + +- (void)serialize:(NSMutableData *)data +{ + NSData *serializedData = [NSKeyedArchiver archivedDataWithRootObject:self]; + int32_t length = (int32_t)serializedData.length; + [data appendBytes:&length length:4]; + [data appendData:serializedData]; +} + +- (TGMediaAttachment *)parseMediaAttachment:(NSInputStream *)is +{ + int32_t length = [is readInt32]; + NSData *data = [is readData:length]; + return [NSKeyedUnarchiver unarchiveObjectWithData:data]; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGBotInfo.h b/submodules/LegacyComponents/LegacyComponents/TGBotInfo.h new file mode 100644 index 0000000000..47cb547ebf --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGBotInfo.h @@ -0,0 +1,15 @@ +#import + +#import +#import + +@interface TGBotInfo : NSObject + +@property (nonatomic, readonly) int32_t version; +@property (nonatomic, strong, readonly) NSString *shortDescription; +@property (nonatomic, strong, readonly) NSString *botDescription; +@property (nonatomic, strong, readonly) NSArray *commandList; + +- (instancetype)initWithVersion:(int32_t)version shortDescription:(NSString *)shortDescription botDescription:(NSString *)botDescription commandList:(NSArray *)commandList; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGBotInfo.m b/submodules/LegacyComponents/LegacyComponents/TGBotInfo.m new file mode 100644 index 0000000000..f575200bad --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGBotInfo.m @@ -0,0 +1,33 @@ +#import "TGBotInfo.h" + +#import "PSKeyValueCoder.h" + +@implementation TGBotInfo + +- (instancetype)initWithVersion:(int32_t)version shortDescription:(NSString *)shortDescription botDescription:(NSString *)botDescription commandList:(NSArray *)commandList +{ + self = [super init]; + if (self != nil) + { + _version = version; + _botDescription = botDescription; + _shortDescription = shortDescription; + _commandList = commandList; + } + return self; +} + +- (instancetype)initWithKeyValueCoder:(PSKeyValueCoder *)coder +{ + return [self initWithVersion:[coder decodeInt32ForCKey:"version"] shortDescription:[coder decodeStringForCKey:"shortDescription"] botDescription:[coder decodeStringForCKey:"botDescription"] commandList:[coder decodeArrayForCKey:"commandList"]]; +} + +- (void)encodeWithKeyValueCoder:(PSKeyValueCoder *)coder +{ + [coder encodeInt32:_version forCKey:"version"]; + [coder encodeString:_shortDescription forCKey:"shortDescription"]; + [coder encodeString:_botDescription forCKey:"botDescription"]; + [coder encodeArray:_commandList forCKey:"commandList"]; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGBotReplyMarkup.h b/submodules/LegacyComponents/LegacyComponents/TGBotReplyMarkup.h new file mode 100644 index 0000000000..7dccceda62 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGBotReplyMarkup.h @@ -0,0 +1,24 @@ +#import + +#import + +#import + +@interface TGBotReplyMarkup : NSObject + +@property (nonatomic, readonly) int32_t userId; +@property (nonatomic, readonly) int32_t messageId; +@property (nonatomic, strong, readonly) NSArray *rows; +@property (nonatomic) bool matchDefaultHeight; +@property (nonatomic) bool hideKeyboardOnActivation; +@property (nonatomic) bool alreadyActivated; +@property (nonatomic) bool manuallyHidden; +@property (nonatomic) bool isInline; + +- (instancetype)initWithUserId:(int32_t)userId messageId:(int32_t)messageId rows:(NSArray *)rows matchDefaultHeight:(bool)matchDefaultHeight hideKeyboardOnActivation:(bool)hideKeyboardOnActivation alreadyActivated:(bool)alreadyActivated manuallyHidden:(bool)manuallyHidden isInline:(bool)isInline; + +- (TGBotReplyMarkup *)activatedMarkup; +- (TGBotReplyMarkup *)manuallyHide; +- (TGBotReplyMarkup *)manuallyUnhide; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGBotReplyMarkup.m b/submodules/LegacyComponents/LegacyComponents/TGBotReplyMarkup.m new file mode 100644 index 0000000000..9c9bfcdb6a --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGBotReplyMarkup.m @@ -0,0 +1,87 @@ +#import "TGBotReplyMarkup.h" + +#import "PSKeyValueCoder.h" + +@implementation TGBotReplyMarkup + +- (instancetype)initWithUserId:(int32_t)userId messageId:(int32_t)messageId rows:(NSArray *)rows matchDefaultHeight:(bool)matchDefaultHeight hideKeyboardOnActivation:(bool)hideKeyboardOnActivation alreadyActivated:(bool)alreadyActivated manuallyHidden:(bool)manuallyHidden isInline:(bool)isInline +{ + self = [super init]; + if (self != nil) + { + _userId = userId; + _messageId = messageId; + _rows = rows; + _matchDefaultHeight = matchDefaultHeight; + _hideKeyboardOnActivation = hideKeyboardOnActivation; + _alreadyActivated = alreadyActivated; + _manuallyHidden = manuallyHidden; + _isInline = isInline; + } + return self; +} + +- (instancetype)initWithKeyValueCoder:(PSKeyValueCoder *)coder +{ + return [self initWithUserId:[coder decodeInt32ForCKey:"userId"] messageId:[coder decodeInt32ForCKey:"messageId"] rows:[coder decodeArrayForCKey:"rows"] matchDefaultHeight:[coder decodeInt32ForCKey:"matchDefaultHeight"] hideKeyboardOnActivation:[coder decodeInt32ForCKey:"hideKeyboardOnActivation"] alreadyActivated:[coder decodeInt32ForCKey:"alreadyActivated"] manuallyHidden:[coder decodeInt32ForCKey:"manuallyHidden"] isInline:[coder decodeInt32ForCKey:"isInline"]]; +} + +- (void)encodeWithKeyValueCoder:(PSKeyValueCoder *)coder +{ + [coder encodeInt32:_userId forCKey:"userId"]; + [coder encodeInt32:_messageId forCKey:"messageId"]; + [coder encodeArray:_rows forCKey:"rows"]; + [coder encodeInt32:_matchDefaultHeight forCKey:"matchDefaultHeight"]; + [coder encodeInt32:_hideKeyboardOnActivation forCKey:"hideKeyboardOnActivation"]; + [coder encodeInt32:_alreadyActivated forCKey:"alreadyActivated"]; + [coder encodeInt32:_manuallyHidden forCKey:"manuallyHidden"]; + [coder encodeInt32:_isInline forCKey:"isInline"]; +} + +- (instancetype)initWithCoder:(NSCoder *)aDecoder { + return [self initWithUserId:[aDecoder decodeInt32ForKey:@"userId"] messageId:[aDecoder decodeInt32ForKey:@"messageId"] rows:[aDecoder decodeObjectForKey:@"rows"] matchDefaultHeight:[aDecoder decodeBoolForKey:@"matchDefaultHeight"] hideKeyboardOnActivation:[aDecoder decodeBoolForKey:@"hideKeyboardOnDeactivation"] alreadyActivated:[aDecoder decodeBoolForKey:@"alreadyActivated"] manuallyHidden:[aDecoder decodeBoolForKey:@"manuallyHidden"] isInline:[aDecoder decodeBoolForKey:@"isInline"]]; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder { + [aCoder encodeInt32:_userId forKey:@"userId"]; + [aCoder encodeInt32:_messageId forKey:@"messageId"]; + [aCoder encodeObject:_rows forKey:@"rows"]; + [aCoder encodeBool:_matchDefaultHeight forKey:@"matchDefaultHeight"]; + [aCoder encodeBool:_hideKeyboardOnActivation forKey:@"hideKeyboardOnActivation"]; + [aCoder encodeBool:_alreadyActivated forKey:@"alreadyActivated"]; + [aCoder encodeBool:_manuallyHidden forKey:@"manuallyHidden"]; + [aCoder encodeBool:_isInline forKey:@"isInline"]; +} + +- (BOOL)isEqual:(id)object +{ + return [object isKindOfClass:[TGBotReplyMarkup class]] && [((TGBotReplyMarkup *)object)->_rows isEqual:_rows] && ((TGBotReplyMarkup *)object)->_userId == _userId && ((TGBotReplyMarkup *)object)->_messageId == _messageId && ((TGBotReplyMarkup *)object)->_matchDefaultHeight == _matchDefaultHeight && _isInline == ((TGBotReplyMarkup *)object)->_isInline; +} + +- (TGBotReplyMarkup *)activatedMarkup +{ + if (_alreadyActivated) + return self; + + return [[TGBotReplyMarkup alloc] initWithUserId:_userId messageId:_messageId rows:_rows matchDefaultHeight:_matchDefaultHeight hideKeyboardOnActivation:_hideKeyboardOnActivation alreadyActivated:true manuallyHidden:_manuallyHidden isInline:_isInline]; +} + +- (TGBotReplyMarkup *)manuallyHide +{ + if (_manuallyHidden) { + return self; + } + + return [[TGBotReplyMarkup alloc] initWithUserId:_userId messageId:_messageId rows:_rows matchDefaultHeight:_matchDefaultHeight hideKeyboardOnActivation:_hideKeyboardOnActivation alreadyActivated:_alreadyActivated manuallyHidden:true isInline:_isInline]; +} + +- (TGBotReplyMarkup *)manuallyUnhide +{ + if (!_manuallyHidden) { + return self; + } + + return [[TGBotReplyMarkup alloc] initWithUserId:_userId messageId:_messageId rows:_rows matchDefaultHeight:_matchDefaultHeight hideKeyboardOnActivation:_hideKeyboardOnActivation alreadyActivated:_alreadyActivated manuallyHidden:false isInline:_isInline]; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGBotReplyMarkupButton.h b/submodules/LegacyComponents/LegacyComponents/TGBotReplyMarkupButton.h new file mode 100644 index 0000000000..75ec8be851 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGBotReplyMarkupButton.h @@ -0,0 +1,61 @@ +#import + +#import + +@interface TGBotReplyMarkupButtonActionUrl : NSObject + +@property (nonatomic, strong, readonly) NSString *url; + +- (instancetype)initWithUrl:(NSString *)url; + +@end + +@interface TGBotReplyMarkupButtonActionCallback : NSObject + +@property (nonatomic, strong, readonly) NSData *data; + +- (instancetype)initWithData:(NSData *)data; + +@end + +@interface TGBotReplyMarkupButtonActionRequestPhone : NSObject + +@end + +@interface TGBotReplyMarkupButtonActionRequestLocation : NSObject + +@end + +@interface TGBotReplyMarkupButtonActionSwitchInline : NSObject + +@property (nonatomic, strong, readonly) NSString *query; +@property (nonatomic, readonly) bool samePeer; + +- (instancetype)initWithQuery:(NSString *)query samePeer:(bool)samePeer; + +@end + +@interface TGBotReplyMarkupButtonActionGame : NSObject + +@property (nonatomic, strong, readonly) NSString *text; + +- (instancetype)initWithText:(NSString *)text; + +@end + +@interface TGBotReplyMarkupButtonActionPurchase : NSObject + +@property (nonatomic, strong, readonly) NSString *text; + +- (instancetype)initWithText:(NSString *)text; + +@end + +@interface TGBotReplyMarkupButton : NSObject + +@property (nonatomic, strong, readonly) NSString *text; +@property (nonatomic, strong, readonly) id action; + +- (instancetype)initWithText:(NSString *)text action:(id)action; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGBotReplyMarkupButton.m b/submodules/LegacyComponents/LegacyComponents/TGBotReplyMarkupButton.m new file mode 100644 index 0000000000..f1e0010427 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGBotReplyMarkupButton.m @@ -0,0 +1,262 @@ +#import "TGBotReplyMarkupButton.h" + +#import "LegacyComponentsInternal.h" + +#import "PSKeyValueCoder.h" + +@implementation TGBotReplyMarkupButtonActionUrl + +- (instancetype)initWithUrl:(NSString *)url { + self = [super init]; + if (self != nil) { + _url = url; + } + return self; +} + +- (instancetype)initWithKeyValueCoder:(PSKeyValueCoder *)coder { + return [self initWithUrl:[coder decodeStringForCKey:"url"]]; +} + +- (void)encodeWithKeyValueCoder:(PSKeyValueCoder *)coder { + [coder encodeString:_url forCKey:"url"]; +} + +- (instancetype)initWithCoder:(NSCoder *)aDecoder { + return [self initWithUrl:[aDecoder decodeObjectForKey:@"url"]]; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder { + [aCoder encodeObject:_url forKey:@"url"]; +} + +- (BOOL)isEqual:(id)object { + return [object isKindOfClass:[TGBotReplyMarkupButtonActionUrl class]] && TGStringCompare(_url, ((TGBotReplyMarkupButtonActionUrl *)object)->_url); +} + +@end + +@implementation TGBotReplyMarkupButtonActionCallback + +- (instancetype)initWithData:(NSData *)data { + self = [super init]; + if (self != nil) { + _data = data; + } + return self; +} + +- (instancetype)initWithKeyValueCoder:(PSKeyValueCoder *)coder { + return [self initWithData:[coder decodeDataCorCKey:"data"]]; +} + +- (void)encodeWithKeyValueCoder:(PSKeyValueCoder *)coder { + [coder encodeData:_data forCKey:"data"]; +} + +- (instancetype)initWithCoder:(NSCoder *)aDecoder { + return [self initWithData:[aDecoder decodeObjectForKey:@"data"]]; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder { + [aCoder encodeObject:_data forKey:@"data"]; +} + +- (BOOL)isEqual:(id)object { + return [object isKindOfClass:[TGBotReplyMarkupButtonActionCallback class]] && TGObjectCompare(_data, ((TGBotReplyMarkupButtonActionCallback *)object)->_data); +} + +@end + +@implementation TGBotReplyMarkupButtonActionRequestPhone + +- (instancetype)init { + return [super init]; +} + +- (instancetype)initWithKeyValueCoder:(PSKeyValueCoder *)__unused coder { + return [self init]; +} + +- (void)encodeWithKeyValueCoder:(PSKeyValueCoder *)__unused coder { +} + +- (instancetype)initWithCoder:(NSCoder *)__unused aDecoder { + return [self init]; +} + +- (void)encodeWithCoder:(NSCoder *)__unused aCoder { +} + +- (BOOL)isEqual:(id)object { + return [object isKindOfClass:[TGBotReplyMarkupButtonActionRequestPhone class]]; +} + +@end + +@implementation TGBotReplyMarkupButtonActionRequestLocation + +- (instancetype)init { + return [super init]; +} + +- (instancetype)initWithKeyValueCoder:(PSKeyValueCoder *)__unused coder { + return [self init]; +} + +- (void)encodeWithKeyValueCoder:(PSKeyValueCoder *)__unused coder { +} + +- (instancetype)initWithCoder:(NSCoder *)__unused aDecoder { + return [self init]; +} + +- (void)encodeWithCoder:(NSCoder *)__unused aCoder { +} + +- (BOOL)isEqual:(id)object { + return [object isKindOfClass:[TGBotReplyMarkupButtonActionRequestLocation class]]; +} + +@end + +@implementation TGBotReplyMarkupButtonActionSwitchInline + +- (instancetype)initWithQuery:(NSString *)query samePeer:(bool)samePeer { + self = [super init]; + if (self != nil) { + _query = query; + _samePeer = samePeer; + } + return self; +} + +- (instancetype)initWithKeyValueCoder:(PSKeyValueCoder *)coder { + return [self initWithQuery:[coder decodeStringForCKey:"query"] samePeer:[coder decodeInt32ForCKey:"samePeer"]]; +} + +- (void)encodeWithKeyValueCoder:(PSKeyValueCoder *)coder { + [coder encodeString:_query forCKey:"query"]; + [coder encodeInt32:_samePeer ? 1 : 0 forCKey:"samePeer"]; +} + +- (instancetype)initWithCoder:(NSCoder *)aDecoder { + return [self initWithQuery:[aDecoder decodeObjectForKey:@"query"] samePeer:[aDecoder decodeBoolForKey:@"samePeer"]]; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder { + [aCoder encodeObject:_query forKey:@"query"]; + [aCoder encodeBool:_samePeer forKey:@"samePeer"]; +} + +- (BOOL)isEqual:(id)object { + return [object isKindOfClass:[TGBotReplyMarkupButtonActionSwitchInline class]] && TGStringCompare(_query, ((TGBotReplyMarkupButtonActionSwitchInline *)object)->_query) && _samePeer == ((TGBotReplyMarkupButtonActionSwitchInline *)object)->_samePeer; +} + +@end + +@implementation TGBotReplyMarkupButtonActionGame + +- (instancetype)initWithText:(NSString *)text { + self = [super init]; + if (self != nil) { + _text = text; + } + return self; +} + +- (instancetype)initWithKeyValueCoder:(PSKeyValueCoder *)coder { + return [self initWithText:[coder decodeStringForCKey:"text"]]; +} + +- (void)encodeWithKeyValueCoder:(PSKeyValueCoder *)coder { + [coder encodeString:_text forCKey:"text"]; +} + + +- (instancetype)initWithCoder:(NSCoder *)aDecoder { + return [self initWithText:[aDecoder decodeObjectForKey:@"text"]]; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder { + [aCoder encodeObject:_text forKey:@"text"]; +} + +- (BOOL)isEqual:(id)object { + return [object isKindOfClass:[TGBotReplyMarkupButtonActionGame class]] && TGStringCompare(_text, ((TGBotReplyMarkupButtonActionGame *)object)->_text); +} + +@end + +@implementation TGBotReplyMarkupButtonActionPurchase + +- (instancetype)initWithText:(NSString *)text { + self = [super init]; + if (self != nil) { + _text = text; + } + return self; +} + +- (instancetype)initWithKeyValueCoder:(PSKeyValueCoder *)coder { + return [self initWithText:[coder decodeStringForCKey:"text"]]; +} + +- (void)encodeWithKeyValueCoder:(PSKeyValueCoder *)coder { + [coder encodeString:_text forCKey:"text"]; +} + + +- (instancetype)initWithCoder:(NSCoder *)aDecoder { + return [self initWithText:[aDecoder decodeObjectForKey:@"text"]]; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder { + [aCoder encodeObject:_text forKey:@"text"]; +} + +- (BOOL)isEqual:(id)object { + return [object isKindOfClass:[TGBotReplyMarkupButtonActionPurchase class]] && TGStringCompare(_text, ((TGBotReplyMarkupButtonActionPurchase *)object)->_text); +} + +@end + +@implementation TGBotReplyMarkupButton + +- (instancetype)initWithText:(NSString *)text action:(id)action +{ + self = [super init]; + if (self != nil) + { + _text = text; + _action = action; + } + return self; +} + +- (instancetype)initWithKeyValueCoder:(PSKeyValueCoder *)coder +{ + return [self initWithText:[coder decodeStringForCKey:"text"] action:(id)[coder decodeObjectForKey:@"action"]]; +} + +- (void)encodeWithKeyValueCoder:(PSKeyValueCoder *)coder +{ + [coder encodeString:_text forCKey:"text"]; + [coder encodeObject:_action forCKey:"action"]; +} + +- (instancetype)initWithCoder:(NSCoder *)aDecoder { + return [self initWithText:[aDecoder decodeObjectForKey:@"text"] action:[aDecoder decodeObjectForKey:@"action"]]; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder { + [aCoder encodeObject:_text forKey:@"text"]; + [aCoder encodeObject:_action forKey:@"action"]; +} + +- (BOOL)isEqual:(id)object +{ + return [object isKindOfClass:[TGBotReplyMarkupButton class]] && [((TGBotReplyMarkupButton *)object)->_text isEqualToString:_text] && TGObjectCompare(_action, ((TGBotReplyMarkupButton *)object)->_action); +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGBotReplyMarkupRow.h b/submodules/LegacyComponents/LegacyComponents/TGBotReplyMarkupRow.h new file mode 100644 index 0000000000..b33c40a8dc --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGBotReplyMarkupRow.h @@ -0,0 +1,13 @@ +#import + +#import + +#import + +@interface TGBotReplyMarkupRow : NSObject + +@property (nonatomic, strong, readonly) NSArray *buttons; + +- (instancetype)initWithButtons:(NSArray *)buttons; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGBotReplyMarkupRow.m b/submodules/LegacyComponents/LegacyComponents/TGBotReplyMarkupRow.m new file mode 100644 index 0000000000..edd4296131 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGBotReplyMarkupRow.m @@ -0,0 +1,40 @@ +#import "TGBotReplyMarkupRow.h" + +#import "PSKeyValueCoder.h" + +@implementation TGBotReplyMarkupRow + +- (instancetype)initWithButtons:(NSArray *)buttons +{ + self = [super init]; + if (self != nil) + { + _buttons = buttons; + } + return self; +} + +- (instancetype)initWithKeyValueCoder:(PSKeyValueCoder *)coder +{ + return [self initWithButtons:[coder decodeArrayForCKey:"buttons"]]; +} + +- (void)encodeWithKeyValueCoder:(PSKeyValueCoder *)coder +{ + [coder encodeArray:_buttons forCKey:"buttons"]; +} + +- (instancetype)initWithCoder:(NSCoder *)aDecoder { + return [self initWithButtons:[aDecoder decodeObjectForKey:@"buttons"]]; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder { + [aCoder encodeObject:_buttons forKey:@"buttons"]; +} + +- (BOOL)isEqual:(id)object +{ + return [object isKindOfClass:[TGBotReplyMarkupRow class]] && [((TGBotReplyMarkupRow *)object)->_buttons isEqual:_buttons]; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGBuiltinWallpaperInfo.h b/submodules/LegacyComponents/LegacyComponents/TGBuiltinWallpaperInfo.h new file mode 100644 index 0000000000..256c0cf625 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGBuiltinWallpaperInfo.h @@ -0,0 +1,13 @@ +#import + +@interface TGBuiltinWallpaperInfo : TGWallpaperInfo + +- (instancetype)initWithBuiltinId:(int)builtinId; +- (instancetype)initWithBuiltinId:(int)builtinId tintColor:(int)tintColor systemAlpha:(CGFloat)systemAlpha buttonsAlpha:(CGFloat)buttonsAlpha highlightedButtonAlpha:(CGFloat)highlightedButtonAlpha progressAlpha:(CGFloat)progressAlpha version:(int32_t)version; + +- (BOOL)isDefault; +- (int32_t)version; + +@end + +extern const int32_t TGBuilitinWallpaperCurrentVersion; diff --git a/submodules/LegacyComponents/LegacyComponents/TGBuiltinWallpaperInfo.m b/submodules/LegacyComponents/LegacyComponents/TGBuiltinWallpaperInfo.m new file mode 100644 index 0000000000..8914297b8e --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGBuiltinWallpaperInfo.m @@ -0,0 +1,140 @@ +#import "TGBuiltinWallpaperInfo.h" + +const int32_t TGBuilitinWallpaperCurrentVersion = 1; + +@interface TGBuiltinWallpaperInfo () +{ + int _builtinId; + int _tintColor; + int32_t _version; + + CGFloat _systemAlpha; + CGFloat _buttonsAlpha; + CGFloat _highlightedButtonAlpha; + CGFloat _progressAlpha; +} + +@end + +@implementation TGBuiltinWallpaperInfo + +- (instancetype)initWithBuiltinId:(int)builtinId +{ + return [self initWithBuiltinId:builtinId tintColor:0x000000 systemAlpha:0.25f buttonsAlpha:0.35f highlightedButtonAlpha:0.50f progressAlpha:0.35f version:1]; +} + +- (instancetype)initWithBuiltinId:(int)builtinId tintColor:(int)tintColor systemAlpha:(CGFloat)systemAlpha buttonsAlpha:(CGFloat)buttonsAlpha highlightedButtonAlpha:(CGFloat)highlightedButtonAlpha progressAlpha:(CGFloat)progressAlpha version:(int32_t)version +{ + self = [super init]; + if (self != nil) + { + _builtinId = builtinId; + _tintColor = tintColor; + _systemAlpha = systemAlpha; + _buttonsAlpha = buttonsAlpha; + _highlightedButtonAlpha = highlightedButtonAlpha; + _progressAlpha = progressAlpha; + _version = version; + } + return self; +} + +- (BOOL)isDefault +{ + return _builtinId == 0; +} + +- (int32_t)version +{ + return _version; +} + +- (NSString *)thumbnailUrl +{ + return [[NSString alloc] initWithFormat:@"builtin-wallpaper://?id=%d&size=thumbnail", _builtinId]; +} + +- (NSString *)fullscreenUrl +{ + return [[NSString alloc] initWithFormat:@"builtin-wallpaper://?id=%d", _builtinId]; +} + +- (int)tintColor +{ + return _tintColor; +} + +- (CGFloat)systemAlpha +{ + return _systemAlpha; +} + +- (CGFloat)buttonsAlpha +{ + return _buttonsAlpha; +} + +- (CGFloat)highlightedButtonAlpha +{ + return _highlightedButtonAlpha; +} + +- (CGFloat)progressAlpha +{ + return _progressAlpha; +} + +- (UIImage *)image +{ + NSString *filePath = [[NSBundle mainBundle] pathForResource:[[NSString alloc] initWithFormat:@"%@builtin-wallpaper-%d", [[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad ? @"pad-" : @"", _builtinId] ofType:@"jpg"]; + + return [[UIImage alloc] initWithContentsOfFile:filePath]; +} + +- (NSData *)imageData +{ + NSString *filePath = [[NSBundle mainBundle] pathForResource:[[NSString alloc] initWithFormat:@"%@builtin-wallpaper-%d", [[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad ? @"pad-" : @"", _builtinId] ofType:@"jpg"]; + + return [[NSData alloc] initWithContentsOfFile:filePath]; +} + +- (bool)hasData +{ + return true; +} + +- (BOOL)isEqual:(id)object +{ + if ([object isKindOfClass:[TGBuiltinWallpaperInfo class]]) + { + if (((TGBuiltinWallpaperInfo *)object)->_builtinId == _builtinId && + ((TGBuiltinWallpaperInfo *)object)->_tintColor == _tintColor && + ((TGBuiltinWallpaperInfo *)object)->_version == _version) + { + return true; + } + } + + return false; +} + +- (NSDictionary *)infoDictionary +{ + return @{ + @"_className": NSStringFromClass([self class]), + @"builtinId": @(_builtinId), + @"tintColor": @(_tintColor), + @"systemAlpha": @(_systemAlpha), + @"buttonsAlpha": @(_buttonsAlpha), + @"highlightedButtonAlpha": @(_highlightedButtonAlpha), + @"progressAlpha": @(_progressAlpha), + @"version": @(_version) + }; +} + ++ (TGWallpaperInfo *)infoWithDictionary:(NSDictionary *)dict +{ + return [[TGBuiltinWallpaperInfo alloc] initWithBuiltinId:[dict[@"builtinId"] intValue] tintColor:[dict[@"tintColor"] intValue] systemAlpha:[dict[@"systemAlpha"] floatValue] buttonsAlpha:[dict[@"buttonsAlpha"] floatValue] highlightedButtonAlpha:[dict[@"highlightedButtonAlpha"] floatValue] progressAlpha:[dict[@"progressAlpha"] floatValue] version:[dict[@"version"] intValue]]; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGCache.h b/submodules/LegacyComponents/LegacyComponents/TGCache.h new file mode 100644 index 0000000000..f31845d248 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGCache.h @@ -0,0 +1,58 @@ +#import + +typedef enum { + TGCacheMemory = 1, + TGCacheDisk = 2, + TGCacheBoth = 1 | 2 +} TGCacheLocation; + +typedef UIImage *(^TGCacheJpegDecodingBlock)(NSData *data); + +@interface TGCache : NSObject + +@property (nonatomic) int imageMemoryLimit; +@property (nonatomic) int imageMemoryEvictionInterval; + +@property (nonatomic) int thumbnailMemoryLimit; +@property (nonatomic) int thumbnailEvictionInterval; +@property (nonatomic) int thumbnailBackgroundBaseline; + +@property (nonatomic) int dataMemoryLimit; +@property (nonatomic) int dataMemoryEvictionInterval; + +@property (nonatomic) int memoryWarningBaseline; +@property (nonatomic) int backgroundBaseline; + +@property (nonatomic) int diskLimit; +@property (nonatomic) int diskEvictionInterval; + +@property (nonatomic, strong, readonly) NSString *diskCachePath; + ++ (dispatch_queue_t)diskCacheQueue; ++ (NSFileManager *)diskFileManager; + +- (id)init; +- (id)initWithCachesPath:(NSString *)cachesPath; + +- (void)addTemporaryCachedImagesSource:(NSDictionary *)source autoremove:(bool)autoremove; +- (void)removeTemporaryCachedImageSource:(NSDictionary *)source; + +- (void)cacheImage:(UIImage *)image withData:(NSData *)data url:(NSString *)url availability:(int)availability; +- (void)cacheImage:(UIImage *)image withData:(NSData *)data url:(NSString *)url availability:(int)availability completion:(void (^)(void))completion; +- (UIImage *)cachedImage:(NSString *)url availability:(int)availability; +- (void)removeFromMemoryCache:(NSString *)url matchEnd:(bool)matchEnd; +- (NSString *)pathForCachedData:(NSString *)url; + +- (void)cacheThumbnail:(UIImage *)image url:(NSString *)url; +- (UIImage *)cachedThumbnail:(NSString *)url; + +- (void)diskCacheContains:(NSString *)url1 orUrl:(NSString *)url2 completion:(void (^)(bool containsFirst, bool containsSecond))completion; +- (bool)diskCacheContainsSync:(NSString *)url; +- (void)removeFromDiskCache:(NSString *)url; +- (void)changeCacheItemUrl:(NSString *)oldUrl newUrl:(NSString *)newUrl; +- (void)moveToCache:(NSString *)fileUrl cacheUrl:(NSString *)cacheUrl; +- (void)clearCache:(int)availability; +- (NSArray *)storeMemoryCache; +- (void)restoreMemoryCache:(NSArray *)urlArray; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGCache.m b/submodules/LegacyComponents/LegacyComponents/TGCache.m new file mode 100644 index 0000000000..e202bfe64e --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGCache.m @@ -0,0 +1,825 @@ +#import "TGCache.h" + +#import "LegacyComponentsInternal.h" + +#import "TGImageUtils.h" + +#import +#import +#import + +#import + +#undef TG_SYNCHRONIZED_DEFINE +#undef TG_SYNCHRONIZED_INIT +#undef TG_SYNCHRONIZED_BEGIN +#undef TG_SYNCHRONIZED_END + +#define TG_SYNCHRONIZED_DEFINE(lock) pthread_mutex_t TG_SYNCHRONIZED_##lock +#define TG_SYNCHRONIZED_INIT(lock) pthread_mutex_init(&TG_SYNCHRONIZED_##lock, NULL) +#define TG_SYNCHRONIZED_BEGIN(lock) pthread_mutex_lock(&TG_SYNCHRONIZED_##lock); +#define TG_SYNCHRONIZED_END(lock) pthread_mutex_unlock(&TG_SYNCHRONIZED_##lock); + +static NSString *md5String(NSString *string) +{ + /*static const char *md5PropertyKey = "MD5Key"; + NSString *result = objc_getAssociatedObject(string, md5PropertyKey); + if (result != nil) + return result;*/ + + const char *ptr = [string UTF8String]; + unsigned char md5Buffer[16]; + CC_MD5(ptr, (CC_LONG)[string lengthOfBytesUsingEncoding:NSUTF8StringEncoding], md5Buffer); + NSString *output = [[NSString alloc] initWithFormat:@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x", md5Buffer[0], md5Buffer[1], md5Buffer[2], md5Buffer[3], md5Buffer[4], md5Buffer[5], md5Buffer[6], md5Buffer[7], md5Buffer[8], md5Buffer[9], md5Buffer[10], md5Buffer[11], md5Buffer[12], md5Buffer[13], md5Buffer[14], md5Buffer[15]]; + //objc_setAssociatedObject(string, md5PropertyKey, output, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + return output; +} + +@interface TGCacheRecord : NSObject + +@property (nonatomic) NSTimeInterval date; +@property (nonatomic, strong) id object; +@property (nonatomic) NSUInteger size; + +- (id)initWithObject:(id)object size:(NSUInteger)size; + +@end + +@implementation TGCacheRecord + +@synthesize date = _date; +@synthesize object = _object; +@synthesize size = _size; + +- (id)initWithObject:(id)object size:(NSUInteger)size +{ + self = [super init]; + if (self != nil) + { + _object = object; + _date = CFAbsoluteTimeGetCurrent(); + _size = size; + } + return self; +} + +@end + +static NSFileManager *cacheFileManager = nil; + +@interface TGCache () +{ + TG_SYNCHRONIZED_DEFINE(_dataMemoryCache); +} + +@property (nonatomic, strong) NSMutableArray *temporaryCachedImagesSources; + +@property (nonatomic, strong) NSMutableDictionary *memoryCache; +@property (nonatomic) int memoryCacheSize1; + +@property (nonatomic, strong) NSMutableDictionary *thumbnailCache; +@property (nonatomic) int thumbnailCacheSize; + +@property (nonatomic, strong) NSMutableDictionary *dataMemoryCache; +@property (nonatomic) int dataMemoryCacheSize; + +@end + +@implementation TGCache + ++ (dispatch_queue_t)diskCacheQueue +{ + static dispatch_queue_t queue = NULL; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^ + { + queue = dispatch_queue_create("com.telegraph.diskcache", 0); + //dispatch_set_target_queue(queue, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0)); + + if (cacheFileManager == nil) + cacheFileManager = [[NSFileManager alloc] init]; + }); + return queue; +} + ++ (NSFileManager *)diskFileManager +{ + if (cacheFileManager == nil) + cacheFileManager = [[NSFileManager alloc] init]; + + return cacheFileManager; +} + +- (id)init { + return [self initWithCachesPath:[[LegacyComponentsGlobals provider] dataCachePath]]; +} + +- (id)initWithCachesPath:(NSString *)cachesPath +{ + self = [super init]; + if (self != nil) + { + TG_SYNCHRONIZED_INIT(_dataMemoryCache); + + _imageMemoryLimit = deviceMemorySize() > 300 ? (int)(15 * 1024 * 1024) : (int)(11 * 1024 * 1024); + _imageMemoryEvictionInterval = deviceMemorySize() > 300 ? 1024 * 1024 : 812 * 1024; + + //_imageMemoryLimit = 10; + //_imageMemoryEvictionInterval = 10; + + _thumbnailMemoryLimit = deviceMemorySize() > 300 ? (int)(1.6 * 1024 * 1024) : (int)(1.1 * 1024 * 1024); + _thumbnailEvictionInterval = cpuCoreCount() > 1 ? (int)(0.4 * 1024 * 1024) : (int)(0.25 * 1024 * 1024); + + _dataMemoryLimit = deviceMemorySize() > 300 ? (int)(1 * 1024 * 1024) : (int)(0.6 * 1024 * 1024); + _dataMemoryEvictionInterval = cpuCoreCount() > 1 ? (int)(0.4 * 1024 * 1024) : (int)(0.25 * 1024 * 1024); + + _diskLimit = 32 * 1024 * 1024; + _diskEvictionInterval = 6 * 1024 * 1024; + + _memoryWarningBaseline = deviceMemorySize() > 300 ? (int)(1.5 * 1024 * 1024) : (int)(1.1 * 1024 * 1024); + + _backgroundBaseline = deviceMemorySize() > 300 ? (int)(5.8 * 1024 * 1024) : (int)(2.8 * 1024 * 1024); + + _temporaryCachedImagesSources = [[NSMutableArray alloc] init]; + + _memoryCache = [[NSMutableDictionary alloc] init]; + self.memoryCacheSize = 0; + + _thumbnailCache = [[NSMutableDictionary alloc] init]; + _thumbnailCacheSize = 0; + + _dataMemoryCache = [[NSMutableDictionary alloc] init]; + _dataMemoryCacheSize = 0; + + _diskCachePath = cachesPath; + NSFileManager *fileManager = [[NSFileManager alloc] init]; + if (![fileManager fileExistsAtPath:_diskCachePath]) + [fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL]; + + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didEnterBackground:) name:UIApplicationDidEnterBackgroundNotification object:nil]; + } + return self; +} + +- (int)memoryCacheSize +{ + return _memoryCacheSize1; +} + +- (void)setMemoryCacheSize:(int)memoryCacheSize +{ + _memoryCacheSize1 = memoryCacheSize; +} + +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +- (void)didReceiveMemoryWarning:(NSNotification *)__unused notification +{ + [self freeMemoryCache:_memoryWarningBaseline]; +} + +- (void)didEnterBackground:(NSNotification *)__unused notification +{ + [self freeMemoryCache:_backgroundBaseline]; +} + +- (void)addTemporaryCachedImagesSource:(NSDictionary *)source autoremove:(bool)autoremove +{ + dispatch_block_t block = ^ + { + [_temporaryCachedImagesSources addObject:source]; + if (autoremove) + { + dispatch_async(dispatch_get_main_queue(), ^{ + [self removeTemporaryCachedImageSource:source]; + }); + } + }; + + if ([NSThread isMainThread]) + block(); + else + dispatch_async(dispatch_get_main_queue(), block); +} + +- (void)removeTemporaryCachedImageSource:(NSDictionary *)source +{ + dispatch_block_t block = ^ + { + [_temporaryCachedImagesSources removeObject:source]; + }; + + if ([NSThread isMainThread]) + block(); + else + dispatch_async(dispatch_get_main_queue(), block); +} + +- (void)freeMemoryCache:(NSUInteger)targetSize +{ + dispatch_block_t block = ^ + { + if (self.memoryCacheSize > (int)targetSize) + { + __unused int sizeBefore = self.memoryCacheSize; + NSArray *sortedKeys = [_memoryCache keysSortedByValueUsingComparator:^NSComparisonResult(id obj1, id obj2) + { + return (((TGCacheRecord *)obj1).date < ((TGCacheRecord *)obj2).date) ? NSOrderedAscending : NSOrderedDescending; + }]; + for (int i = 0; i < (int)sortedKeys.count && self.memoryCacheSize > (int)targetSize; i++) + { + NSString *key = [sortedKeys objectAtIndex:i]; + TGCacheRecord *record = [_memoryCache objectForKey:key]; + self.memoryCacheSize -= (int)record.size; + if (self.memoryCacheSize < 0) + self.memoryCacheSize = 0; + [_memoryCache removeObjectForKey:key]; + //TGLegacyLog(@"evict %@", key); + } + + __block int currentCacheSize = 0; + [_memoryCache enumerateKeysAndObjectsUsingBlock:^(__unused NSString *key, TGCacheRecord *record, __unused BOOL *stop) + { + currentCacheSize += record.size; + }]; + + self.memoryCacheSize = currentCacheSize; + + //TGLegacyLog(@"TGCache: freed %d kbytes (cache size: %d kbytes)", (int)((sizeBefore - self.memoryCacheSize) / 1024), (int)(self.memoryCacheSize / 1024)); + } + }; + if ([NSThread isMainThread]) + block(); + else + dispatch_async(dispatch_get_main_queue(), block); +} + +- (void)freeThumbnailCache:(NSUInteger)targetSize +{ + dispatch_block_t block = ^ + { + if (_thumbnailCacheSize > (int)targetSize) + { + //int sizeBefore = _thumbnailCacheSize; + NSArray *sortedKeys = [_thumbnailCache keysSortedByValueUsingComparator:^NSComparisonResult(id obj1, id obj2) + { + return (((TGCacheRecord *)obj1).date < ((TGCacheRecord *)obj2).date) ? NSOrderedAscending : NSOrderedDescending; + }]; + for (int i = 0; i < (int)sortedKeys.count && _thumbnailCacheSize > (int)targetSize; i++) + { + NSString *key = [sortedKeys objectAtIndex:i]; + TGCacheRecord *record = [_thumbnailCache objectForKey:key]; + _thumbnailCacheSize -= record.size; + if (_thumbnailCacheSize < 0) + _thumbnailCacheSize = 0; + [_thumbnailCache removeObjectForKey:key]; + } + } + }; + if ([NSThread isMainThread]) + block(); + else + dispatch_async(dispatch_get_main_queue(), block); +} + +- (void)freeCompressedMemoryCache:(NSUInteger)targetSize reentrant:(bool)reentrant +{ + if (!reentrant) + TG_SYNCHRONIZED_BEGIN(_dataMemoryCache); + { + if (_dataMemoryCacheSize > (int)targetSize) + { + __unused int sizeBefore = _dataMemoryCacheSize; + NSArray *sortedKeys = [_dataMemoryCache keysSortedByValueUsingComparator:^NSComparisonResult(id obj1, id obj2) + { + return (((TGCacheRecord *)obj1).date < ((TGCacheRecord *)obj2).date) ? NSOrderedAscending : NSOrderedDescending; + }]; + for (int i = 0; i < (int)sortedKeys.count && _dataMemoryCacheSize > (int)targetSize; i++) + { + NSString *key = [sortedKeys objectAtIndex:i]; + TGCacheRecord *record = [_dataMemoryCache objectForKey:key]; + _dataMemoryCacheSize -= record.size; + if (_dataMemoryCacheSize < 0) + _dataMemoryCacheSize = 0; + [_dataMemoryCache removeObjectForKey:key]; + } + //TGLegacyLog(@"TGCache (compressed): freed %d kbytes (cache size: %d kbytes)", (int)((sizeBefore - _dataMemoryCacheSize) / 1024), (int)(_dataMemoryCacheSize / 1024)); + } + } + if (!reentrant) + TG_SYNCHRONIZED_END(_dataMemoryCache); +} + +- (void)cacheCompressedObject:(NSData *)data url:(NSString *)url reentrant:(bool)reentrant +{ + if (!reentrant) + TG_SYNCHRONIZED_BEGIN(_dataMemoryCache); + + TGCacheRecord *cacheRecord = [_dataMemoryCache objectForKey:url]; + if (cacheRecord != nil) + { + _dataMemoryCacheSize -= cacheRecord.size; + cacheRecord.date = CFAbsoluteTimeGetCurrent(); + cacheRecord.object = data; + cacheRecord.size = data.length; + _dataMemoryCacheSize += cacheRecord.size; + } + else + { + [_dataMemoryCache setObject:[[TGCacheRecord alloc] initWithObject:data size:data.length] forKey:url]; + _dataMemoryCacheSize += data.length; + } + + if (_dataMemoryCacheSize >= _dataMemoryLimit + _dataMemoryEvictionInterval) + [self freeCompressedMemoryCache:_dataMemoryLimit reentrant:true]; + + if (!reentrant) + TG_SYNCHRONIZED_END(_dataMemoryCache); +} + +- (void)cacheImage:(UIImage *)image withData:(NSData *)data url:(NSString *)url availability:(int)availability +{ + [self cacheImage:image withData:data url:url availability:availability completion:nil]; +} + +- (void)cacheImage:(UIImage *)image withData:(NSData *)data url:(NSString *)url availability:(int)availability completion:(void (^)(void))completion +{ +#ifdef DEBUG + if (data != nil) + TGLegacyLog(@"cache image %d bytes with url %@", (int)data.length, url); +#endif + + if (image != nil && (availability & TGCacheMemory)) + { + int size = (int)(image.size.width * image.size.height * 4 * image.scale); + dispatch_block_t block = ^ + { + TGCacheRecord *cacheRecord = [_memoryCache objectForKey:url]; + if (cacheRecord != nil) + { + self.memoryCacheSize -= (int)cacheRecord.size; + cacheRecord.date = CFAbsoluteTimeGetCurrent(); + cacheRecord.object = image; + cacheRecord.size = size; + self.memoryCacheSize += size; + } + else + { + [_memoryCache setObject:[[TGCacheRecord alloc] initWithObject:image size:size] forKey:url]; + self.memoryCacheSize += size; + } + + if (self.memoryCacheSize >= _imageMemoryLimit + _imageMemoryEvictionInterval) + [self freeMemoryCache:_imageMemoryLimit]; + }; + if ([NSThread isMainThread]) + block(); + else + dispatch_async(dispatch_get_main_queue(), block); + } + + if ((data != nil || image != nil) && (availability & TGCacheDisk) && url != nil) + { + dispatch_async([TGCache diskCacheQueue], ^ + { + if (data != nil) + { + [self cacheCompressedObject:data url:url reentrant:false]; + + [data writeToFile:[_diskCachePath stringByAppendingPathComponent:md5String(url)] atomically:true]; + + if (completion != nil) + completion(); + } + }); + } +} + +- (UIImage *)cachedImage:(NSString *)url availability:(int)availability +{ + UIImage *image = nil; + + if (availability & TGCacheMemory) + { + __block UIImage *blockImage = nil; + dispatch_block_t block = ^ + { + TGCacheRecord *cacheRecord = [_memoryCache objectForKey:url]; + if (cacheRecord != nil) + { + cacheRecord.date = CFAbsoluteTimeGetCurrent(); + blockImage = cacheRecord.object; + } + else if (_temporaryCachedImagesSources.count != 0) + { + for (NSDictionary *dict in _temporaryCachedImagesSources) + { + UIImage *image = [dict objectForKey:url]; + //TGLegacyLog(@"From temp cache %@", url); + if (image != nil) + { + blockImage = image; + break; + } + } + } + }; + if ([NSThread isMainThread]) + block(); + else + dispatch_sync(dispatch_get_main_queue(), block); + image = blockImage; + } + + if (image != nil) + return image; + + if (availability & TGCacheDisk) + { + UIImage *dataImage = nil; + + TG_SYNCHRONIZED_BEGIN(_dataMemoryCache); + { + TGCacheRecord *cacheRecord = [_dataMemoryCache objectForKey:url]; + if (cacheRecord != nil) + { + cacheRecord.date = CFAbsoluteTimeGetCurrent(); + + if (false) + { + dataImage = [[UIImage alloc] initWithData:cacheRecord.object]; + } + else + { + NSDictionary *dict = [NSDictionary dictionaryWithObject:[NSNumber numberWithBool:true] forKey:(id)kCGImageSourceShouldCache]; + CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)cacheRecord.object, nil); + if (source != nil) + { + CGImageRef cgImage = CGImageSourceCreateImageAtIndex(source, 0, (__bridge CFDictionaryRef)dict); + + dataImage = [[UIImage alloc] initWithCGImage:cgImage]; + + CGImageRelease(cgImage); + CFRelease(source); + } + } + + if (dataImage != nil && (availability & TGCacheMemory)) + [self cacheImage:dataImage withData:nil url:url availability:TGCacheMemory]; + } + } + TG_SYNCHRONIZED_END(_dataMemoryCache); + + if (dataImage != nil) + return dataImage; + + __block UIImage *diskImageResult = nil; + dispatch_sync([TGCache diskCacheQueue], ^ + { + NSDictionary *dict = [NSDictionary dictionaryWithObject:[NSNumber numberWithBool:true] forKey:(id)kCGImageSourceShouldCache]; + + NSURL *realUrl = [[NSURL alloc] initFileURLWithPath:[_diskCachePath stringByAppendingPathComponent:md5String(url)]]; + CGImageSourceRef source = CGImageSourceCreateWithURL((__bridge CFURLRef)realUrl, NULL); + if (source != nil) + { + CGImageRef cgImage = CGImageSourceCreateImageAtIndex(source, 0, (__bridge CFDictionaryRef)dict); + + UIImage *diskImage = [UIImage imageWithCGImage:cgImage]; + + if (diskImage != nil && (availability & TGCacheMemory)) + { + [self cacheImage:diskImage withData:nil url:url availability:TGCacheMemory]; + } + + if (diskImage != nil) + { + diskImageResult = diskImage; + + if (diskImage != nil) + { + dispatch_async([TGCache diskCacheQueue], ^ + { + NSData *data = [[NSData alloc] initWithContentsOfURL:realUrl]; + if (data != nil) + { + [self cacheCompressedObject:data url:url reentrant:false]; + } + }); + } + } + + CGImageRelease(cgImage); + CFRelease(source); + } + }); + image = diskImageResult; + return image; + } + + return nil; +} + +- (void)removeFromMemoryCache:(NSString *)url matchEnd:(bool)matchEnd +{ + if (url == nil) + return; + + dispatch_block_t block = ^ + { + TGCacheRecord *cacheRecord = [_memoryCache objectForKey:url]; + if (cacheRecord != nil) + { + self.memoryCacheSize -= (int)cacheRecord.size; + if (self.memoryCacheSize < 0) + self.memoryCacheSize = 0; + [_memoryCache removeObjectForKey:url]; + } + + if (matchEnd) + { + NSMutableArray *removeKeys = [[NSMutableArray alloc] init]; + + [_memoryCache enumerateKeysAndObjectsUsingBlock:^(NSString *key, __unused id obj, __unused BOOL *stop) + { + if ([key hasSuffix:url]) + [removeKeys addObject:key]; + }]; + + [_memoryCache removeObjectsForKeys:removeKeys]; + } + + TGCacheRecord *dataCacheRecord = [_dataMemoryCache objectForKey:url]; + if (dataCacheRecord != nil) + { + _dataMemoryCacheSize -= dataCacheRecord.size; + if (_dataMemoryCacheSize < 0) + _dataMemoryCacheSize = 0; + [_dataMemoryCache removeObjectForKey:url]; + } + }; + if ([NSThread isMainThread]) + block(); + else + dispatch_sync(dispatch_get_main_queue(), block); +} + +- (void)cacheThumbnail:(UIImage *)image url:(NSString *)url +{ + if ([self cachedThumbnail:url] != nil) + return; + + if (image != nil) + { + int side = 32; + if (!TGIsRetina()) + side *= 2; + int size = (int)(side * side * 4); + if (image.size.width > side || image.size.height > side) + { + image = TGScaleImage(image, CGSizeMake(side, side)); + } + else + return; + + dispatch_block_t block = ^ + { + TGCacheRecord *cacheRecord = [_thumbnailCache objectForKey:url]; + if (cacheRecord != nil) + { + _thumbnailCacheSize -= cacheRecord.size; + cacheRecord.date = CFAbsoluteTimeGetCurrent(); + cacheRecord.object = image; + cacheRecord.size = size; + _thumbnailCacheSize += size; + } + else + { + [_thumbnailCache setObject:[[TGCacheRecord alloc] initWithObject:image size:size] forKey:url]; + _thumbnailCacheSize += size; + } + + //TGLegacyLog(@"Cache thumbnail: %@", url); + + //TGLegacyLog(@"_thumbnailCacheSize = %d", _thumbnailCacheSize); + + if (_thumbnailCacheSize >= _thumbnailMemoryLimit + _thumbnailEvictionInterval) + [self freeThumbnailCache:_thumbnailMemoryLimit]; + }; + if ([NSThread isMainThread]) + block(); + else + dispatch_async(dispatch_get_main_queue(), block); + } +} + +- (UIImage *)cachedThumbnail:(NSString *)url +{ + UIImage *image = nil; + __block UIImage *blockImage = nil; + dispatch_block_t block = ^ + { + TGCacheRecord *cacheRecord = [_thumbnailCache objectForKey:url]; + if (cacheRecord != nil) + { + cacheRecord.date = CFAbsoluteTimeGetCurrent(); + blockImage = cacheRecord.object; + } + else + { + //TGLegacyLog(@"Thumbnail not found: %@", url); + } + }; + if ([NSThread isMainThread]) + block(); + else + dispatch_async(dispatch_get_main_queue(), block); + image = blockImage; + if (image != nil) + return image; + + return nil; +} + +- (void)diskCacheContains:(NSString *)url1 orUrl:(NSString *)url2 completion:(void (^)(bool containsFirst, bool containsSecond))completion +{ + dispatch_async([TGCache diskCacheQueue], ^ + { + bool cached = [cacheFileManager fileExistsAtPath:[_diskCachePath stringByAppendingPathComponent:md5String(url1)]]; + if (cached) + { + if (completion) + completion(true, false); + } + else if (url2 != nil) + { + cached = [cacheFileManager fileExistsAtPath:[_diskCachePath stringByAppendingPathComponent:md5String(url2)]]; + if (completion) + completion(false, cached); + } + else + { + if (completion) + completion(false, false); + } + }); +} + +- (bool)diskCacheContainsSync:(NSString *)url +{ + __block bool result = false; + + dispatch_sync([TGCache diskCacheQueue], ^ + { + result = [cacheFileManager fileExistsAtPath:[_diskCachePath stringByAppendingPathComponent:md5String(url)]]; + }); + + return result; +} + +- (void)removeFromDiskCache:(NSString *)url +{ + dispatch_async([TGCache diskCacheQueue], ^ + { + [cacheFileManager removeItemAtPath:[_diskCachePath stringByAppendingPathComponent:md5String(url)] error:nil]; + }); +} + +- (NSString *)pathForCachedData:(NSString *)url +{ + return [_diskCachePath stringByAppendingPathComponent:md5String(url)]; +} + +- (void)changeCacheItemUrl:(NSString *)oldUrl newUrl:(NSString *)newUrl +{ + //TGLegacyLog(@"TGCache: rename \"%@\" -> \"%@\"", oldUrl, newUrl); + dispatch_block_t block = ^ + { + TGCacheRecord *record = [_memoryCache objectForKey:oldUrl]; + if (record != nil) + { + [_memoryCache setObject:record forKey:newUrl]; + [_memoryCache removeObjectForKey:oldUrl]; + } + }; + if ([NSThread isMainThread]) + block(); + else + dispatch_async(dispatch_get_main_queue(), block); + + dispatch_async([TGCache diskCacheQueue], ^ + { + NSError *error = nil; + [cacheFileManager moveItemAtPath:[_diskCachePath stringByAppendingPathComponent:md5String(oldUrl)] toPath:[_diskCachePath stringByAppendingPathComponent:md5String(newUrl)] error:&error]; + if (error != nil) + TGLegacyLog(@"Failed to move: %@", error); + }); +} + +- (void)moveToCache:(NSString *)fileUrl cacheUrl:(NSString *)cacheUrl +{ + TGLegacyLog(@"TGCache: move \"%@\" -> \"%@\"", fileUrl, cacheUrl); + dispatch_block_t block = ^ + { + TGCacheRecord *record = [_memoryCache objectForKey:fileUrl]; + if (record != nil) + { + [_memoryCache setObject:record forKey:cacheUrl]; + [_memoryCache removeObjectForKey:fileUrl]; + } + }; + if ([NSThread isMainThread]) + block(); + else + dispatch_async(dispatch_get_main_queue(), block); + + dispatch_async([TGCache diskCacheQueue], ^ + { + NSError *error = nil; + + NSString *targetPath = [_diskCachePath stringByAppendingPathComponent:md5String(cacheUrl)]; + if ([[NSFileManager defaultManager] fileExistsAtPath:targetPath]) + [[NSFileManager defaultManager] removeItemAtPath:targetPath error:NULL]; + + [cacheFileManager moveItemAtPath:fileUrl toPath:targetPath error:&error]; + if (error != nil) + TGLegacyLog(@"Failed to move: %@", error); + }); +} + +- (void)clearCache:(int)availability +{ + if (availability & TGCacheMemory) + { + dispatch_block_t block = ^ + { + [_memoryCache removeAllObjects]; + self.memoryCacheSize = 0; + }; + if ([NSThread isMainThread]) + block(); + else + dispatch_async(dispatch_get_main_queue(), block); + + TG_SYNCHRONIZED_BEGIN(_dataMemoryCache); + [_dataMemoryCache removeAllObjects]; + _dataMemoryCacheSize = 0; + TG_SYNCHRONIZED_END(_dataMemoryCache); + } + if (availability & TGCacheDisk) + { + dispatch_async([TGCache diskCacheQueue], ^ + { + NSDirectoryEnumerator* en = [cacheFileManager enumeratorAtPath:_diskCachePath]; + NSError* error = nil; + + int removedCount = 0; + int failedCount = 0; + + NSString* file; + while (file = [en nextObject]) + { + if ([cacheFileManager removeItemAtPath:[_diskCachePath stringByAppendingPathComponent:file] error:&error]) + removedCount++; + else + failedCount++; + } + + TGLegacyLog(@"TGCache: removed %d files (%d failed)", removedCount, failedCount); + }); + } +} + +- (NSArray *)storeMemoryCache +{ + NSMutableArray *array = [[NSMutableArray alloc] init]; + + dispatch_block_t block = ^ + { + [_memoryCache enumerateKeysAndObjectsUsingBlock:^(NSString *key, __unused id obj, __unused BOOL *stop) + { + [array addObject:key]; + }]; + }; + if ([NSThread isMainThread]) + block(); + else + dispatch_sync(dispatch_get_main_queue(), block); + + return array; +} + +- (void)restoreMemoryCache:(NSArray *)array +{ + CFAbsoluteTime startTime = CFAbsoluteTimeGetCurrent(); + for (NSString *url in array) + { + [self cachedImage:url availability:TGCacheDisk]; + } + TGLegacyLog(@"Cache restored in %f ms", (CFAbsoluteTimeGetCurrent() - startTime) * 1000.0); +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGCameraCapturedPhoto.h b/submodules/LegacyComponents/LegacyComponents/TGCameraCapturedPhoto.h new file mode 100644 index 0000000000..251ff85651 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGCameraCapturedPhoto.h @@ -0,0 +1,17 @@ +#import +#import +#import + +@class PGCameraShotMetadata; + +@interface TGCameraCapturedPhoto : NSObject + +@property (nonatomic, readonly) NSURL *url; +@property (nonatomic, readonly) PGCameraShotMetadata *metadata; + +- (instancetype)initWithImage:(UIImage *)image metadata:(PGCameraShotMetadata *)metadata; +- (instancetype)initWithExistingImage:(UIImage *)image; + +- (void)_cleanUp; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGCameraCapturedPhoto.m b/submodules/LegacyComponents/LegacyComponents/TGCameraCapturedPhoto.m new file mode 100644 index 0000000000..88cb73db87 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGCameraCapturedPhoto.m @@ -0,0 +1,209 @@ +#import "TGCameraCapturedPhoto.h" +#import +#import +#import +#import + +@interface TGCameraCapturedPhoto () +{ + NSString *_identifier; + CGSize _dimensions; + + UIImage *_existingImage; + SVariable *_thumbnail; + UIImage *_thumbImage; +} +@end + +@implementation TGCameraCapturedPhoto + +- (instancetype)initWithImage:(UIImage *)image metadata:(PGCameraShotMetadata *)metadata +{ + self = [super init]; + if (self != nil) + { + _identifier = [NSString stringWithFormat:@"%ld", lrand48()]; + _dimensions = CGSizeMake(image.size.width * image.scale, image.size.height * image.scale); + _metadata = metadata; + _thumbnail = [[SVariable alloc] init]; + + [self _saveToDisk:image]; + } + return self; +} + +- (instancetype)initWithExistingImage:(UIImage *)image +{ + self = [super init]; + if (self != nil) + { + _identifier = [NSString stringWithFormat:@"%ld", lrand48()]; + _dimensions = CGSizeMake(image.size.width, image.size.height); + _thumbnail = [[SVariable alloc] init]; + + _existingImage = image; + SSignal *thumbnailSignal = [[[SSignal alloc] initWithGenerator:^id(SSubscriber *subscriber) + { + CGFloat thumbnailImageSide = TGPhotoThumbnailSizeForCurrentScreen().width * TGScreenScaling(); + CGSize thumbnailSize = TGScaleToSize(image.size, CGSizeMake(thumbnailImageSide, thumbnailImageSide)); + UIImage *thumbnailImage = TGScaleImageToPixelSize(image, thumbnailSize); + + [subscriber putNext:thumbnailImage]; + [subscriber putCompletion]; + + return nil; + }] startOn:[SQueue concurrentDefaultQueue]]; + + [_thumbnail set:thumbnailSignal]; + } + return self; +} + +- (void)_cleanUp +{ + [[NSFileManager defaultManager] removeItemAtPath:[self filePath] error:nil]; +} + +#define PGTick NSDate *startTime = [NSDate date] +#define PGTock NSLog(@"!=========== %s Time: %f", __func__, -[startTime timeIntervalSinceNow]) + +- (void)_saveToDisk:(UIImage *)image +{ + if (image == nil) + return; + + TGWriteJPEGRepresentationToFile(image, 0.93f, [self filePath]); + + CGFloat thumbnailImageSide = TGPhotoThumbnailSizeForCurrentScreen().width; + CGSize size = TGScaleToSize(image.size, CGSizeMake(thumbnailImageSide, thumbnailImageSide)); + SSignal *thumbnailSignal = [[[SSignal alloc] initWithGenerator:^id(SSubscriber *subscriber) + { + CGImageSourceRef imageSource = CGImageSourceCreateWithURL((CFURLRef)[NSURL fileURLWithPath:[self filePath]], NULL); + if (imageSource == NULL) + { + [subscriber putError:nil]; + return nil; + } + + CGImageRef imageRef = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, (__bridge CFDictionaryRef)@ + { + (id)kCGImageSourceShouldAllowFloat : (id)kCFBooleanTrue, + (id)kCGImageSourceCreateThumbnailWithTransform : (id)kCFBooleanFalse, + (id)kCGImageSourceCreateThumbnailFromImageIfAbsent : (id)kCFBooleanTrue, + (id)kCGImageSourceThumbnailMaxPixelSize : @(MAX(size.width, size.height) * TGScreenScaling()) + }); + + UIImage *image = [UIImage imageWithCGImage:imageRef]; + CGImageRelease(imageRef); + CFRelease(imageSource); + + [subscriber putNext:image]; + [subscriber putCompletion]; + + return nil; + }] startOn:[SQueue concurrentDefaultQueue]]; + [_thumbnail set:thumbnailSignal]; +} + +- (NSString *)filePath +{ + return [NSTemporaryDirectory() stringByAppendingPathComponent:[[NSString alloc] initWithFormat:@"camphoto_%@.jpg", _identifier]]; +} + +- (NSURL *)url +{ + return [NSURL fileURLWithPath:[self filePath]]; +} + +- (bool)isVideo +{ + return false; +} + +- (NSString *)uniqueIdentifier +{ + return _identifier; +} + +- (CGSize)originalSize +{ + return _dimensions; +} + +- (SSignal *)thumbnailImageSignal +{ + return _thumbnail.signal; +} + +- (SSignal *)screenImageSignal:(NSTimeInterval)__unused position +{ + if (_existingImage != nil) + { + return [SSignal single:_existingImage]; + } + else + { + return [[SSignal alloc] initWithGenerator:^id(SSubscriber *subscriber) + { + CGImageSourceRef imageSource = CGImageSourceCreateWithURL((CFURLRef)[NSURL fileURLWithPath:[self filePath]], NULL); + if (imageSource == NULL) + { + [subscriber putError:nil]; + return nil; + } + + CGImageRef imageRef = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, (__bridge CFDictionaryRef)@ + { + (id)kCGImageSourceShouldAllowFloat : (id)kCFBooleanTrue, + (id)kCGImageSourceCreateThumbnailWithTransform : (id)kCFBooleanFalse, + (id)kCGImageSourceCreateThumbnailFromImageIfAbsent : (id)kCFBooleanTrue, + (id)kCGImageSourceThumbnailMaxPixelSize : @(1600) + }); + if (imageRef == NULL) + imageRef = CGImageSourceCreateImageAtIndex(imageSource, 0, nil); + + UIImage *image = [UIImage imageWithCGImage:imageRef]; + CGImageRelease(imageRef); + CFRelease(imageSource); + + [subscriber putNext:image]; + [subscriber putCompletion]; + + return nil; + }]; + } +} + +- (SSignal *)originalImageSignal:(NSTimeInterval)__unused position +{ + if (_existingImage != nil) + { + return [SSignal single:_existingImage]; + } + else + { + return [[SSignal alloc] initWithGenerator:^id(SSubscriber *subscriber) + { + NSData *data = [[NSData alloc] initWithContentsOfFile:[self filePath] options:NSDataReadingMappedIfSafe error:NULL]; + if (data.length == 0) + { + [subscriber putError:nil]; + return nil; + } + + UIImage *image = [[UIImage alloc] initWithData:data]; + if (image == nil) + { + [subscriber putError:nil]; + return nil; + } + + [subscriber putNext:image]; + [subscriber putCompletion]; + + return nil; + }]; + } +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGCameraCapturedVideo.h b/submodules/LegacyComponents/LegacyComponents/TGCameraCapturedVideo.h new file mode 100644 index 0000000000..23b96a7c0d --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGCameraCapturedVideo.h @@ -0,0 +1,16 @@ +#import +#import +#import + +@class AVURLAsset; + +@interface TGCameraCapturedVideo : NSObject + +@property (nonatomic, readonly) AVURLAsset *avAsset; +@property (nonatomic, readonly) NSTimeInterval videoDuration; + +- (instancetype)initWithURL:(NSURL *)url; + +- (void)_cleanUp; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGCameraCapturedVideo.m b/submodules/LegacyComponents/LegacyComponents/TGCameraCapturedVideo.m new file mode 100644 index 0000000000..05fafa2751 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGCameraCapturedVideo.m @@ -0,0 +1,87 @@ +#import "TGCameraCapturedVideo.h" +#import +#import +#import + +@interface TGCameraCapturedVideo () +{ + CGSize _cachedSize; + NSTimeInterval _cachedDuration; +} +@end + +@implementation TGCameraCapturedVideo + +- (instancetype)initWithURL:(NSURL *)url +{ + self = [super init]; + if (self != nil) + { + _avAsset = [[AVURLAsset alloc] initWithURL:url options:nil]; + _cachedSize = CGSizeZero; + _cachedDuration = 0.0; + } + return self; +} + +- (void)_cleanUp +{ + [[NSFileManager defaultManager] removeItemAtPath:_avAsset.URL.path error:nil]; +} + +- (bool)isVideo +{ + return true; +} + +- (NSString *)uniqueIdentifier +{ + return _avAsset.URL.absoluteString; +} + +- (CGSize)originalSize +{ + if (!CGSizeEqualToSize(_cachedSize, CGSizeZero)) + return _cachedSize; + + AVAssetTrack *track = _avAsset.tracks.firstObject; + _cachedSize = CGRectApplyAffineTransform((CGRect){ CGPointZero, track.naturalSize }, track.preferredTransform).size; + return _cachedSize; +} + +- (NSTimeInterval)videoDuration +{ + return [self originalDuration]; +} + +- (NSTimeInterval)originalDuration +{ + if (_cachedDuration > DBL_EPSILON) + return _cachedDuration; + + _cachedDuration = CMTimeGetSeconds(_avAsset.duration); + return _cachedDuration; +} + +- (SSignal *)thumbnailImageSignal +{ + CGFloat thumbnailImageSide = TGPhotoEditorScreenImageMaxSize().width; + CGSize size = TGScaleToSize(self.originalSize, CGSizeMake(thumbnailImageSide, thumbnailImageSide)); + + return [TGMediaAssetImageSignals videoThumbnailForAVAsset:_avAsset size:size timestamp:kCMTimeZero]; +} + +- (SSignal *)screenImageSignal:(NSTimeInterval)__unused position +{ + CGFloat imageSide = 1280.0f; + CGSize size = TGScaleToSize(self.originalSize, CGSizeMake(imageSide, imageSide)); + + return [TGMediaAssetImageSignals videoThumbnailForAVAsset:_avAsset size:size timestamp:kCMTimeZero]; +} + +- (SSignal *)originalImageSignal:(NSTimeInterval)position +{ + return [TGMediaAssetImageSignals videoThumbnailForAVAsset:_avAsset size:self.originalSize timestamp:CMTimeMakeWithSeconds(position, NSEC_PER_SEC)]; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGCameraController.h b/submodules/LegacyComponents/LegacyComponents/TGCameraController.h new file mode 100644 index 0000000000..051f15e933 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGCameraController.h @@ -0,0 +1,70 @@ +#import +#import +#import +#import + +@class PGCamera; +@class TGCameraPreviewView; +@class TGMediaSelectionContext; +@class TGMediaEditingContext; +@class TGSuggestionContext; +@class TGVideoEditAdjustments; + +typedef enum { + TGCameraControllerGenericIntent, + TGCameraControllerPassportIntent, + TGCameraControllerPassportIdIntent, + TGCameraControllerPassportMultipleIntent, + TGCameraControllerAvatarIntent, + TGCameraControllerSignupAvatarIntent +} TGCameraControllerIntent; + +@interface TGCameraControllerWindow : TGOverlayControllerWindow + +@end + +@interface TGCameraController : TGOverlayController + +@property (nonatomic, assign) bool liveUploadEnabled; +@property (nonatomic, assign) bool shouldStoreCapturedAssets; + +@property (nonatomic, assign) bool allowCaptions; +@property (nonatomic, assign) bool allowCaptionEntities; +@property (nonatomic, assign) bool allowGrouping; +@property (nonatomic, assign) bool inhibitDocumentCaptions; +@property (nonatomic, assign) bool inhibitMultipleCapture; +@property (nonatomic, assign) bool inhibitMute; +@property (nonatomic, assign) bool hasTimer; +@property (nonatomic, strong) TGSuggestionContext *suggestionContext; +@property (nonatomic, assign) bool shortcut; + +@property (nonatomic, strong) NSString *forcedCaption; +@property (nonatomic, strong) NSArray *forcedEntities; + +@property (nonatomic, strong) NSString *recipientName; + +@property (nonatomic, copy) void(^finishedWithResults)(TGOverlayController *controller, TGMediaSelectionContext *selectionContext, TGMediaEditingContext *editingContext, id currentItem); +@property (nonatomic, copy) void(^finishedWithPhoto)(TGOverlayController *controller, UIImage *resultImage, NSString *caption, NSArray *entities, NSArray *stickers, NSNumber *timer); +@property (nonatomic, copy) void(^finishedWithVideo)(TGOverlayController *controller, NSURL *videoURL, UIImage *previewImage, NSTimeInterval duration, CGSize dimensions, TGVideoEditAdjustments *adjustments, NSString *caption, NSArray *entities, NSArray *stickers, NSNumber *timer); + +@property (nonatomic, copy) void(^recognizedQRCode)(NSString *code); + +@property (nonatomic, copy) CGRect(^beginTransitionOut)(void); +@property (nonatomic, copy) void(^finishedTransitionOut)(void); +@property (nonatomic, copy) void(^customPresentOverlayController)(TGOverlayController *(^)(id)); + +- (instancetype)initWithContext:(id)context saveEditedPhotos:(bool)saveEditedPhotos saveCapturedMedia:(bool)saveCapturedMedia; +- (instancetype)initWithContext:(id)context saveEditedPhotos:(bool)saveEditedPhotos saveCapturedMedia:(bool)saveCapturedMedia intent:(TGCameraControllerIntent)intent; +- (instancetype)initWithContext:(id)context saveEditedPhotos:(bool)saveEditedPhotos saveCapturedMedia:(bool)saveCapturedMedia camera:(PGCamera *)camera previewView:(TGCameraPreviewView *)previewView intent:(TGCameraControllerIntent)intent; + ++ (NSArray *)resultSignalsForSelectionContext:(TGMediaSelectionContext *)selectionContext editingContext:(TGMediaEditingContext *)editingContext currentItem:(id)currentItem storeAssets:(bool)storeAssets saveEditedPhotos:(bool)saveEditedPhotos descriptionGenerator:(id (^)(id, NSString *, NSArray *, NSString *))descriptionGenerator; + +- (void)beginTransitionInFromRect:(CGRect)rect; +- (void)_dismissTransitionForResultController:(TGOverlayController *)resultController; +- (void)beginTransitionOutWithVelocity:(CGFloat)velocity; + ++ (UIInterfaceOrientation)_interfaceOrientationForDeviceOrientation:(UIDeviceOrientation)orientation; + ++ (bool)useLegacyCamera; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGCameraController.m b/submodules/LegacyComponents/LegacyComponents/TGCameraController.m new file mode 100644 index 0000000000..6db6d9770a --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGCameraController.m @@ -0,0 +1,2534 @@ +#import "TGCameraController.h" + +#import "LegacyComponentsInternal.h" + +#import + +#import + +#import +#import +#import + +#import +#import +#import +#import + +#import +#import +#import +#import "TGCameraFocusCrosshairsControl.h" + +#import +#import + +#import +#import +#import +#import +#import +#import + +#import "TGMediaVideoConverter.h" +#import +#import +#import +#import +#import +#import + +#import + +#import + +#import + +#import + +#import "TGMediaPickerGallerySelectedItemsModel.h" +#import "TGCameraCapturedPhoto.h" +#import "TGCameraCapturedVideo.h" + +#import + +const CGFloat TGCameraSwipeMinimumVelocity = 600.0f; +const CGFloat TGCameraSwipeVelocityThreshold = 700.0f; +const CGFloat TGCameraSwipeDistanceThreshold = 128.0f; +const NSTimeInterval TGCameraMinimumClipDuration = 4.0f; + +@implementation TGCameraControllerWindow + +static CGPoint TGCameraControllerClampPointToScreenSize(__unused id self, __unused SEL _cmd, CGPoint point) +{ + CGSize screenSize = TGScreenSize(); + return CGPointMake(MAX(0, MIN(point.x, screenSize.width)), MAX(0, MIN(point.y, screenSize.height))); +} + ++ (void)initialize +{ + static bool initialized = false; + if (!initialized) + { + initialized = true; + + if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPhone && (iosMajorVersion() > 8 || (iosMajorVersion() == 8 && iosMinorVersion() >= 3))) + { + FreedomDecoration instanceDecorations[] = + { + { .name = 0x4ea0b831U, + .imp = (IMP)&TGCameraControllerClampPointToScreenSize, + .newIdentifier = FreedomIdentifierEmpty, + .newEncoding = FreedomIdentifierEmpty + } + }; + + freedomClassAutoDecorate(0x913b3af6, NULL, 0, instanceDecorations, sizeof(instanceDecorations) / sizeof(instanceDecorations[0])); + } + } +} + +@end + +@interface TGCameraController () +{ + bool _standalone; + + TGCameraControllerIntent _intent; + PGCamera *_camera; + PGCameraVolumeButtonHandler *_buttonHandler; + + UIView *_autorotationCorrectionView; + + UIView *_backgroundView; + TGCameraPreviewView *_previewView; + TGCameraMainView *_interfaceView; + UIView *_overlayView; + TGCameraFocusCrosshairsControl *_focusControl; + + TGModernGalleryVideoView *_segmentPreviewView; + bool _previewingSegment; + + UISwipeGestureRecognizer *_photoSwipeGestureRecognizer; + UISwipeGestureRecognizer *_videoSwipeGestureRecognizer; + TGModernGalleryZoomableScrollViewSwipeGestureRecognizer *_panGestureRecognizer; + UIPinchGestureRecognizer *_pinchGestureRecognizer; + + CGFloat _dismissProgress; + bool _dismissing; + bool _finishedWithResult; + + TGMediaPickerGallerySelectedItemsModel *_selectedItemsModel; + NSMutableArray> *_items; + TGMediaEditingContext *_editingContext; + TGMediaSelectionContext *_selectionContext; + + NSTimer *_switchToVideoTimer; + NSTimer *_startRecordingTimer; + bool _recordingByShutterHold; + bool _stopRecordingOnRelease; + bool _shownMicrophoneAlert; + + id _context; + bool _saveEditedPhotos; + bool _saveCapturedMedia; + + bool _shutterIsBusy; + + NSMutableSet *_previousQRCodes; +} +@end + +@implementation TGCameraController + +- (instancetype)initWithContext:(id)context saveEditedPhotos:(bool)saveEditedPhotos saveCapturedMedia:(bool)saveCapturedMedia +{ + return [self initWithContext:context saveEditedPhotos:saveEditedPhotos saveCapturedMedia:saveCapturedMedia intent:TGCameraControllerGenericIntent]; +} + +- (instancetype)initWithContext:(id)context saveEditedPhotos:(bool)saveEditedPhotos saveCapturedMedia:(bool)saveCapturedMedia intent:(TGCameraControllerIntent)intent +{ + return [self initWithContext:context saveEditedPhotos:saveEditedPhotos saveCapturedMedia:saveCapturedMedia camera:[[PGCamera alloc] init] previewView:nil intent:intent]; +} + +- (instancetype)initWithContext:(id)context saveEditedPhotos:(bool)saveEditedPhotos saveCapturedMedia:(bool)saveCapturedMedia camera:(PGCamera *)camera previewView:(TGCameraPreviewView *)previewView intent:(TGCameraControllerIntent)intent +{ + self = [super initWithContext:context]; + if (self != nil) + { + _context = context; + if (previewView == nil) + _standalone = true; + _intent = intent; + _camera = camera; + _previewView = previewView; + + _items = [[NSMutableArray alloc] init]; + + if (_intent != TGCameraControllerGenericIntent) + _allowCaptions = false; + _saveEditedPhotos = saveEditedPhotos; + _saveCapturedMedia = saveCapturedMedia; + + _previousQRCodes = [[NSMutableSet alloc] init]; + } + return self; +} + +- (void)dealloc +{ + _camera.beganModeChange = nil; + _camera.finishedModeChange = nil; + _camera.beganPositionChange = nil; + _camera.finishedPositionChange = nil; + _camera.beganAdjustingFocus = nil; + _camera.finishedAdjustingFocus = nil; + _camera.flashActivityChanged = nil; + _camera.flashAvailabilityChanged = nil; + _camera.beganVideoRecording = nil; + _camera.finishedVideoRecording = nil; + _camera.captureInterrupted = nil; + _camera.requestedCurrentInterfaceOrientation = nil; + _camera.deviceAngleSampler.deviceOrientationChanged = nil; + + PGCamera *camera = _camera; + if (_finishedWithResult || _standalone) + [camera stopCaptureForPause:false completion:nil]; + + [[[LegacyComponentsGlobals provider] applicationInstance] setIdleTimerDisabled:false]; +} + +- (void)loadView +{ + [super loadView]; + object_setClass(self.view, [TGFullscreenContainerView class]); + + CGSize screenSize = TGScreenSize(); + CGRect screenBounds = CGRectMake(0, 0, screenSize.width, screenSize.height); + + if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPhone) + self.view.frame = screenBounds; + + _autorotationCorrectionView = [[UIView alloc] initWithFrame:screenBounds]; + _autorotationCorrectionView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + [self.view addSubview:_autorotationCorrectionView]; + + _backgroundView = [[UIView alloc] initWithFrame:screenBounds]; + _backgroundView.backgroundColor = [UIColor blackColor]; + [_autorotationCorrectionView addSubview:_backgroundView]; + + if (_previewView == nil) + { + _previewView = [[TGCameraPreviewView alloc] initWithFrame:[TGCameraController _cameraPreviewFrameForScreenSize:screenSize mode:PGCameraModePhoto]]; + [_camera attachPreviewView:_previewView]; + [_autorotationCorrectionView addSubview:_previewView]; + } + + _overlayView = [[UIView alloc] initWithFrame:screenBounds]; + _overlayView.clipsToBounds = true; + _overlayView.frame = [TGCameraController _cameraPreviewFrameForScreenSize:screenSize mode:_camera.cameraMode]; + [_autorotationCorrectionView addSubview:_overlayView]; + + UIInterfaceOrientation interfaceOrientation = [[LegacyComponentsGlobals provider] applicationStatusBarOrientation]; + + if (interfaceOrientation == UIInterfaceOrientationPortrait) + interfaceOrientation = [TGCameraController _interfaceOrientationForDeviceOrientation:_camera.deviceAngleSampler.deviceOrientation]; + + __weak TGCameraController *weakSelf = self; + _focusControl = [[TGCameraFocusCrosshairsControl alloc] initWithFrame:_overlayView.bounds]; + _focusControl.enabled = (_camera.supportsFocusPOI || _camera.supportsExposurePOI); + _focusControl.stopAutomatically = (_focusControl.enabled && !_camera.supportsFocusPOI); + _focusControl.previewView = _previewView; + _focusControl.focusPOIChanged = ^(CGPoint point) + { + __strong TGCameraController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [strongSelf->_camera setFocusPoint:point]; + }; + _focusControl.beganExposureChange = ^ + { + __strong TGCameraController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [strongSelf->_camera beginExposureTargetBiasChange]; + }; + _focusControl.exposureChanged = ^(CGFloat value) + { + __strong TGCameraController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [strongSelf->_camera setExposureTargetBias:value]; + }; + _focusControl.endedExposureChange = ^ + { + __strong TGCameraController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [strongSelf->_camera endExposureTargetBiasChange]; + }; + [_focusControl setInterfaceOrientation:interfaceOrientation animated:false]; + [_overlayView addSubview:_focusControl]; + + if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) + { + _panGestureRecognizer = [[TGModernGalleryZoomableScrollViewSwipeGestureRecognizer alloc] initWithTarget:self action:@selector(handlePan:)]; + _panGestureRecognizer.delegate = self; + _panGestureRecognizer.delaysTouchesBegan = true; + _panGestureRecognizer.cancelsTouchesInView = false; + [_overlayView addGestureRecognizer:_panGestureRecognizer]; + } + + _pinchGestureRecognizer = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(handlePinch:)]; + _pinchGestureRecognizer.delegate = self; + [_overlayView addGestureRecognizer:_pinchGestureRecognizer]; + + if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) + { + _interfaceView = [[TGCameraMainPhoneView alloc] initWithFrame:screenBounds]; + [_interfaceView setInterfaceOrientation:interfaceOrientation animated:false]; + } + else + { + _interfaceView = [[TGCameraMainTabletView alloc] initWithFrame:screenBounds]; + [_interfaceView setInterfaceOrientation:interfaceOrientation animated:false]; + + CGSize referenceSize = [self referenceViewSizeForOrientation:interfaceOrientation]; + if (referenceSize.width > referenceSize.height) + referenceSize = CGSizeMake(referenceSize.height, referenceSize.width); + + _interfaceView.transform = CGAffineTransformMakeRotation(TGRotationForInterfaceOrientation(interfaceOrientation)); + _interfaceView.frame = CGRectMake(0, 0, referenceSize.width, referenceSize.height); + } + if (_intent == TGCameraControllerPassportIdIntent) + [_interfaceView setDocumentFrameHidden:false]; + _selectedItemsModel = [[TGMediaPickerGallerySelectedItemsModel alloc] initWithSelectionContext:nil items:[_items copy]]; + [_interfaceView setSelectedItemsModel:_selectedItemsModel]; + _selectedItemsModel.selectionUpdated = ^(bool reload, bool incremental, bool add, NSInteger index) + { + __strong TGCameraController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [strongSelf->_interfaceView updateSelectedPhotosView:reload incremental:incremental add:add index:index]; + NSInteger count = strongSelf->_items.count; + [strongSelf->_interfaceView updateSelectionInterface:count counterVisible:count > 0 animated:true]; + }; + _interfaceView.thumbnailSignalForItem = ^SSignal *(id item) + { + __strong TGCameraController *strongSelf = weakSelf; + if (strongSelf != nil) + return [strongSelf _signalForItem:item]; + return nil; + }; + _interfaceView.requestedVideoRecordingDuration = ^NSTimeInterval + { + __strong TGCameraController *strongSelf = weakSelf; + if (strongSelf == nil) + return 0.0; + + return strongSelf->_camera.videoRecordingDuration; + }; + + _interfaceView.cameraFlipped = ^ + { + __strong TGCameraController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [strongSelf->_camera togglePosition]; + }; + + _interfaceView.cameraShouldLeaveMode = ^bool(__unused PGCameraMode mode) + { + return true; + }; + _interfaceView.cameraModeChanged = ^(PGCameraMode mode) + { + __strong TGCameraController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [strongSelf->_camera setCameraMode:mode]; + }; + + _interfaceView.flashModeChanged = ^(PGCameraFlashMode mode) + { + __strong TGCameraController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [strongSelf->_camera setFlashMode:mode]; + }; + + _interfaceView.shutterPressed = ^(bool fromHardwareButton) + { + __strong TGCameraController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + if (fromHardwareButton) + [strongSelf->_interfaceView setShutterButtonHighlighted:true]; + + [strongSelf shutterPressed]; + }; + + _interfaceView.shutterReleased = ^(bool fromHardwareButton) + { + __strong TGCameraController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + if (fromHardwareButton) + [strongSelf->_interfaceView setShutterButtonHighlighted:false]; + + if (strongSelf->_previewView.hidden) + return; + + [strongSelf shutterReleased]; + }; + + _interfaceView.cancelPressed = ^ + { + __strong TGCameraController *strongSelf = weakSelf; + if (strongSelf != nil) + [strongSelf cancelPressed]; + }; + _interfaceView.resultPressed = ^(NSInteger index) + { + __strong TGCameraController *strongSelf = weakSelf; + if (strongSelf != nil) + [strongSelf presentResultControllerForItem:index == -1 ? nil : strongSelf->_items[index] completion:nil]; + }; + _interfaceView.itemRemoved = ^(NSInteger index) + { + __strong TGCameraController *strongSelf = weakSelf; + if (strongSelf != nil) + { + id item = [strongSelf->_items objectAtIndex:index]; + [strongSelf->_selectionContext setItem:item selected:false]; + [strongSelf->_items removeObjectAtIndex:index]; + [strongSelf->_selectedItemsModel removeSelectedItem:item]; + [strongSelf->_interfaceView setResults:[strongSelf->_items copy]]; + } + }; + + if (_intent != TGCameraControllerGenericIntent) + [_interfaceView setHasModeControl:false]; + + if (iosMajorVersion() >= 11) + { + _backgroundView.accessibilityIgnoresInvertColors = true; + _interfaceView.accessibilityIgnoresInvertColors = true; + _focusControl.accessibilityIgnoresInvertColors = true; + } + + [_autorotationCorrectionView addSubview:_interfaceView]; + + _photoSwipeGestureRecognizer = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(handleSwipe:)]; + _photoSwipeGestureRecognizer.delegate = self; + [_autorotationCorrectionView addGestureRecognizer:_photoSwipeGestureRecognizer]; + + _videoSwipeGestureRecognizer = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(handleSwipe:)]; + _videoSwipeGestureRecognizer.delegate = self; + [_autorotationCorrectionView addGestureRecognizer:_videoSwipeGestureRecognizer]; + + if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) + { + _photoSwipeGestureRecognizer.direction = UISwipeGestureRecognizerDirectionLeft; + _videoSwipeGestureRecognizer.direction = UISwipeGestureRecognizerDirectionRight; + } + else + { + _photoSwipeGestureRecognizer.direction = UISwipeGestureRecognizerDirectionUp; + _videoSwipeGestureRecognizer.direction = UISwipeGestureRecognizerDirectionDown; + } + + void (^buttonPressed)(void) = ^ + { + __strong TGCameraController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + strongSelf->_interfaceView.shutterPressed(true); + }; + + void (^buttonReleased)(void) = ^ + { + __strong TGCameraController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + strongSelf->_interfaceView.shutterReleased(true); + }; + + _buttonHandler = [[PGCameraVolumeButtonHandler alloc] initWithUpButtonPressedBlock:buttonPressed upButtonReleasedBlock:buttonReleased downButtonPressedBlock:buttonPressed downButtonReleasedBlock:buttonReleased]; + + [self _configureCamera]; +} + +- (void)_configureCamera +{ + __weak TGCameraController *weakSelf = self; + _camera.requestedCurrentInterfaceOrientation = ^UIInterfaceOrientation(bool *mirrored) + { + __strong TGCameraController *strongSelf = weakSelf; + if (strongSelf == nil) + return UIInterfaceOrientationUnknown; + + if (strongSelf->_intent == TGCameraControllerPassportIdIntent) + return UIInterfaceOrientationPortrait; + + if (mirrored != NULL) + { + TGCameraPreviewView *previewView = strongSelf->_previewView; + if (previewView != nil) + *mirrored = previewView.captureConnection.videoMirrored; + } + + return [strongSelf->_interfaceView interfaceOrientation]; + }; + + _camera.beganModeChange = ^(PGCameraMode mode, void(^commitBlock)(void)) + { + __strong TGCameraController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + strongSelf->_buttonHandler.ignoring = true; + + [strongSelf->_focusControl reset]; + strongSelf->_focusControl.active = false; + + strongSelf.view.userInteractionEnabled = false; + + PGCameraMode currentMode = strongSelf->_camera.cameraMode; + bool generalModeNotChanged = (mode == PGCameraModePhoto && currentMode == PGCameraModeSquare) || (mode == PGCameraModeSquare && currentMode == PGCameraModePhoto) || (mode == PGCameraModeVideo && currentMode == PGCameraModeClip) || (mode == PGCameraModeClip && currentMode == PGCameraModeVideo); + + if ((mode == PGCameraModeVideo || mode == PGCameraModeClip) && !generalModeNotChanged) + { + [[LegacyComponentsGlobals provider] pauseMusicPlayback]; + } + + if (generalModeNotChanged) + { + if (commitBlock != nil) + commitBlock(); + } + else + { + strongSelf->_camera.zoomLevel = 0.0f; + + [strongSelf->_camera captureNextFrameCompletion:^(UIImage *image) + { + if (commitBlock != nil) + commitBlock(); + + image = TGCameraModeSwitchImage(image, CGSizeMake(image.size.width, image.size.height)); + + TGDispatchOnMainThread(^ + { + [strongSelf->_previewView beginTransitionWithSnapshotImage:image animated:true]; + }); + }]; + } + }; + + _camera.finishedModeChange = ^ + { + __strong TGCameraController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + TGDispatchOnMainThread(^ + { + [strongSelf->_previewView endTransitionAnimated:true]; + + if (!strongSelf->_dismissing) + { + strongSelf.view.userInteractionEnabled = true; + [strongSelf resizePreviewViewForCameraMode:strongSelf->_camera.cameraMode]; + + strongSelf->_focusControl.active = true; + [strongSelf->_interfaceView setFlashMode:strongSelf->_camera.flashMode]; + + [strongSelf->_buttonHandler enableIn:1.5f]; + + if (strongSelf->_camera.cameraMode == PGCameraModeVideo && ([PGCamera microphoneAuthorizationStatus] == PGMicrophoneAuthorizationStatusRestricted || [PGCamera microphoneAuthorizationStatus] == PGMicrophoneAuthorizationStatusDenied) && !strongSelf->_shownMicrophoneAlert) + { + [[[LegacyComponentsGlobals provider] accessChecker] checkMicrophoneAuthorizationStatusForIntent:TGMicrophoneAccessIntentVideo alertDismissCompletion:nil]; + strongSelf->_shownMicrophoneAlert = true; + } + } + }); + }; + + _camera.beganPositionChange = ^(bool targetPositionHasFlash, bool targetPositionHasZoom, void(^commitBlock)(void)) + { + __strong TGCameraController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [strongSelf->_focusControl reset]; + + [strongSelf->_interfaceView setHasFlash:targetPositionHasFlash]; + [strongSelf->_interfaceView setHasZoom:targetPositionHasZoom]; + strongSelf->_camera.zoomLevel = 0.0f; + + strongSelf.view.userInteractionEnabled = false; + + [strongSelf->_camera captureNextFrameCompletion:^(UIImage *image) + { + if (commitBlock != nil) + commitBlock(); + + image = TGCameraPositionSwitchImage(image, CGSizeMake(image.size.width, image.size.height)); + + TGDispatchOnMainThread(^ + { + [UIView transitionWithView:strongSelf->_previewView duration:0.4f options:UIViewAnimationOptionTransitionFlipFromLeft | UIViewAnimationOptionCurveEaseOut animations:^ + { + [strongSelf->_previewView beginTransitionWithSnapshotImage:image animated:false]; + } completion:^(__unused BOOL finished) + { + strongSelf.view.userInteractionEnabled = true; + }]; + }); + }]; + }; + + _camera.finishedPositionChange = ^ + { + __strong TGCameraController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + TGDispatchOnMainThread(^ + { + [strongSelf->_previewView endTransitionAnimated:true]; + [strongSelf->_interfaceView setZoomLevel:0.0f displayNeeded:false]; + + if (strongSelf->_camera.hasFlash && strongSelf->_camera.flashActive) + [strongSelf->_interfaceView setFlashActive:true]; + + strongSelf->_focusControl.enabled = (strongSelf->_camera.supportsFocusPOI || strongSelf->_camera.supportsExposurePOI); + strongSelf->_focusControl.stopAutomatically = (strongSelf->_focusControl.enabled && !strongSelf->_camera.supportsFocusPOI); + }); + }; + + _camera.beganAdjustingFocus = ^ + { + __strong TGCameraController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [strongSelf->_focusControl playAutoFocusAnimation]; + }; + + _camera.finishedAdjustingFocus = ^ + { + __strong TGCameraController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [strongSelf->_focusControl stopAutoFocusAnimation]; + }; + + _camera.flashActivityChanged = ^(bool active) + { + __strong TGCameraController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + if (strongSelf->_camera.flashMode != PGCameraFlashModeAuto) + active = false; + + TGDispatchOnMainThread(^ + { + if (!strongSelf->_camera.isRecordingVideo) + [strongSelf->_interfaceView setFlashActive:active]; + }); + }; + + _camera.flashAvailabilityChanged = ^(bool available) + { + __strong TGCameraController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [strongSelf->_interfaceView setFlashUnavailable:!available]; + }; + + _camera.beganVideoRecording = ^(__unused bool moment) + { + __strong TGCameraController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + strongSelf->_focusControl.ignoreAutofocusing = true; + [strongSelf->_interfaceView setRecordingVideo:true animated:true]; + }; + + _camera.captureInterrupted = ^(AVCaptureSessionInterruptionReason reason) + { + __strong TGCameraController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + if (reason == AVCaptureSessionInterruptionReasonVideoDeviceNotAvailableWithMultipleForegroundApps) + [strongSelf beginTransitionOutWithVelocity:0.0f]; + }; + + _camera.finishedVideoRecording = ^(__unused bool moment) + { + __strong TGCameraController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + strongSelf->_focusControl.ignoreAutofocusing = false; + [strongSelf->_interfaceView setFlashMode:PGCameraFlashModeOff]; + }; + + _camera.deviceAngleSampler.deviceOrientationChanged = ^(UIDeviceOrientation orientation) + { + __strong TGCameraController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [strongSelf handleDeviceOrientationChangedTo:orientation]; + }; + + _camera.captureSession.recognizedQRCode = ^(NSString *value, AVMetadataMachineReadableCodeObject *object) + { + __strong TGCameraController *strongSelf = weakSelf; + if (strongSelf != nil && strongSelf.recognizedQRCode != nil) + { + if (![strongSelf->_previousQRCodes containsObject:value]) + { + strongSelf.recognizedQRCode(value); + [strongSelf->_previousQRCodes addObject:value]; + } + } + }; +} + +#pragma mark - View Life Cycle + +- (void)viewWillAppear:(BOOL)animated +{ + [super viewWillAppear:animated]; + + [UIView animateWithDuration:0.3f animations:^ + { + [_context setApplicationStatusBarAlpha:0.0f]; + }]; + + [[[LegacyComponentsGlobals provider] applicationInstance] setIdleTimerDisabled:true]; + + if (!_camera.isCapturing) + [_camera startCaptureForResume:false completion:nil]; +} + +- (void)viewWillDisappear:(BOOL)animated +{ + [super viewWillDisappear:animated]; + + [UIView animateWithDuration:0.3f animations:^ + { + [_context setApplicationStatusBarAlpha:1.0f]; + }]; +} + +- (void)viewWillLayoutSubviews +{ + [super viewWillLayoutSubviews]; + + if ([self shouldCorrectAutorotation]) + [self applyAutorotationCorrectingTransformForOrientation:[[LegacyComponentsGlobals provider] applicationStatusBarOrientation]]; +} + +- (bool)shouldCorrectAutorotation +{ + return [UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad; +} + +- (void)applyAutorotationCorrectingTransformForOrientation:(UIInterfaceOrientation)orientation +{ + CGSize screenSize = TGScreenSize(); + CGRect screenBounds = CGRectMake(0, 0, screenSize.width, screenSize.height); + + _autorotationCorrectionView.transform = CGAffineTransformIdentity; + _autorotationCorrectionView.frame = screenBounds; + + CGAffineTransform transform = CGAffineTransformIdentity; + switch (orientation) + { + case UIInterfaceOrientationPortraitUpsideDown: + transform = CGAffineTransformMakeRotation(M_PI); + break; + + case UIInterfaceOrientationLandscapeLeft: + transform = CGAffineTransformMakeRotation(M_PI_2); + break; + + case UIInterfaceOrientationLandscapeRight: + transform = CGAffineTransformMakeRotation(-M_PI_2); + break; + + default: + break; + } + + _autorotationCorrectionView.transform = transform; + CGSize bounds = [_context fullscreenBounds].size; + _autorotationCorrectionView.center = CGPointMake(bounds.width / 2, bounds.height / 2); +} + +- (UIInterfaceOrientationMask)supportedInterfaceOrientations +{ + if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad) + return UIInterfaceOrientationMaskAll; + + return UIInterfaceOrientationMaskPortrait; +} + +- (BOOL)shouldAutorotate +{ + if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad) + return true; + + return false; +} + +- (void)setInterfaceHidden:(bool)hidden animated:(bool)animated +{ + if (animated) + { + if (hidden && _interfaceView.alpha < FLT_EPSILON) + return; + + CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"opacity"]; + animation.fromValue = @(_interfaceView.alpha); + animation.toValue = @(hidden ? 0.0f : 1.0f); + animation.duration = 0.2f; + [_interfaceView.layer addAnimation:animation forKey:@"opacity"]; + + _interfaceView.alpha = hidden ? 0.0f : 1.0f; + } + else + { + [_interfaceView.layer removeAllAnimations]; + _interfaceView.alpha = 0.0f; + } +} + +#pragma mark - + +- (void)startVideoRecording +{ + __weak TGCameraController *weakSelf = self; + if (_camera.cameraMode == PGCameraModePhoto) + { + _switchToVideoTimer = nil; + + _camera.onAutoStartVideoRecording = ^ + { + __strong TGCameraController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + strongSelf->_stopRecordingOnRelease = true; + + [strongSelf->_camera startVideoRecordingForMoment:false completion:^(NSURL *outputURL, __unused CGAffineTransform transform, CGSize dimensions, NSTimeInterval duration, bool success) + { + __strong TGCameraController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + if (success) + { + TGCameraCapturedVideo *capturedVideo = [[TGCameraCapturedVideo alloc] initWithURL:outputURL]; + [strongSelf addResultItem:capturedVideo]; + + if (![strongSelf maybePresentResultControllerForItem:capturedVideo completion:nil]) + { + strongSelf->_camera.disabled = false; + [strongSelf->_interfaceView setRecordingVideo:false animated:true]; + } + } + else + { + [strongSelf->_interfaceView setRecordingVideo:false animated:false]; + } + }]; + }; + _camera.autoStartVideoRecording = true; + + [_camera setCameraMode:PGCameraModeVideo]; + [_interfaceView setCameraMode:PGCameraModeVideo]; + } + else if (_camera.cameraMode == PGCameraModeVideo) + { + _startRecordingTimer = nil; + + [_camera startVideoRecordingForMoment:false completion:^(NSURL *outputURL, __unused CGAffineTransform transform, CGSize dimensions, NSTimeInterval duration, bool success) + { + __strong TGCameraController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + if (success) + { + TGCameraCapturedVideo *capturedVideo = [[TGCameraCapturedVideo alloc] initWithURL:outputURL]; + [strongSelf addResultItem:capturedVideo]; + + if (![strongSelf maybePresentResultControllerForItem:capturedVideo completion:nil]) + { + strongSelf->_camera.disabled = false; + [strongSelf->_interfaceView setRecordingVideo:false animated:true]; + } + } + else + { + [strongSelf->_interfaceView setRecordingVideo:false animated:false]; + } + }]; + + _stopRecordingOnRelease = true; + } +} + +- (void)shutterPressed +{ + PGCameraMode cameraMode = _camera.cameraMode; + switch (cameraMode) + { + case PGCameraModePhoto: + { + if (_intent == TGCameraControllerGenericIntent) + { + _switchToVideoTimer = [TGTimerTarget scheduledMainThreadTimerWithTarget:self action:@selector(startVideoRecording) interval:0.25 repeat:false]; + } + } + break; + + case PGCameraModeVideo: + { + if (!_camera.isRecordingVideo) + { + _startRecordingTimer = [TGTimerTarget scheduledMainThreadTimerWithTarget:self action:@selector(startVideoRecording) interval:0.25 repeat:false]; + } + else + { + _stopRecordingOnRelease = true; + } + } + break; + + case PGCameraModeClip: + { + } + break; + + default: + break; + } +} + +- (void)shutterReleased +{ + [_switchToVideoTimer invalidate]; + _switchToVideoTimer = nil; + + [_startRecordingTimer invalidate]; + _startRecordingTimer = nil; + + if (_shutterIsBusy) + return; + + __weak TGCameraController *weakSelf = self; + PGCameraMode cameraMode = _camera.cameraMode; + if (cameraMode == PGCameraModePhoto || cameraMode == PGCameraModeSquare) + { + _camera.disabled = true; + + _shutterIsBusy = true; + if (![self willPresentResultController]) + { + TGDispatchAfter(0.35, dispatch_get_main_queue(), ^ + { + [_previewView blink]; + }); + } + else + { + _buttonHandler.enabled = false; + [_buttonHandler ignoreEventsFor:1.5f andDisable:true]; + } + + [_camera takePhotoWithCompletion:^(UIImage *result, PGCameraShotMetadata *metadata) + { + __strong TGCameraController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + TGDispatchOnMainThread(^ + { + strongSelf->_shutterIsBusy = false; + + if (strongSelf->_intent == TGCameraControllerAvatarIntent || strongSelf->_intent == TGCameraControllerSignupAvatarIntent) + { + [strongSelf presentPhotoResultControllerWithImage:result metadata:metadata completion:^{}]; + } + else + { + TGCameraCapturedPhoto *capturedPhoto = [[TGCameraCapturedPhoto alloc] initWithImage:result metadata:metadata]; + [strongSelf addResultItem:capturedPhoto]; + + if (![strongSelf maybePresentResultControllerForItem:capturedPhoto completion:nil]) + strongSelf->_camera.disabled = false; + } + }); + }]; + } + else if (cameraMode == PGCameraModeVideo) + { + if (!_camera.isRecordingVideo) + { + [_buttonHandler ignoreEventsFor:1.0f andDisable:false]; + + [_camera startVideoRecordingForMoment:false completion:^(NSURL *outputURL, __unused CGAffineTransform transform, CGSize dimensions, NSTimeInterval duration, bool success) + { + __strong TGCameraController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + if (success) + { + TGCameraCapturedVideo *capturedVideo = [[TGCameraCapturedVideo alloc] initWithURL:outputURL]; + [strongSelf addResultItem:capturedVideo]; + + if (![strongSelf maybePresentResultControllerForItem:capturedVideo completion:nil]) + { + strongSelf->_camera.disabled = false; + [strongSelf->_interfaceView setRecordingVideo:false animated:true]; + } + } + else + { + [strongSelf->_interfaceView setRecordingVideo:false animated:false]; + } + }]; + } + else if (_stopRecordingOnRelease) + { + _stopRecordingOnRelease = false; + + _camera.disabled = true; + [_camera stopVideoRecording]; + + [_buttonHandler ignoreEventsFor:1.0f andDisable:[self willPresentResultController]]; + } + } +} + +- (void)cancelPressed +{ + if (_items.count > 0) + { + __weak TGCameraController *weakSelf = self; + + TGMenuSheetController *controller = [[TGMenuSheetController alloc] initWithContext:_context dark:false]; + controller.dismissesByOutsideTap = true; + controller.narrowInLandscape = true; + __weak TGMenuSheetController *weakController = controller; + + NSArray *items = @ + [ + [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Camera.Discard") type:TGMenuSheetButtonTypeDefault action:^ + { + __strong TGMenuSheetController *strongController = weakController; + if (strongController == nil) + return; + + __strong TGCameraController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [strongController dismissAnimated:true manual:false completion:nil]; + [strongSelf beginTransitionOutWithVelocity:0.0f]; + }], + [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Common.Cancel") type:TGMenuSheetButtonTypeCancel action:^ + { + __strong TGMenuSheetController *strongController = weakController; + if (strongController != nil) + [strongController dismissAnimated:true]; + }] + ]; + + [controller setItemViews:items]; + controller.sourceRect = ^ + { + __strong TGCameraController *strongSelf = weakSelf; + if (strongSelf == nil) + return CGRectZero; + + UIButton *cancelButton = strongSelf->_interfaceView->_cancelButton; + return [cancelButton convertRect:cancelButton.bounds toView:strongSelf.view]; + }; + controller.permittedArrowDirections = UIPopoverArrowDirectionAny; + [controller presentInViewController:self sourceView:self.view animated:true]; + } + else + { + [self beginTransitionOutWithVelocity:0.0f]; + } +} + +#pragma mark - Result + +- (void)addResultItem:(id)item +{ + [_items addObject:item]; +} + +- (bool)willPresentResultController +{ + return _items.count == 0 || (_items.count > 0 && (_items.count + 1) % 10 == 0); +} + +- (bool)shouldPresentResultController +{ + return _items.count == 1 || (_items.count > 0 && _items.count % 10 == 0); +} + +- (bool)maybePresentResultControllerForItem:(id)editableItem completion:(void (^)(void))completion +{ + if ([self shouldPresentResultController]) + { + [self presentResultControllerForItem:editableItem completion:^ + { + [_selectedItemsModel addSelectedItem:editableItem]; + [_selectionContext setItem:editableItem selected:true]; + [_interfaceView setResults:[_items copy]]; + if (completion != nil) + completion(); + }]; + return true; + } + else + { + [_selectedItemsModel addSelectedItem:editableItem]; + [_selectionContext setItem:editableItem selected:true]; + [_interfaceView setResults:[_items copy]]; + return false; + } +} + +- (NSArray *)prepareGalleryItemsForResults:(void (^)(TGMediaPickerGalleryItem *))enumerationBlock +{ + NSMutableArray *galleryItems = [[NSMutableArray alloc] init]; + for (id item in _items) + { + TGMediaPickerGalleryItem *galleryItem = nil; + if ([item isKindOfClass:[TGCameraCapturedPhoto class]]) + { + galleryItem = [[TGMediaPickerGalleryPhotoItem alloc] initWithAsset:item]; + } + else if ([item isKindOfClass:[TGCameraCapturedVideo class]]) + { + galleryItem = [[TGMediaPickerGalleryVideoItem alloc] initWithAsset:item]; + } + + galleryItem.selectionContext = _selectionContext; + galleryItem.editingContext = _editingContext; + + if (enumerationBlock != nil) + enumerationBlock(galleryItem); + + if (galleryItem != nil) + [galleryItems addObject:galleryItem]; + } + + return galleryItems; +} + +- (void)presentResultControllerForItem:(id)editableItemValue completion:(void (^)(void))completion +{ + __block id editableItem = editableItemValue; + UIViewController *(^begin)(id) = ^(id windowContext) { + 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]; + if (self.allowGrouping) + selectionContext.grouping = ![[[NSUserDefaults standardUserDefaults] objectForKey:@"TG_mediaGroupingDisabled_v0"] boolValue]; + _selectionContext = selectionContext; + } + + if (editableItem == nil) + editableItem = _items.lastObject; + + [[[LegacyComponentsGlobals provider] applicationInstance] setIdleTimerDisabled:false]; + + if (_intent == TGCameraControllerPassportIdIntent) + { + TGCameraCapturedPhoto *photo = (TGCameraCapturedPhoto *)editableItem; + CGSize size = photo.originalSize; + CGFloat height = size.width * 0.704f; + PGPhotoEditorValues *values = [PGPhotoEditorValues editorValuesWithOriginalSize:size cropRect:CGRectMake(0, floor((size.height - height) / 2.0f), size.width, height) cropRotation:0.0f cropOrientation:UIImageOrientationUp cropLockedAspectRatio:0.0f cropMirrored:false toolValues:nil paintingData:nil sendAsGif:false]; + + SSignal *cropSignal = [[photo originalImageSignal:0.0] map:^UIImage *(UIImage *image) + { + UIImage *croppedImage = TGPhotoEditorCrop(image, nil, UIImageOrientationUp, 0.0f, values.cropRect, false, TGPhotoEditorResultImageMaxSize, size, true); + return croppedImage; + }]; + + [cropSignal startWithNext:^(UIImage *image) + { + CGSize fillSize = TGPhotoThumbnailSizeForCurrentScreen(); + fillSize.width = CGCeil(fillSize.width); + fillSize.height = CGCeil(fillSize.height); + + CGSize size = TGScaleToFillSize(image.size, fillSize); + + UIGraphicsBeginImageContextWithOptions(size, true, 0.0f); + CGContextRef context = UIGraphicsGetCurrentContext(); + CGContextSetInterpolationQuality(context, kCGInterpolationMedium); + + [image drawInRect:CGRectMake(0, 0, size.width, size.height)]; + + UIImage *thumbnailImage = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + + [editingContext setAdjustments:values forItem:photo]; + [editingContext setImage:image thumbnailImage:thumbnailImage forItem:photo synchronous:true]; + }]; + } + + __weak TGCameraController *weakSelf = self; + TGModernGalleryController *galleryController = [[TGModernGalleryController alloc] initWithContext:windowContext]; + galleryController.adjustsStatusBarVisibility = false; + galleryController.hasFadeOutTransition = true; + + __block id focusItem = nil; + NSArray *galleryItems = [self prepareGalleryItemsForResults:^(TGMediaPickerGalleryItem *item) + { + if (focusItem == nil && [item.asset isEqual:editableItem]) + { + focusItem = item; + + if ([item.asset isKindOfClass:[TGCameraCapturedVideo class]]) + { + AVAssetImageGenerator *generator = [[AVAssetImageGenerator alloc] initWithAsset:((TGCameraCapturedVideo *)item.asset).avAsset]; + generator.appliesPreferredTrackTransform = true; + generator.maximumSize = CGSizeMake(640.0f, 640.0f); + CGImageRef imageRef = [generator copyCGImageAtTime:kCMTimeZero actualTime:NULL error:NULL]; + UIImage *thumbnailImage = [[UIImage alloc] initWithCGImage:imageRef]; + CGImageRelease(imageRef); + + item.immediateThumbnailImage = thumbnailImage; + } + } + }]; + + bool hasCamera = !self.inhibitMultipleCapture && ((_intent == TGCameraControllerGenericIntent && !_shortcut) || (_intent == TGCameraControllerPassportMultipleIntent)); + TGMediaPickerGalleryModel *model = [[TGMediaPickerGalleryModel alloc] initWithContext:windowContext items:galleryItems focusItem:focusItem selectionContext:_items.count > 1 ? selectionContext : nil editingContext:editingContext hasCaptions:self.allowCaptions allowCaptionEntities:self.allowCaptionEntities hasTimer:self.hasTimer onlyCrop:_intent == TGCameraControllerPassportIntent || _intent == TGCameraControllerPassportIdIntent || _intent == TGCameraControllerPassportMultipleIntent inhibitDocumentCaptions:self.inhibitDocumentCaptions hasSelectionPanel:true hasCamera:hasCamera recipientName:self.recipientName]; + model.inhibitMute = self.inhibitMute; + model.controller = galleryController; + model.suggestionContext = self.suggestionContext; + + model.willFinishEditingItem = ^(id editableItem, id adjustments, id representation, bool hasChanges) + { + __strong TGCameraController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + if (hasChanges) + { + [editingContext setAdjustments:adjustments forItem:editableItem]; + [editingContext setTemporaryRep:representation forItem:editableItem]; + } + }; + + model.didFinishEditingItem = ^(id editableItem, __unused id adjustments, UIImage *resultImage, UIImage *thumbnailImage) + { + [editingContext setImage:resultImage thumbnailImage:thumbnailImage forItem:editableItem synchronous:false]; + }; + + model.saveItemCaption = ^(id editableItem, NSString *caption, NSArray *entities) + { + __strong TGCameraController *strongSelf = weakSelf; + if (strongSelf != nil) + [strongSelf->_editingContext setCaption:caption entities:entities forItem:editableItem]; + }; + + model.interfaceView.hasSwipeGesture = false; + galleryController.model = model; + + __weak TGModernGalleryController *weakGalleryController = galleryController; + __weak TGMediaPickerGalleryModel *weakModel = model; + + if (_items.count > 1) + [model.interfaceView updateSelectionInterface:selectionContext.count counterVisible:(selectionContext.count > 0) animated:false]; + else + [model.interfaceView updateSelectionInterface:1 counterVisible:false animated:false]; + model.interfaceView.thumbnailSignalForItem = ^SSignal *(id item) + { + __strong TGCameraController *strongSelf = weakSelf; + if (strongSelf != nil) + return [strongSelf _signalForItem:item]; + return nil; + }; + model.interfaceView.donePressed = ^(TGMediaPickerGalleryItem *item) + { + __strong TGCameraController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + TGMediaPickerGalleryModel *strongModel = weakModel; + if (strongModel == nil) + return; + + __strong TGModernGalleryController *strongController = weakGalleryController; + if (strongController == nil) + return; + + if ([item isKindOfClass:[TGMediaPickerGalleryVideoItem class]]) + { + TGMediaPickerGalleryVideoItemView *itemView = (TGMediaPickerGalleryVideoItemView *)[strongController itemViewForItem:item]; + [itemView stop]; + [itemView setPlayButtonHidden:true animated:true]; + } + + if (strongSelf->_selectionContext.allowGrouping) + [[NSUserDefaults standardUserDefaults] setObject:@(!strongSelf->_selectionContext.grouping) forKey:@"TG_mediaGroupingDisabled_v0"]; + + if (strongSelf.finishedWithResults != nil) + strongSelf.finishedWithResults(strongController, strongSelf->_selectionContext, strongSelf->_editingContext, item.asset); + + if (strongSelf->_shortcut) + return; + + [strongSelf _dismissTransitionForResultController:strongController]; + }; + + CGSize snapshotSize = TGScaleToFill(CGSizeMake(480, 640), CGSizeMake(self.view.frame.size.width, self.view.frame.size.width)); + UIView *snapshotView = [_previewView snapshotViewAfterScreenUpdates:false]; + snapshotView.contentMode = UIViewContentModeScaleAspectFill; + snapshotView.frame = CGRectMake(_previewView.center.x - snapshotSize.width / 2, _previewView.center.y - snapshotSize.height / 2, snapshotSize.width, snapshotSize.height); + snapshotView.hidden = true; + [_previewView.superview insertSubview:snapshotView aboveSubview:_previewView]; + + galleryController.beginTransitionIn = ^UIView *(__unused TGMediaPickerGalleryItem *item, __unused TGModernGalleryItemView *itemView) + { + __strong TGCameraController *strongSelf = weakSelf; + if (strongSelf != nil) + { + TGModernGalleryController *strongGalleryController = weakGalleryController; + strongGalleryController.view.alpha = 0.0f; + [UIView animateWithDuration:0.3f animations:^ + { + strongGalleryController.view.alpha = 1.0f; + strongSelf->_interfaceView.alpha = 0.0f; + }]; + return snapshotView; + } + return nil; + }; + + galleryController.finishedTransitionIn = ^(__unused TGMediaPickerGalleryItem *item, __unused TGModernGalleryItemView *itemView) + { + __strong TGCameraController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + TGMediaPickerGalleryModel *strongModel = weakModel; + if (strongModel == nil) + return; + + [strongModel.interfaceView setSelectedItemsModel:strongModel.selectedItemsModel]; + + [strongSelf->_camera stopCaptureForPause:true completion:nil]; + + snapshotView.hidden = true; + + if (completion != nil) + completion(); + }; + + galleryController.beginTransitionOut = ^UIView *(__unused TGMediaPickerGalleryItem *item, __unused TGModernGalleryItemView *itemView) + { + __strong TGCameraController *strongSelf = weakSelf; + if (strongSelf != nil) + { + TGMediaPickerGalleryModel *strongModel = weakModel; + if (strongModel == nil) + return nil; + + [[[LegacyComponentsGlobals provider] applicationInstance] setIdleTimerDisabled:true]; + + if (strongSelf->_camera.cameraMode == PGCameraModeVideo) + [strongSelf->_interfaceView setRecordingVideo:false animated:false]; + + strongSelf->_buttonHandler.enabled = true; + [strongSelf->_buttonHandler ignoreEventsFor:2.0f andDisable:false]; + + strongSelf->_camera.disabled = false; + [strongSelf->_camera startCaptureForResume:true completion:nil]; + + [UIView animateWithDuration:0.3f delay:0.1f options:UIViewAnimationOptionCurveLinear animations:^ + { + strongSelf->_interfaceView.alpha = 1.0f; + } completion:nil]; + + if (!strongModel.interfaceView.capturing) + { + [strongSelf->_items removeAllObjects]; + [strongSelf->_interfaceView setResults:nil]; + [strongSelf->_selectionContext clear]; + [strongSelf->_selectedItemsModel clear]; + + [strongSelf->_interfaceView updateSelectionInterface:0 counterVisible:false animated:false]; + } + + return snapshotView; + } + return nil; + }; + + void (^dismissGalleryImpl)() = nil; + + galleryController.completedTransitionOut = ^ + { + [snapshotView removeFromSuperview]; + + TGModernGalleryController *strongGalleryController = weakGalleryController; + if (strongGalleryController == nil) { + return; + } + if (strongGalleryController.customDismissSelf) { + strongGalleryController.customDismissSelf(); + } + }; + + TGOverlayController *contentController = galleryController; + if (_shortcut) + { + contentController = [[TGOverlayController alloc] initWithContext:_context]; + + TGNavigationController *navigationController = [TGNavigationController navigationControllerWithControllers:@[galleryController]]; + galleryController.navigationBarShouldBeHidden = true; + + [contentController addChildViewController:navigationController]; + [contentController.view addSubview:navigationController.view]; + } + + if (_customPresentOverlayController) { + dismissGalleryImpl = ^{ + TGModernGalleryController *strongGalleryController = weakGalleryController; + if (strongGalleryController == nil) { + return; + } + if (strongGalleryController.customDismissSelf) { + strongGalleryController.customDismissSelf(); + } + }; + } else { + dismissGalleryImpl = ^{ + TGModernGalleryController *strongGalleryController = weakGalleryController; + if (strongGalleryController != nil && strongGalleryController.overlayWindow == nil) + { + TGNavigationController *navigationController = (TGNavigationController *)strongGalleryController.navigationController; + TGOverlayControllerWindow *window = (TGOverlayControllerWindow *)navigationController.view.window; + if ([window isKindOfClass:[TGOverlayControllerWindow class]]) + [window dismiss]; + } + }; + } + galleryController.view.clipsToBounds = true; + return contentController; + }; + + if (_customPresentOverlayController) { + _customPresentOverlayController(^TGOverlayController * (id context) { + return (TGOverlayController *)begin(context); + }); + } else { + id windowManager = nil; + id windowContext = nil; + windowManager = [_context makeOverlayWindowManager]; + windowContext = [windowManager context]; + + UIViewController *controller = begin(windowContext); + + TGOverlayControllerWindow *controllerWindow = [[TGOverlayControllerWindow alloc] initWithManager:windowManager parentController:self contentController:(TGOverlayController *)controller]; + controllerWindow.hidden = false; + controllerWindow.windowLevel = self.view.window.windowLevel + 0.0001f; + } +} + +- (SSignal *)_signalForItem:(id)item +{ + SSignal *assetSignal = [item thumbnailImageSignal]; + if (_editingContext == nil) + return assetSignal; + + return [[_editingContext thumbnailImageSignalForItem:item] mapToSignal:^SSignal *(id result) + { + if (result != nil) + return [SSignal single:result]; + else + return assetSignal; + }]; +} + +#pragma mark - Legacy Photo Result + +- (void)presentPhotoResultControllerWithImage:(UIImage *)image metadata:(PGCameraShotMetadata *)metadata completion:(void (^)(void))completion +{ + [[[LegacyComponentsGlobals provider] applicationInstance] setIdleTimerDisabled:false]; + + if (image == nil || image.size.width < FLT_EPSILON) + { + [self beginTransitionOutWithVelocity:0.0f]; + return; + } + + id windowManager = nil; + id windowContext = nil; + if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPhone) { + windowManager = [_context makeOverlayWindowManager]; + windowContext = [windowManager context]; + } else { + windowContext = _context; + } + + __weak TGCameraController *weakSelf = self; + TGOverlayController *overlayController = nil; + + _focusControl.ignoreAutofocusing = true; + + switch (_intent) + { + case TGCameraControllerAvatarIntent: + case TGCameraControllerSignupAvatarIntent: + { + TGPhotoEditorControllerIntent intent = TGPhotoEditorControllerAvatarIntent; + if (_intent == TGCameraControllerSignupAvatarIntent) { + intent = TGPhotoEditorControllerSignupAvatarIntent; + } + TGPhotoEditorController *controller = [[TGPhotoEditorController alloc] initWithContext:windowContext item:image intent:(TGPhotoEditorControllerFromCameraIntent | intent) adjustments:nil caption:nil screenImage:image availableTabs:[TGPhotoEditorController defaultTabsForAvatarIntent] selectedTab:TGPhotoEditorCropTab]; + __weak TGPhotoEditorController *weakController = controller; + controller.beginTransitionIn = ^UIView *(CGRect *referenceFrame, __unused UIView **parentView) + { + __strong TGCameraController *strongSelf = weakSelf; + if (strongSelf == nil) + return nil; + + strongSelf->_previewView.hidden = true; + *referenceFrame = strongSelf->_previewView.frame; + + UIImageView *imageView = [[UIImageView alloc] initWithFrame:strongSelf->_previewView.frame]; + imageView.image = image; + + return imageView; + }; + + controller.beginTransitionOut = ^UIView *(CGRect *referenceFrame, __unused UIView **parentView) + { + __strong TGCameraController *strongSelf = weakSelf; + if (strongSelf == nil) + return nil; + + CGRect startFrame = CGRectZero; + if (referenceFrame != NULL) + { + startFrame = *referenceFrame; + *referenceFrame = strongSelf->_previewView.frame; + } + + [strongSelf transitionBackFromResultControllerWithReferenceFrame:startFrame]; + + return strongSelf->_previewView; + }; + + controller.didFinishEditing = ^(PGPhotoEditorValues *editorValues, UIImage *resultImage, __unused UIImage *thumbnailImage, bool hasChanges) + { + if (!hasChanges) + return; + + __strong TGCameraController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + TGDispatchOnMainThread(^ + { + if (strongSelf.finishedWithPhoto != nil) + strongSelf.finishedWithPhoto(nil, resultImage, nil, nil, nil, nil); + + if (strongSelf.shouldStoreCapturedAssets) + { + [strongSelf _savePhotoToCameraRollWithOriginalImage:image editedImage:[editorValues toolsApplied] ? resultImage : nil]; + } + + __strong TGPhotoEditorController *strongController = weakController; + if (strongController != nil) + { + [strongController updateStatusBarAppearanceForDismiss]; + [strongSelf _dismissTransitionForResultController:(TGOverlayController *)strongController]; + } + }); + }; + + controller.requestThumbnailImage = ^(id editableItem) + { + return [editableItem thumbnailImageSignal]; + }; + + controller.requestOriginalScreenSizeImage = ^(id editableItem, NSTimeInterval position) + { + return [editableItem screenImageSignal:position]; + }; + + controller.requestOriginalFullSizeImage = ^(id editableItem, NSTimeInterval position) + { + return [editableItem originalImageSignal:position]; + }; + + overlayController = (TGOverlayController *)controller; + } + break; + + default: + { + TGCameraPhotoPreviewController *controller = _shortcut ? [[TGCameraPhotoPreviewController alloc] initWithContext:windowContext image:image metadata:metadata recipientName:self.recipientName backButtonTitle:TGLocalized(@"Camera.Retake") doneButtonTitle:TGLocalized(@"Common.Next") saveCapturedMedia:_saveCapturedMedia saveEditedPhotos:_saveEditedPhotos] : [[TGCameraPhotoPreviewController alloc] initWithContext:windowContext image:image metadata:metadata recipientName:self.recipientName saveCapturedMedia:_saveCapturedMedia saveEditedPhotos:_saveEditedPhotos]; + controller.allowCaptions = self.allowCaptions; + controller.shouldStoreAssets = self.shouldStoreCapturedAssets; + controller.suggestionContext = self.suggestionContext; + controller.hasTimer = self.hasTimer; + + __weak TGCameraPhotoPreviewController *weakController = controller; + controller.beginTransitionIn = ^CGRect + { + __strong TGCameraController *strongSelf = weakSelf; + if (strongSelf == nil) + return CGRectZero; + + strongSelf->_previewView.hidden = true; + + return strongSelf->_previewView.frame; + }; + + controller.finishedTransitionIn = ^ + { + __strong TGCameraController *strongSelf = weakSelf; + if (strongSelf != nil) + [strongSelf->_camera stopCaptureForPause:true completion:nil]; + }; + + controller.beginTransitionOut = ^CGRect(CGRect referenceFrame) + { + __strong TGCameraController *strongSelf = weakSelf; + if (strongSelf == nil) + return CGRectZero; + + [strongSelf->_camera startCaptureForResume:true completion:nil]; + + return [strongSelf transitionBackFromResultControllerWithReferenceFrame:referenceFrame]; + }; + + controller.retakePressed = ^ + { + __strong TGCameraController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [[[LegacyComponentsGlobals provider] applicationInstance] setIdleTimerDisabled:true]; + }; + + controller.sendPressed = ^(TGOverlayController *controller, UIImage *resultImage, NSString *caption, NSArray *entities, NSArray *stickers, NSNumber *timer) + { + __strong TGCameraController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + if (strongSelf.finishedWithPhoto != nil) + strongSelf.finishedWithPhoto(controller, resultImage, caption, entities, stickers, timer); + + if (strongSelf->_shortcut) + return; + + __strong TGOverlayController *strongController = weakController; + if (strongController != nil) + [strongSelf _dismissTransitionForResultController:strongController]; + }; + + overlayController = controller; + } + break; + } + + if (windowManager != nil) + { + TGOverlayController *contentController = overlayController; + if (_shortcut) + { + contentController = [[TGOverlayController alloc] init]; + + TGNavigationController *navigationController = [TGNavigationController navigationControllerWithControllers:@[overlayController]]; + overlayController.navigationBarShouldBeHidden = true; + [contentController addChildViewController:navigationController]; + [contentController.view addSubview:navigationController.view]; + } + + TGOverlayControllerWindow *controllerWindow = [[TGOverlayControllerWindow alloc] initWithManager:windowManager parentController:self contentController:contentController]; + controllerWindow.windowLevel = self.view.window.windowLevel + 0.0001f; + controllerWindow.hidden = false; + } + else + { + [self addChildViewController:overlayController]; + [self.view addSubview:overlayController.view]; + } + + if (completion != nil) + completion(); + + [UIView animateWithDuration:0.3f animations:^ + { + _interfaceView.alpha = 0.0f; + }]; +} + +- (void)_savePhotoToCameraRollWithOriginalImage:(UIImage *)originalImage editedImage:(UIImage *)editedImage +{ + if (!_saveEditedPhotos || originalImage == nil) + return; + + SSignal *savePhotoSignal = _saveCapturedMedia ? [[TGMediaAssetsLibrary sharedLibrary] saveAssetWithImage:originalImage] : [SSignal complete]; + if (_saveEditedPhotos && editedImage != nil) + savePhotoSignal = [savePhotoSignal then:[[TGMediaAssetsLibrary sharedLibrary] saveAssetWithImage:editedImage]]; + + [savePhotoSignal startWithNext:nil]; +} + +- (void)_saveVideoToCameraRollWithURL:(NSURL *)url completion:(void (^)(void))completion +{ + if (!_saveCapturedMedia) + return; + + [[[TGMediaAssetsLibrary sharedLibrary] saveAssetWithVideoAtUrl:url] startWithNext:nil error:^(__unused NSError *error) + { + if (completion != nil) + completion(); + } completed:completion]; +} + +- (CGRect)transitionBackFromResultControllerWithReferenceFrame:(CGRect)referenceFrame +{ + _camera.disabled = false; + + _buttonHandler.enabled = true; + [_buttonHandler ignoreEventsFor:2.0f andDisable:false]; + _previewView.hidden = false; + + _focusControl.ignoreAutofocusing = false; + + CGRect targetFrame = _previewView.frame; + + _previewView.frame = referenceFrame; + POPSpringAnimation *animation = [TGPhotoEditorAnimation prepareTransitionAnimationForPropertyNamed:kPOPViewFrame]; + animation.fromValue = [NSValue valueWithCGRect:referenceFrame]; + animation.toValue = [NSValue valueWithCGRect:targetFrame]; + [_previewView pop_addAnimation:animation forKey:@"frame"]; + + [UIView animateWithDuration:0.3f delay:0.1f options:UIViewAnimationOptionCurveLinear animations:^ + { + _interfaceView.alpha = 1.0f; + } completion:nil]; + + _interfaceView.previewViewFrame = _previewView.frame; + [_interfaceView layoutPreviewRelativeViews]; + + return targetFrame; +} + +#pragma mark - Transition + +- (void)beginTransitionInFromRect:(CGRect)rect +{ + [_autorotationCorrectionView insertSubview:_previewView aboveSubview:_backgroundView]; + + _previewView.frame = rect; + + _backgroundView.alpha = 0.0f; + _interfaceView.alpha = 0.0f; + + [UIView animateWithDuration:0.3f animations:^ + { + _backgroundView.alpha = 1.0f; + _interfaceView.alpha = 1.0f; + }]; + + CGRect fromFrame = rect; + CGRect toFrame = [TGCameraController _cameraPreviewFrameForScreenSize:TGScreenSize() mode:_camera.cameraMode]; + + if (!CGRectEqualToRect(fromFrame, CGRectZero)) + { + POPSpringAnimation *frameAnimation = [POPSpringAnimation animationWithPropertyNamed:kPOPViewFrame]; + frameAnimation.fromValue = [NSValue valueWithCGRect:fromFrame]; + frameAnimation.toValue = [NSValue valueWithCGRect:toFrame]; + frameAnimation.springSpeed = 20; + frameAnimation.springBounciness = 1; + [_previewView pop_addAnimation:frameAnimation forKey:@"frame"]; + } + else + { + _previewView.frame = toFrame; + } + + _interfaceView.previewViewFrame = toFrame; + [_interfaceView layoutPreviewRelativeViews]; +} + +- (void)beginTransitionOutWithVelocity:(CGFloat)velocity +{ + _dismissing = true; + self.view.userInteractionEnabled = false; + + _focusControl.active = false; + + [UIView animateWithDuration:0.3f animations:^ + { + [_context setApplicationStatusBarAlpha:1.0f]; + }]; + + [self setInterfaceHidden:true animated:true]; + + [UIView animateWithDuration:0.25f animations:^ + { + _backgroundView.alpha = 0.0f; + }]; + + CGRect referenceFrame = CGRectZero; + if (self.beginTransitionOut != nil) + referenceFrame = self.beginTransitionOut(); + + __weak TGCameraController *weakSelf = self; + if (_standalone) + { + [self simpleTransitionOutWithVelocity:velocity completion:^ + { + __strong TGCameraController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [strongSelf dismiss]; + }]; + return; + } + + bool resetNeeded = _camera.isResetNeeded; + if (resetNeeded) + [_previewView beginResetTransitionAnimated:true]; + + [_camera resetSynchronous:false completion:^ + { + TGDispatchOnMainThread(^ + { + if (resetNeeded) + [_previewView endResetTransitionAnimated:true]; + }); + }]; + + [_previewView.layer removeAllAnimations]; + + if (!CGRectIsEmpty(referenceFrame)) + { + POPSpringAnimation *frameAnimation = [POPSpringAnimation animationWithPropertyNamed:kPOPViewFrame]; + frameAnimation.fromValue = [NSValue valueWithCGRect:_previewView.frame]; + frameAnimation.toValue = [NSValue valueWithCGRect:referenceFrame]; + frameAnimation.springSpeed = 20; + frameAnimation.springBounciness = 1; + frameAnimation.completionBlock = ^(__unused POPAnimation *animation, __unused BOOL finished) + { + __strong TGCameraController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + if (strongSelf.finishedTransitionOut != nil) + strongSelf.finishedTransitionOut(); + + [strongSelf dismiss]; + }; + [_previewView pop_addAnimation:frameAnimation forKey:@"frame"]; + } + else + { + if (self.finishedTransitionOut != nil) + self.finishedTransitionOut(); + + [self dismiss]; + } +} + +- (void)_dismissTransitionForResultController:(TGOverlayController *)resultController +{ + _finishedWithResult = true; + + [_context setApplicationStatusBarAlpha:1.0f]; + + self.view.hidden = true; + + [resultController.view.layer animatePositionFrom:resultController.view.layer.position to:CGPointMake(resultController.view.layer.position.x, resultController.view.layer.position.y + resultController.view.bounds.size.height) duration:0.3 timingFunction:kCAMediaTimingFunctionSpring removeOnCompletion:false completion:^(__unused bool finished) { + if (resultController.customDismissSelf) { + resultController.customDismissSelf(); + } else { + [resultController dismiss]; + } + [self dismiss]; + }]; + + return; + + [UIView animateWithDuration:0.3 delay:0.0f options:(7 << 16) animations:^ + { + resultController.view.frame = CGRectOffset(resultController.view.frame, 0, resultController.view.frame.size.height); + } completion:^(__unused BOOL finished) + { + if (resultController.customDismissSelf) { + resultController.customDismissSelf(); + } else { + [resultController dismiss]; + } + [self dismiss]; + }]; +} + +- (void)simpleTransitionOutWithVelocity:(CGFloat)velocity completion:(void (^)())completion +{ + self.view.userInteractionEnabled = false; + + const CGFloat minVelocity = 2000.0f; + if (ABS(velocity) < minVelocity) + velocity = (velocity < 0.0f ? -1.0f : 1.0f) * minVelocity; + CGFloat distance = (velocity < FLT_EPSILON ? -1.0f : 1.0f) * self.view.frame.size.height; + CGRect targetFrame = (CGRect){{_previewView.frame.origin.x, distance}, _previewView.frame.size}; + + [UIView animateWithDuration:ABS(distance / velocity) animations:^ + { + _previewView.frame = targetFrame; + } completion:^(__unused BOOL finished) + { + if (completion) + completion(); + }]; +} + +- (void)_updateDismissTransitionMovementWithDistance:(CGFloat)distance animated:(bool)animated +{ + CGRect originalFrame = [TGCameraController _cameraPreviewFrameForScreenSize:TGScreenSize() mode:_camera.cameraMode]; + CGRect frame = (CGRect){ { originalFrame.origin.x, originalFrame.origin.y + distance }, originalFrame.size }; + if (animated) + { + [UIView animateWithDuration:0.3 animations:^ + { + _previewView.frame = frame; + }]; + } + else + { + _previewView.frame = frame; + } +} + +- (void)_updateDismissTransitionWithProgress:(CGFloat)progress animated:(bool)animated +{ + CGFloat alpha = 1.0f - MAX(0.0f, MIN(1.0f, progress * 4.0f)); + CGFloat transitionProgress = MAX(0.0f, MIN(1.0f, progress * 2.0f)); + + if (transitionProgress > FLT_EPSILON) + { + [self setInterfaceHidden:true animated:true]; + _focusControl.active = false; + } + else if (animated) + { + [self setInterfaceHidden:false animated:true]; + _focusControl.active = true; + } + + if (animated) + { + [UIView animateWithDuration:0.3 animations:^ + { + _backgroundView.alpha = alpha; + }]; + } + else + { + _backgroundView.alpha = alpha; + } +} + +- (void)resizePreviewViewForCameraMode:(PGCameraMode)mode +{ + CGRect frame = [TGCameraController _cameraPreviewFrameForScreenSize:TGScreenSize() mode:mode]; + _interfaceView.previewViewFrame = frame; + [_interfaceView layoutPreviewRelativeViews]; + [_interfaceView updateForCameraModeChangeAfterResize]; + + [UIView animateWithDuration:0.3f delay:0.0f options:UIViewAnimationOptionCurveEaseInOut | UIViewAnimationOptionLayoutSubviews animations:^ + { + _previewView.frame = frame; + _overlayView.frame = frame; + } completion:nil]; +} + +- (void)handleDeviceOrientationChangedTo:(UIDeviceOrientation)deviceOrientation +{ + if (_camera.isRecordingVideo || _intent == TGCameraControllerPassportIdIntent) + return; + + UIInterfaceOrientation orientation = [TGCameraController _interfaceOrientationForDeviceOrientation:deviceOrientation]; + if ([_interfaceView isKindOfClass:[TGCameraMainPhoneView class]]) + { + [_interfaceView setInterfaceOrientation:orientation animated:true]; + } + else + { + if (orientation == UIInterfaceOrientationUnknown) + return; + + switch (deviceOrientation) + { + case UIDeviceOrientationPortrait: + { + _photoSwipeGestureRecognizer.direction = UISwipeGestureRecognizerDirectionUp; + _videoSwipeGestureRecognizer.direction = UISwipeGestureRecognizerDirectionDown; + } + break; + case UIDeviceOrientationPortraitUpsideDown: + { + _photoSwipeGestureRecognizer.direction = UISwipeGestureRecognizerDirectionDown; + _videoSwipeGestureRecognizer.direction = UISwipeGestureRecognizerDirectionUp; + } + break; + case UIDeviceOrientationLandscapeLeft: + { + _photoSwipeGestureRecognizer.direction = UISwipeGestureRecognizerDirectionRight; + _videoSwipeGestureRecognizer.direction = UISwipeGestureRecognizerDirectionLeft; + } + break; + case UIDeviceOrientationLandscapeRight: + { + _photoSwipeGestureRecognizer.direction = UISwipeGestureRecognizerDirectionLeft; + _videoSwipeGestureRecognizer.direction = UISwipeGestureRecognizerDirectionRight; + } + break; + + default: + break; + } + + [_interfaceView setInterfaceOrientation:orientation animated:false]; + CGSize referenceSize = [self referenceViewSizeForOrientation:orientation]; + if (referenceSize.width > referenceSize.height) + referenceSize = CGSizeMake(referenceSize.height, referenceSize.width); + + self.view.userInteractionEnabled = false; + [UIView animateWithDuration:0.5f delay:0.0f options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionLayoutSubviews animations:^ + { + _interfaceView.transform = CGAffineTransformMakeRotation(TGRotationForInterfaceOrientation(orientation)); + _interfaceView.frame = CGRectMake(0, 0, referenceSize.width, referenceSize.height); + [_interfaceView setNeedsLayout]; + } completion:^(BOOL finished) + { + if (finished) + self.view.userInteractionEnabled = true; + }]; + } + + [_focusControl setInterfaceOrientation:orientation animated:true]; +} + +#pragma mark - Gesture Recognizers + +- (CGFloat)dismissProgressForSwipeDistance:(CGFloat)distance +{ + return MAX(0.0f, MIN(1.0f, ABS(distance / 150.0f))); +} + +- (void)handleSwipe:(UISwipeGestureRecognizer *)gestureRecognizer +{ + PGCameraMode newMode = PGCameraModeUndefined; + if (gestureRecognizer == _photoSwipeGestureRecognizer) + { + newMode = PGCameraModePhoto; + } + else if (gestureRecognizer == _videoSwipeGestureRecognizer) + { + newMode = PGCameraModeVideo; + } + + if (newMode != PGCameraModeUndefined && _camera.cameraMode != newMode) + { + [_camera setCameraMode:newMode]; + [_interfaceView setCameraMode:newMode]; + } +} + +- (void)handlePan:(TGModernGalleryZoomableScrollViewSwipeGestureRecognizer *)gestureRecognizer +{ + switch (gestureRecognizer.state) + { + case UIGestureRecognizerStateChanged: + { + _dismissProgress = [self dismissProgressForSwipeDistance:[gestureRecognizer swipeDistance]]; + [self _updateDismissTransitionWithProgress:_dismissProgress animated:false]; + [self _updateDismissTransitionMovementWithDistance:[gestureRecognizer swipeDistance] animated:false]; + } + break; + + case UIGestureRecognizerStateEnded: + { + CGFloat swipeVelocity = [gestureRecognizer swipeVelocity]; + if (ABS(swipeVelocity) < TGCameraSwipeMinimumVelocity) + swipeVelocity = (swipeVelocity < 0.0f ? -1.0f : 1.0f) * TGCameraSwipeMinimumVelocity; + + __weak TGCameraController *weakSelf = self; + bool(^transitionOut)(CGFloat) = ^bool(CGFloat swipeVelocity) + { + __strong TGCameraController *strongSelf = weakSelf; + if (strongSelf == nil) + return false; + + [strongSelf beginTransitionOutWithVelocity:swipeVelocity]; + + return true; + }; + + if ((ABS(swipeVelocity) < TGCameraSwipeVelocityThreshold && ABS([gestureRecognizer swipeDistance]) < TGCameraSwipeDistanceThreshold) || !transitionOut(swipeVelocity)) + { + _dismissProgress = 0.0f; + [self _updateDismissTransitionWithProgress:0.0f animated:true]; + [self _updateDismissTransitionMovementWithDistance:0.0f animated:true]; + } + } + break; + + case UIGestureRecognizerStateCancelled: + { + _dismissProgress = 0.0f; + [self _updateDismissTransitionWithProgress:0.0f animated:true]; + [self _updateDismissTransitionMovementWithDistance:0.0f animated:true]; + } + break; + + default: + break; + } +} + +- (void)handlePinch:(UIPinchGestureRecognizer *)gestureRecognizer +{ + switch (gestureRecognizer.state) + { + case UIGestureRecognizerStateChanged: + { + CGFloat delta = (gestureRecognizer.scale - 1.0f) / 1.5f; + CGFloat value = MAX(0.0f, MIN(1.0f, _camera.zoomLevel + delta)); + + [_camera setZoomLevel:value]; + [_interfaceView setZoomLevel:value displayNeeded:true]; + + gestureRecognizer.scale = 1.0f; + } + break; + + case UIGestureRecognizerStateEnded: + case UIGestureRecognizerStateCancelled: + { + [_interfaceView zoomChangingEnded]; + } + break; + + default: + break; + } +} + +- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer +{ + if (gestureRecognizer == _panGestureRecognizer) + return !_camera.isRecordingVideo && _items.count == 0; + else if (gestureRecognizer == _photoSwipeGestureRecognizer || gestureRecognizer == _videoSwipeGestureRecognizer) + return _intent == TGCameraControllerGenericIntent && !_camera.isRecordingVideo; + else if (gestureRecognizer == _pinchGestureRecognizer) + return _camera.isZoomAvailable; + + return true; +} + ++ (CGRect)_cameraPreviewFrameForScreenSize:(CGSize)screenSize mode:(PGCameraMode)mode +{ + CGFloat widescreenWidth = MAX(screenSize.width, screenSize.height); + + if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPhone) + { + switch (mode) + { + case PGCameraModeVideo: + { + if (widescreenWidth == 896.0f) + return CGRectMake(0, 77, screenSize.width, screenSize.height - 77 - 83); + else if (widescreenWidth == 812.0f) + return CGRectMake(0, 77, screenSize.width, screenSize.height - 77 - 68); + else + return CGRectMake(0, 0, screenSize.width, screenSize.height); + } + break; + + case PGCameraModeSquare: + case PGCameraModeClip: + { + CGRect rect = [self _cameraPreviewFrameForScreenSize:screenSize mode:PGCameraModePhoto]; + CGFloat topOffset = CGRectGetMidY(rect) - rect.size.width / 2; + + if (widescreenWidth - 480.0f < FLT_EPSILON) + topOffset = 40.0f; + + return CGRectMake(0, floor(topOffset), rect.size.width, rect.size.width); + } + break; + + default: + { + if (widescreenWidth == 896.0f) + return CGRectMake(0, 121, screenSize.width, screenSize.height - 121 - 223); + else if (widescreenWidth == 812.0f) + return CGRectMake(0, 121, screenSize.width, screenSize.height - 121 - 191); + else if (widescreenWidth >= 736.0f - FLT_EPSILON) + return CGRectMake(0, 44, screenSize.width, screenSize.height - 50 - 136); + else if (widescreenWidth >= 667.0f - FLT_EPSILON) + return CGRectMake(0, 44, screenSize.width, screenSize.height - 44 - 123); + else if (widescreenWidth >= 568.0f - FLT_EPSILON) + return CGRectMake(0, 40, screenSize.width, screenSize.height - 40 - 101); + else + return CGRectMake(0, 0, screenSize.width, screenSize.height); + } + break; + } + } + else + { + if (mode == PGCameraModeSquare) + return CGRectMake(0, (screenSize.height - screenSize.width) / 2, screenSize.width, screenSize.width); + + return CGRectMake(0, 0, screenSize.width, screenSize.height); + } +} + ++ (UIInterfaceOrientation)_interfaceOrientationForDeviceOrientation:(UIDeviceOrientation)orientation +{ + switch (orientation) + { + case UIDeviceOrientationPortrait: + return UIInterfaceOrientationPortrait; + + case UIDeviceOrientationPortraitUpsideDown: + return UIInterfaceOrientationPortraitUpsideDown; + + case UIDeviceOrientationLandscapeLeft: + return UIInterfaceOrientationLandscapeRight; + + case UIDeviceOrientationLandscapeRight: + return UIInterfaceOrientationLandscapeLeft; + + default: + return UIInterfaceOrientationUnknown; + } +} + ++ (bool)useLegacyCamera +{ + return iosMajorVersion() < 7 || [UIDevice currentDevice].platformType == UIDevice4iPhone || [UIDevice currentDevice].platformType == UIDevice4GiPod; +} + ++ (NSArray *)resultSignalsForSelectionContext:(TGMediaSelectionContext *)selectionContext editingContext:(TGMediaEditingContext *)editingContext currentItem:(id)currentItem storeAssets:(bool)storeAssets saveEditedPhotos:(bool)saveEditedPhotos descriptionGenerator:(id (^)(id, NSString *, NSArray *, NSString *))descriptionGenerator +{ + NSMutableArray *signals = [[NSMutableArray alloc] init]; + NSMutableArray *selectedItems = selectionContext.selectedItems != nil ? [selectionContext.selectedItems mutableCopy] : [[NSMutableArray alloc] init]; + if (selectedItems.count == 0 && currentItem != nil) + [selectedItems addObject:currentItem]; + + if (storeAssets) + { + NSMutableArray *fullSizeSignals = [[NSMutableArray alloc] init]; + for (id item in selectedItems) + { + if ([editingContext timerForItem:item] == nil) + { + SSignal *saveMedia = [SSignal defer:^SSignal * + { + if ([item isKindOfClass:[TGCameraCapturedPhoto class]]) + { + TGCameraCapturedPhoto *photo = (TGCameraCapturedPhoto *)item; + return [SSignal single:@{@"type": @"photo", @"url": photo.url}]; + } + else if ([item isKindOfClass:[TGCameraCapturedVideo class]]) + { + TGCameraCapturedVideo *video = (TGCameraCapturedVideo *)item; + return [SSignal single:@{@"type": @"video", @"url": video.avAsset.URL}]; + } + + return [SSignal complete]; + }]; + + [fullSizeSignals addObject:saveMedia]; + + if (saveEditedPhotos) + { + [fullSizeSignals addObject:[[[editingContext fullSizeImageUrlForItem:item] filter:^bool(id result) + { + return [result isKindOfClass:[NSURL class]]; + }] mapToSignal:^SSignal *(NSURL *url) + { + return [SSignal single:@{@"type": @"photo", @"url": url}]; + }]]; + } + } + } + + SSignal *combinedSignal = nil; + SQueue *queue = [SQueue concurrentDefaultQueue]; + + for (SSignal *signal in fullSizeSignals) + { + if (combinedSignal == nil) + combinedSignal = [signal startOn:queue]; + else + combinedSignal = [[combinedSignal then:signal] startOn:queue]; + } + + [[[combinedSignal deliverOn:[SQueue mainQueue]] mapToSignal:^SSignal *(NSDictionary *desc) + { + if ([desc[@"type"] isEqualToString:@"photo"]) + { + return [[TGMediaAssetsLibrary sharedLibrary] saveAssetWithImageAtUrl:desc[@"url"]]; + } + else if ([desc[@"type"] isEqualToString:@"video"]) + { + return [[TGMediaAssetsLibrary sharedLibrary] saveAssetWithVideoAtUrl:desc[@"url"]]; + } + else + { + return [SSignal complete]; + } + }] startWithNext:nil]; + } + + static dispatch_once_t onceToken; + static UIImage *blankImage; + dispatch_once(&onceToken, ^ + { + UIGraphicsBeginImageContextWithOptions(CGSizeMake(1, 1), true, 0.0f); + + CGContextRef context = UIGraphicsGetCurrentContext(); + CGContextSetFillColorWithColor(context, [UIColor blackColor].CGColor); + CGContextFillRect(context, CGRectMake(0, 0, 1, 1)); + + blankImage = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + }); + + SSignal *(^inlineThumbnailSignal)(id) = ^SSignal *(id item) + { + return [item thumbnailImageSignal]; + }; + + NSNumber *groupedId; + NSInteger i = 0; + if (selectionContext.grouping && selectedItems.count > 1) + groupedId = @([TGCameraController generateGroupedId]); + + bool hasAnyTimers = false; + if (editingContext != nil) + { + for (id item in selectedItems) + { + if ([editingContext timerForItem:item] != nil) + { + hasAnyTimers = true; + break; + } + } + } + + for (id asset in selectedItems) + { + if ([asset isKindOfClass:[TGCameraCapturedPhoto class]]) + { + NSString *caption = [editingContext captionForItem:asset]; + NSArray *entities = [editingContext entitiesForItem:asset]; + id adjustments = [editingContext adjustmentsForItem:asset]; + NSNumber *timer = [editingContext timerForItem:asset]; + + SSignal *inlineSignal = [[asset screenImageSignal:0.0] map:^id(UIImage *originalImage) + { + NSMutableDictionary *dict = [[NSMutableDictionary alloc] init]; + dict[@"type"] = @"editedPhoto"; + dict[@"image"] = originalImage; + + if (timer != nil) + dict[@"timer"] = timer; + else if (groupedId != nil && !hasAnyTimers) + dict[@"groupedId"] = groupedId; + + id generatedItem = descriptionGenerator(dict, caption, entities, nil); + return generatedItem; + }]; + + SSignal *assetSignal = inlineSignal; + SSignal *imageSignal = assetSignal; + if (editingContext != nil) + { + imageSignal = [[[[[editingContext imageSignalForItem:asset withUpdates:true] filter:^bool(id result) + { + return result == nil || ([result isKindOfClass:[UIImage class]] && !((UIImage *)result).degraded); + }] take:1] mapToSignal:^SSignal *(id result) + { + if (result == nil) + { + return [SSignal fail:nil]; + } + else if ([result isKindOfClass:[UIImage class]]) + { + UIImage *image = (UIImage *)result; + image.edited = true; + return [SSignal single:image]; + } + + return [SSignal complete]; + }] onCompletion:^ + { + __strong TGMediaEditingContext *strongEditingContext = editingContext; + [strongEditingContext description]; + }]; + } + + [signals addObject:[[imageSignal map:^NSDictionary *(UIImage *image) + { + NSMutableDictionary *dict = [[NSMutableDictionary alloc] init]; + dict[@"type"] = @"editedPhoto"; + dict[@"image"] = image; + + if (adjustments.paintingData.stickers.count > 0) + dict[@"stickers"] = adjustments.paintingData.stickers; + + if (timer != nil) + dict[@"timer"] = timer; + else if (groupedId != nil && !hasAnyTimers) + dict[@"groupedId"] = groupedId; + + id generatedItem = descriptionGenerator(dict, caption, entities, nil); + return generatedItem; + }] catch:^SSignal *(__unused id error) + { + return inlineSignal; + }]]; + + i++; + } + else if ([asset isKindOfClass:[TGCameraCapturedVideo class]]) + { + TGCameraCapturedVideo *video = (TGCameraCapturedVideo *)asset; + + TGVideoEditAdjustments *adjustments = (TGVideoEditAdjustments *)[editingContext adjustmentsForItem:asset]; + NSString *caption = [editingContext captionForItem:asset]; + NSArray *entities = [editingContext entitiesForItem:asset]; + NSNumber *timer = [editingContext timerForItem:asset]; + + UIImage *(^cropVideoThumbnail)(UIImage *, CGSize, CGSize, bool) = ^UIImage *(UIImage *image, CGSize targetSize, CGSize sourceSize, bool resize) + { + if ([adjustments cropAppliedForAvatar:false] || adjustments.hasPainting) + { + CGRect scaledCropRect = CGRectMake(adjustments.cropRect.origin.x * image.size.width / adjustments.originalSize.width, adjustments.cropRect.origin.y * image.size.height / adjustments.originalSize.height, adjustments.cropRect.size.width * image.size.width / adjustments.originalSize.width, adjustments.cropRect.size.height * image.size.height / adjustments.originalSize.height); + return TGPhotoEditorCrop(image, adjustments.paintingData.image, adjustments.cropOrientation, 0, scaledCropRect, adjustments.cropMirrored, targetSize, sourceSize, resize); + } + + return image; + }; + + CGSize imageSize = TGFillSize(asset.originalSize, CGSizeMake(384, 384)); + SSignal *trimmedVideoThumbnailSignal = [[TGMediaAssetImageSignals videoThumbnailForAVAsset:video.avAsset size:imageSize timestamp:CMTimeMakeWithSeconds(adjustments.trimStartValue, NSEC_PER_SEC)] map:^UIImage *(UIImage *image) + { + return cropVideoThumbnail(image, TGScaleToFill(asset.originalSize, CGSizeMake(256, 256)), asset.originalSize, true); + }]; + + SSignal *videoThumbnailSignal = [inlineThumbnailSignal(asset) map:^UIImage *(UIImage *image) + { + return cropVideoThumbnail(image, image.size, image.size, false); + }]; + + SSignal *thumbnailSignal = adjustments.trimStartValue > FLT_EPSILON ? trimmedVideoThumbnailSignal : videoThumbnailSignal; + + TGMediaVideoConversionPreset preset = [TGMediaVideoConverter presetFromAdjustments:adjustments]; + CGSize dimensions = [TGMediaVideoConverter dimensionsFor:asset.originalSize adjustments:adjustments preset:preset]; + NSTimeInterval duration = adjustments.trimApplied ? (adjustments.trimEndValue - adjustments.trimStartValue) : video.videoDuration; + + [signals addObject:[thumbnailSignal map:^id(UIImage *image) + { + NSMutableDictionary *dict = [[NSMutableDictionary alloc] init]; + dict[@"type"] = @"cameraVideo"; + dict[@"url"] = video.avAsset.URL; + dict[@"previewImage"] = image; + dict[@"adjustments"] = adjustments; + dict[@"dimensions"] = [NSValue valueWithCGSize:dimensions]; + dict[@"duration"] = @(duration); + + if (adjustments.paintingData.stickers.count > 0) + dict[@"stickers"] = adjustments.paintingData.stickers; + if (timer != nil) + dict[@"timer"] = timer; + else if (groupedId != nil && !hasAnyTimers) + dict[@"groupedId"] = groupedId; + + id generatedItem = descriptionGenerator(dict, caption, entities, nil); + return generatedItem; + }]]; + + i++; + } + + if (groupedId != nil && i == 10) + { + i = 0; + groupedId = @([TGCameraController generateGroupedId]); + } + } + return signals; +} + ++ (int64_t)generateGroupedId +{ + int64_t value; + arc4random_buf(&value, sizeof(int64_t)); + return value; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGCameraFlashActiveView.h b/submodules/LegacyComponents/LegacyComponents/TGCameraFlashActiveView.h new file mode 100644 index 0000000000..516ab21996 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGCameraFlashActiveView.h @@ -0,0 +1,7 @@ +#import + +@interface TGCameraFlashActiveView : UIView + +- (void)setActive:(bool)active animated:(bool)animated; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGCameraFlashActiveView.m b/submodules/LegacyComponents/LegacyComponents/TGCameraFlashActiveView.m new file mode 100644 index 0000000000..71502ab3fd --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGCameraFlashActiveView.m @@ -0,0 +1,66 @@ +#import "TGCameraFlashActiveView.h" + +#import "LegacyComponentsInternal.h" + +#import "TGCameraInterfaceAssets.h" + +@interface TGCameraFlashActiveView () +{ + UIView *_backgroundView; + UIImageView *_iconView; +} +@end + +@implementation TGCameraFlashActiveView + +- (instancetype)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + if (self != nil) + { + self.userInteractionEnabled = false; + + _backgroundView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, frame.size.width, frame.size.height)]; + _backgroundView.backgroundColor = [TGCameraInterfaceAssets accentColor]; + _backgroundView.layer.cornerRadius = 2.0f; + [self addSubview:_backgroundView]; + + _iconView = [[UIImageView alloc] initWithFrame:CGRectMake((frame.size.width - 8) / 2, (frame.size.height - 13) / 2, 8, 13)]; + _iconView.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleBottomMargin; + _iconView.image = TGComponentsImageNamed(@"CameraFlashActive"); + [_backgroundView addSubview:_iconView]; + + [self setActive:false animated:false]; + } + return self; +} + +- (void)setHidden:(bool)hidden animated:(bool)animated +{ + if (animated) + { + _backgroundView.hidden = false; + + [UIView animateWithDuration:0.25f + animations:^ + { + _backgroundView.alpha = hidden ? 0.0f : 1.0f; + } completion:^(BOOL finished) + { + if (finished) + _backgroundView.hidden = hidden; + }]; + } + else + { + _backgroundView.alpha = hidden ? 0.0f : 1.0f; + _backgroundView.hidden = hidden; + } +} + +- (void)setActive:(bool)active animated:(bool)animated +{ + [self setHidden:!active animated:animated]; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGCameraFlashControl.h b/submodules/LegacyComponents/LegacyComponents/TGCameraFlashControl.h new file mode 100644 index 0000000000..b914fd8eef --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGCameraFlashControl.h @@ -0,0 +1,20 @@ +#import +#import + +@interface TGCameraFlashControl : UIControl + +@property (nonatomic, assign) PGCameraFlashMode mode; +@property (nonatomic, assign) UIInterfaceOrientation interfaceOrientation; + +@property (nonatomic, copy) void(^becameActive)(void); +@property (nonatomic, copy) void(^modeChanged)(PGCameraFlashMode mode); + +- (void)setFlashUnavailable:(bool)unavailable; + +- (void)setHidden:(bool)hidden animated:(bool)animated; + +- (void)dismissAnimated:(bool)animated; + +@end + +extern const CGFloat TGCameraFlashControlHeight; diff --git a/submodules/LegacyComponents/LegacyComponents/TGCameraFlashControl.m b/submodules/LegacyComponents/LegacyComponents/TGCameraFlashControl.m new file mode 100644 index 0000000000..4962d217f6 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGCameraFlashControl.m @@ -0,0 +1,613 @@ +#import "TGCameraFlashControl.h" + +#import "LegacyComponentsInternal.h" + +#import "UIControl+HitTestEdgeInsets.h" + +#import "TGCameraInterfaceAssets.h" +#import + +const CGFloat TGCameraFlashControlHeight = 44.0f; + +@interface TGCameraFlashControl () +{ + UIButton *_flashIconView; + UIButton *_autoButton; + UIButton *_onButton; + UIButton *_offButton; + + bool _active; +} +@end + +@implementation TGCameraFlashControl + +- (instancetype)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + if (self != nil) + { + self.hitTestEdgeInsets = UIEdgeInsetsMake(-10, -10, -10, -10); + + _flashIconView = [[UIButton alloc] initWithFrame:CGRectMake(0, 0, 34, 44)]; + _flashIconView.adjustsImageWhenHighlighted = false; + _flashIconView.contentMode = UIViewContentModeCenter; + _flashIconView.exclusiveTouch = true; + _flashIconView.hitTestEdgeInsets = UIEdgeInsetsMake(0, -10, 0, -10); + _flashIconView.tag = -1; + [_flashIconView setImage:TGComponentsImageNamed(@"CameraFlashButton") forState:UIControlStateNormal]; + [_flashIconView addTarget:self action:@selector(buttonPressed:) forControlEvents:UIControlEventTouchUpInside]; + [self addSubview:_flashIconView]; + + static UIImage *highlightedIconImage = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^ + { + UIImage *image = TGComponentsImageNamed(@"CameraFlashButton"); + UIGraphicsBeginImageContextWithOptions(image.size, false, 0.0f); + CGContextRef context = UIGraphicsGetCurrentContext(); + [image drawInRect:CGRectMake(0, 0, image.size.width, image.size.height)]; + CGContextSetBlendMode (context, kCGBlendModeSourceAtop); + CGContextSetFillColorWithColor(context, [TGCameraInterfaceAssets accentColor].CGColor); + CGContextFillRect(context, CGRectMake(0, 0, image.size.width, image.size.height)); + + highlightedIconImage = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + }); + [_flashIconView setImage:highlightedIconImage forState:UIControlStateSelected]; + [_flashIconView setImage:highlightedIconImage forState:UIControlStateHighlighted | UIControlStateSelected]; + + _autoButton = [[UIButton alloc] init]; + _autoButton.backgroundColor = [UIColor clearColor]; + _autoButton.contentHorizontalAlignment = UIControlContentHorizontalAlignmentLeft; + _autoButton.exclusiveTouch = true; + _autoButton.hitTestEdgeInsets = UIEdgeInsetsMake(-10, -15, -10, -15); + _autoButton.tag = PGCameraFlashModeAuto; + _autoButton.titleLabel.font = [TGCameraInterfaceAssets normalFontOfSize:13]; + [_autoButton setAttributedTitle:[[NSAttributedString alloc] initWithString:TGLocalized(@"Camera.FlashAuto") attributes:@{ NSForegroundColorAttributeName: [TGCameraInterfaceAssets normalColor], NSKernAttributeName: @2 }] forState:UIControlStateNormal]; + [_autoButton setAttributedTitle:[[NSAttributedString alloc] initWithString:TGLocalized(@"Camera.FlashAuto") attributes:@{ NSForegroundColorAttributeName: [TGCameraInterfaceAssets accentColor], NSKernAttributeName: @2 }] forState:UIControlStateSelected]; + [_autoButton setAttributedTitle:[_autoButton attributedTitleForState:UIControlStateSelected] forState:UIControlStateHighlighted | UIControlStateSelected]; + [_autoButton addTarget:self action:@selector(buttonPressed:) forControlEvents:UIControlEventTouchUpInside]; + [_autoButton sizeToFit]; + _autoButton.frame = (CGRect){ CGPointZero, [TGCameraFlashControl _sizeForModeButtonWithTitle:[_autoButton attributedTitleForState:UIControlStateNormal]] }; + [self addSubview:_autoButton]; + + _onButton = [[UIButton alloc] init]; + _onButton.backgroundColor = [UIColor clearColor]; + _onButton.contentHorizontalAlignment = UIControlContentHorizontalAlignmentLeft; + _onButton.exclusiveTouch = true; + _onButton.hitTestEdgeInsets = UIEdgeInsetsMake(-10, -15, -10, -15); + _onButton.tag = PGCameraFlashModeOn; + _onButton.titleLabel.font = [TGCameraInterfaceAssets normalFontOfSize:13]; + [_onButton setAttributedTitle:[[NSAttributedString alloc] initWithString:TGLocalized(@"Camera.FlashOn") attributes:@{ NSForegroundColorAttributeName: [TGCameraInterfaceAssets normalColor], NSKernAttributeName: @2 }] forState:UIControlStateNormal]; + [_onButton setAttributedTitle:[[NSAttributedString alloc] initWithString:TGLocalized(@"Camera.FlashOn") attributes:@{ NSForegroundColorAttributeName: [TGCameraInterfaceAssets accentColor], NSKernAttributeName: @2 }] forState:UIControlStateSelected]; + [_onButton setAttributedTitle:[_onButton attributedTitleForState:UIControlStateSelected] forState:UIControlStateHighlighted | UIControlStateSelected]; + [_onButton addTarget:self action:@selector(buttonPressed:) forControlEvents:UIControlEventTouchUpInside]; + [_onButton sizeToFit]; + _onButton.frame = (CGRect){ CGPointZero, [TGCameraFlashControl _sizeForModeButtonWithTitle:[_onButton attributedTitleForState:UIControlStateNormal]] }; + [self addSubview:_onButton]; + + _offButton = [[UIButton alloc] init]; + _offButton.backgroundColor = [UIColor clearColor]; + _offButton.contentHorizontalAlignment = UIControlContentHorizontalAlignmentLeft; + _offButton.exclusiveTouch = true; + _offButton.hitTestEdgeInsets = UIEdgeInsetsMake(-10, -15, -10, -15); + _offButton.tag = PGCameraFlashModeOff; + _offButton.titleLabel.font = [TGCameraInterfaceAssets normalFontOfSize:13]; + [_offButton setAttributedTitle:[[NSAttributedString alloc] initWithString:TGLocalized(@"Camera.FlashOff") attributes:@{ NSForegroundColorAttributeName: [TGCameraInterfaceAssets normalColor], NSKernAttributeName: @2 }] forState:UIControlStateNormal]; + [_offButton setAttributedTitle:[[NSAttributedString alloc] initWithString:TGLocalized(@"Camera.FlashOff") attributes:@{ NSForegroundColorAttributeName: [TGCameraInterfaceAssets accentColor], NSKernAttributeName: @2 }] forState:UIControlStateSelected]; + [_offButton setAttributedTitle:[_offButton attributedTitleForState:UIControlStateSelected] forState:UIControlStateHighlighted | UIControlStateSelected]; + [_offButton addTarget:self action:@selector(buttonPressed:) forControlEvents:UIControlEventTouchUpInside]; + [_offButton sizeToFit]; + _offButton.frame = (CGRect){ CGPointZero, [TGCameraFlashControl _sizeForModeButtonWithTitle:[_offButton attributedTitleForState:UIControlStateNormal]] }; + [self addSubview:_offButton]; + + [UIView performWithoutAnimation:^ + { + self.mode = PGCameraFlashModeOff; + [self setActive:false animated:false]; + }]; + } + return self; +} + +- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event +{ + UIView *view = [super hitTest:point withEvent:event]; + + if ([view isKindOfClass:[UIButton class]]) + return view; + + return nil; +} + +- (void)buttonPressed:(UIButton *)sender +{ + if (!_active) + { + [self setActive:true animated:true]; + } + else + { + if (sender != _flashIconView) + self.mode = (int)sender.tag; + else + self.mode = _mode; + + if (self.modeChanged != nil) + self.modeChanged(self.mode); + } +} + +- (void)setFlashUnavailable:(bool)unavailable +{ + self.userInteractionEnabled = !unavailable; + [self setActive:false animated:false]; + + +} + +- (void)setActive:(bool)active animated:(bool)animated +{ + _active = active; + + if (animated) + { + self.userInteractionEnabled = false; + + if (active) + { + UIView *animatedView = nil; + UIView *snapshotView = nil; + CGRect targetFrame = CGRectZero; + + if (self.mode != PGCameraFlashModeAuto) + { + _autoButton.frame = [self _autoButtonFrameForInterfaceOrientation:_interfaceOrientation]; + _autoButton.alpha = 0.0f; + _autoButton.hidden = false; + } + else + { + animatedView = _autoButton; + targetFrame = [self _autoButtonFrameForInterfaceOrientation:_interfaceOrientation]; + snapshotView = [animatedView snapshotViewAfterScreenUpdates:false]; + } + _autoButton.selected = (self.mode == PGCameraFlashModeAuto); + + if (self.mode != PGCameraFlashModeOn) + { + _onButton.frame = [self _onButtonFrameForInterfaceOrientation:_interfaceOrientation]; + _onButton.alpha = 0.0f; + _onButton.hidden = false; + } + else + { + animatedView = _onButton; + targetFrame = [self _onButtonFrameForInterfaceOrientation:_interfaceOrientation]; + } + _onButton.selected = (self.mode == PGCameraFlashModeOn); + + if (self.mode != PGCameraFlashModeOff) + { + _offButton.frame = [self _offButtonFrameForInterfaceOrientation:_interfaceOrientation]; + _offButton.alpha = 0.0f; + _offButton.hidden = false; + } + else + { + animatedView = _offButton; + targetFrame = [self _offButtonFrameForInterfaceOrientation:_interfaceOrientation]; + snapshotView = [animatedView snapshotViewAfterScreenUpdates:false]; + } + _offButton.selected = (self.mode == PGCameraFlashModeOff); + + if (snapshotView != nil) + { + snapshotView.frame = animatedView.frame; + [animatedView.superview insertSubview:snapshotView belowSubview:animatedView]; + animatedView.alpha = 0.0f; + } + + UIView *iconSnapshotView = nil; + if (_flashIconView.selected) + { + iconSnapshotView = [_flashIconView snapshotViewAfterScreenUpdates:false]; + iconSnapshotView.frame = _flashIconView.frame; + [_flashIconView.superview insertSubview:iconSnapshotView belowSubview:_flashIconView]; + _flashIconView.selected = false; + _flashIconView.alpha = 0.0f; + } + + [UIView animateWithDuration:0.25f delay:0.0f options:UIViewAnimationOptionCurveEaseInOut animations:^ + { + _flashIconView.alpha = 1.0f; + _flashIconView.frame = [self _flashIconFrameForActive:active interfaceOrientation:_interfaceOrientation]; + iconSnapshotView.frame = _flashIconView.frame; + + _autoButton.alpha = 1.0f; + _onButton.alpha = 1.0f; + _offButton.alpha = 1.0f; + + animatedView.alpha = 1.0f; + animatedView.frame = targetFrame; + snapshotView.frame = targetFrame; + } completion:^(BOOL finished) + { + [snapshotView removeFromSuperview]; + [iconSnapshotView removeFromSuperview]; + if (finished) + self.userInteractionEnabled = true; + }]; + } + else + { + UIView *animatedView = nil; + UIView *snapshotView = nil; + UIView *iconSnapshotView = nil; + + switch (self.mode) + { + case PGCameraFlashModeAuto: + { + animatedView = _autoButton; + snapshotView = [animatedView snapshotViewAfterScreenUpdates:false]; + _autoButton.selected = false; + } + break; + + case PGCameraFlashModeOn: + { + animatedView = _onButton; + if (!_onButton.selected) + { + snapshotView = [animatedView snapshotViewAfterScreenUpdates:false]; + _onButton.selected = true; + } + + iconSnapshotView = [_flashIconView snapshotViewAfterScreenUpdates:false]; + iconSnapshotView.frame = _flashIconView.frame; + [_flashIconView.superview insertSubview:iconSnapshotView belowSubview:_flashIconView]; + _flashIconView.selected = true; + _flashIconView.alpha = 0.0f; + } + break; + + case PGCameraFlashModeOff: + { + animatedView = _offButton; + snapshotView = [animatedView snapshotViewAfterScreenUpdates:false]; + _offButton.selected = false; + } + break; + + default: + break; + } + + if (snapshotView != nil) + { + snapshotView.frame = animatedView.frame; + [animatedView.superview insertSubview:snapshotView belowSubview:animatedView]; + animatedView.alpha = 0.0f; + } + + [UIView animateWithDuration:0.25f delay:0.0f options:UIViewAnimationOptionCurveEaseInOut animations:^ + { + _flashIconView.alpha = 1.0f; + _flashIconView.frame = [self _flashIconFrameForActive:active interfaceOrientation:_interfaceOrientation]; + iconSnapshotView.frame = _flashIconView.frame; + + if (self.mode != PGCameraFlashModeAuto) + _autoButton.alpha = 0.0f; + + if (self.mode != PGCameraFlashModeOn) + _onButton.alpha = 0.0f; + + if (self.mode != PGCameraFlashModeOff) + _offButton.alpha = 0.0f; + + animatedView.alpha = 1.0f; + animatedView.frame = [self _selectedButtonFrameForSize:animatedView.frame.size interfaceOrientation:_interfaceOrientation]; + snapshotView.frame = animatedView.frame; + } completion:^(BOOL finished) + { + [snapshotView removeFromSuperview]; + [iconSnapshotView removeFromSuperview]; + if (finished) + { + self.userInteractionEnabled = true; + + if (self.mode != PGCameraFlashModeAuto) + _autoButton.hidden = true; + + if (self.mode != PGCameraFlashModeOn) + _onButton.hidden = true; + + if (self.mode != PGCameraFlashModeOff) + _offButton.hidden = true; + } + }]; + } + } + else + { + _flashIconView.frame = [self _flashIconFrameForActive:active interfaceOrientation:_interfaceOrientation]; + + if (active) + { + _flashIconView.selected = false; + + _autoButton.frame = [self _autoButtonFrameForInterfaceOrientation:_interfaceOrientation]; + _autoButton.alpha = 1.0f; + _autoButton.hidden = false; + _autoButton.selected = (self.mode == PGCameraFlashModeAuto); + + _onButton.frame = [self _onButtonFrameForInterfaceOrientation:_interfaceOrientation]; + _onButton.alpha = 1.0f; + _onButton.hidden = false; + _onButton.selected = (self.mode == PGCameraFlashModeOn); + + _offButton.frame = [self _offButtonFrameForInterfaceOrientation:_interfaceOrientation]; + _offButton.alpha = 1.0f; + _offButton.hidden = false; + _offButton.selected = (self.mode == PGCameraFlashModeOff); + } + else + { + switch (self.mode) + { + case PGCameraFlashModeOff: + { + _flashIconView.selected = false; + + _autoButton.alpha = 0.0f; + _autoButton.hidden = true; + _autoButton.selected = false; + + _onButton.alpha = 0.0f; + _onButton.hidden = true; + _onButton.selected = false; + + _offButton.frame = [self _selectedButtonFrameForSize:_offButton.frame.size interfaceOrientation:_interfaceOrientation]; + _offButton.alpha = 1.0f; + _offButton.hidden = false; + _offButton.selected = false; + } + break; + + case PGCameraFlashModeOn: + { + _flashIconView.selected = true; + + _autoButton.alpha = 0.0f; + _autoButton.hidden = true; + _autoButton.selected = false; + + _onButton.frame = [self _selectedButtonFrameForSize:_onButton.frame.size interfaceOrientation:_interfaceOrientation]; + _onButton.alpha = 1.0f; + _onButton.hidden = false; + _onButton.selected = true; + + _offButton.alpha = 0.0f; + _offButton.hidden = true; + _offButton.selected = false; + } + break; + + case PGCameraFlashModeAuto: + { + _flashIconView.selected = false; + + _autoButton.frame = [self _selectedButtonFrameForSize:_autoButton.frame.size interfaceOrientation:_interfaceOrientation]; + _autoButton.alpha = 1.0f; + _autoButton.hidden = false; + _autoButton.selected = false; + + _onButton.alpha = 0.0f; + _onButton.hidden = true; + _onButton.selected = false; + + _offButton.alpha = 0.0f; + _offButton.hidden = true; + _offButton.selected = false; + } + break; + + default: + break; + } + } + } + + if (active && self.becameActive != nil) + self.becameActive(); +} + +- (void)setMode:(PGCameraFlashMode)mode +{ + _mode = mode; + + [self setActive:false animated:_active]; +} + +- (void)dismissAnimated:(bool)animated +{ + if (animated && _active) + [self setActive:false animated:animated]; + else + [self setActive:false animated:false]; +} + +- (void)setHidden:(BOOL)hidden +{ + self.alpha = hidden ? 0.0f : 1.0f; + super.hidden = hidden; + + [self setActive:false animated:false]; +} + +- (void)setHidden:(bool)hidden animated:(bool)animated +{ + if (animated) + { + super.hidden = false; + self.userInteractionEnabled = false; + + [UIView animateWithDuration:0.25f + animations:^ + { + self.alpha = hidden ? 0.0f : 1.0f; + } completion:^(BOOL finished) + { + self.userInteractionEnabled = true; + + if (finished) + self.hidden = hidden; + + [self setActive:false animated:false]; + }]; + } + else + { + self.alpha = hidden ? 0.0f : 1.0f; + super.hidden = hidden; + + [self setActive:false animated:false]; + } +} + +- (void)setInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation +{ + _interfaceOrientation = interfaceOrientation; + + [self setActive:false animated:false]; +} + +- (CGRect)_flashIconFrameForActive:(bool)active interfaceOrientation:(UIInterfaceOrientation)interfaceOrientation +{ + CGPoint origin = CGPointZero; + CGSize size = self.frame.size; + if (UIInterfaceOrientationIsLandscape(interfaceOrientation)) + size = CGSizeMake(size.height, size.width); + + switch (interfaceOrientation) + { + case UIInterfaceOrientationLandscapeLeft: + { + if (active) + origin = CGPointMake(size.width - _flashIconView.frame.size.width - 5, (size.height - _flashIconView.frame.size.height) / 2); + else + origin = CGPointMake(size.width - _flashIconView.frame.size.width - 5, (size.height - _flashIconView.frame.size.height) / 2 - 9); + } + break; + case UIInterfaceOrientationLandscapeRight: + { + if (active) + origin = CGPointMake(5, (size.height - _flashIconView.frame.size.height) / 2); + else + origin = CGPointMake(5, (size.height - _flashIconView.frame.size.height) / 2 - 9); + } + break; + + case UIInterfaceOrientationPortraitUpsideDown: + { + if (active) + { + origin = CGPointMake(0, 0); + } + else + { + CGFloat maxWidth = MAX(MAX(_offButton.frame.size.width, _onButton.frame.size.width), _autoButton.frame.size.width); + origin = CGPointMake(size.width - _flashIconView.frame.size.width - maxWidth, 0); + } + } + break; + + default: + { + origin = CGPointZero; + } + break; + } + + return CGRectMake(origin.x, origin.y, _flashIconView.frame.size.width, _flashIconView.frame.size.height); +} + +- (CGRect)_selectedButtonFrameForSize:(CGSize)buttonSize interfaceOrientation:(UIInterfaceOrientation)interfaceOrientation +{ + CGPoint origin = CGPointZero; + CGSize size = self.frame.size; + if (UIInterfaceOrientationIsLandscape(interfaceOrientation)) + size = CGSizeMake(size.height, size.width); + + switch (interfaceOrientation) + { + case UIInterfaceOrientationLandscapeLeft: + case UIInterfaceOrientationLandscapeRight: + { + origin = CGPointMake(CGRectGetMidX(_flashIconView.frame) - buttonSize.width / 2, 21); + } + break; + + case UIInterfaceOrientationPortraitUpsideDown: + { + CGRect iconFrame = [self _flashIconFrameForActive:false interfaceOrientation:interfaceOrientation]; + origin = CGPointMake(iconFrame.origin.x + iconFrame.size.width - 3, (size.height - buttonSize.height) / 2); + } + break; + + default: + { + origin = CGPointMake(_flashIconView.frame.size.width - 5, + (size.height - buttonSize.height) / 2); + } + break; + } + + return CGRectMake(origin.x, origin.y, buttonSize.width, buttonSize.height); +} + +- (CGRect)_autoButtonFrameForInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation +{ + CGSize size = self.frame.size; + if (UIInterfaceOrientationIsLandscape(interfaceOrientation)) + size = CGSizeMake(size.height, size.width); + + return CGRectMake(size.width / 4 - _autoButton.frame.size.width / 2, + (size.height - _autoButton.frame.size.height) / 2, + _autoButton.frame.size.width, _autoButton.frame.size.height); +} + +- (CGRect)_onButtonFrameForInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation +{ + CGSize size = self.frame.size; + if (UIInterfaceOrientationIsLandscape(interfaceOrientation)) + size = CGSizeMake(size.height, size.width); + + return CGRectMake((size.width - _onButton.frame.size.width) / 2, + (size.height - _onButton.frame.size.height) / 2, + _onButton.frame.size.width, _onButton.frame.size.height); +} + +- (CGRect)_offButtonFrameForInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation +{ + CGSize size = self.frame.size; + if (UIInterfaceOrientationIsLandscape(interfaceOrientation)) + size = CGSizeMake(size.height, size.width); + + return CGRectMake(size.width / 4 * 3 - _offButton.frame.size.width / 2, + (size.height - _offButton.frame.size.height) / 2, + _offButton.frame.size.width, _offButton.frame.size.height); +} + ++ (CGSize)_sizeForModeButtonWithTitle:(NSAttributedString *)title +{ + CGSize size = title.size; + CGFloat width = CGCeil(size.width); + if (iosMajorVersion() < 7) + width += 2; + return CGSizeMake(width, 20); +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGCameraFlipButton.h b/submodules/LegacyComponents/LegacyComponents/TGCameraFlipButton.h new file mode 100644 index 0000000000..f774b427b9 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGCameraFlipButton.h @@ -0,0 +1,8 @@ +#import + +@interface TGCameraFlipButton : TGModernButton + +- (instancetype)initWithFrame:(CGRect)frame large:(bool)large; +- (void)setHidden:(bool)hidden animated:(bool)animated; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGCameraFlipButton.m b/submodules/LegacyComponents/LegacyComponents/TGCameraFlipButton.m new file mode 100644 index 0000000000..0f204e71ad --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGCameraFlipButton.m @@ -0,0 +1,52 @@ +#import "TGCameraFlipButton.h" + +#import "LegacyComponentsInternal.h" +#import "TGImageUtils.h" + +@implementation TGCameraFlipButton + +- (instancetype)initWithFrame:(CGRect)frame large:(bool)large +{ + self = [super initWithFrame:frame]; + if (self != nil) + { + self.exclusiveTouch = true; + UIImage *image = large ? TGComponentsImageNamed(@"CameraLargeFlipButton") : TGTintedImage(TGComponentsImageNamed(@"CameraFlipButton"), [UIColor whiteColor]); + [self setImage:image forState:UIControlStateNormal]; + } + return self; +} + +- (void)setHidden:(BOOL)hidden +{ + self.alpha = hidden ? 0.0f : 1.0f; + super.hidden = hidden; +} + +- (void)setHidden:(bool)hidden animated:(bool)animated +{ + if (animated) + { + super.hidden = false; + self.userInteractionEnabled = false; + + [UIView animateWithDuration:0.25f + animations:^ + { + self.alpha = hidden ? 0.0f : 1.0f; + } completion:^(BOOL finished) + { + self.userInteractionEnabled = true; + + if (finished) + self.hidden = hidden; + }]; + } + else + { + self.alpha = hidden ? 0.0f : 1.0f; + super.hidden = hidden; + } +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGCameraFocusCrosshairsControl.h b/submodules/LegacyComponents/LegacyComponents/TGCameraFocusCrosshairsControl.h new file mode 100644 index 0000000000..1abc2ad409 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGCameraFocusCrosshairsControl.h @@ -0,0 +1,25 @@ +#import + +@class TGCameraPreviewView; + +@interface TGCameraFocusCrosshairsControl : UIControl + +@property (nonatomic, weak) TGCameraPreviewView *previewView; +@property (nonatomic, copy) void(^focusPOIChanged)(CGPoint point); + +@property (nonatomic, copy) void(^beganExposureChange)(void); +@property (nonatomic, copy) void(^exposureChanged)(CGFloat value); +@property (nonatomic, copy) void(^endedExposureChange)(void); + +@property (nonatomic, assign) bool stopAutomatically; +@property (nonatomic, assign) bool active; +@property (nonatomic, assign) bool ignoreAutofocusing; + +- (void)playAutoFocusAnimation; +- (void)stopAutoFocusAnimation; + +- (void)reset; + +- (void)setInterfaceOrientation:(UIInterfaceOrientation)orientation animated:(bool)animated; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGCameraFocusCrosshairsControl.m b/submodules/LegacyComponents/LegacyComponents/TGCameraFocusCrosshairsControl.m new file mode 100644 index 0000000000..6b8407fcc8 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGCameraFocusCrosshairsControl.m @@ -0,0 +1,487 @@ +#import "TGCameraFocusCrosshairsControl.h" +#import + +#import "LegacyComponentsInternal.h" + +#import + +#import +#import + +@interface TGCameraFocusCrosshairsControl () +{ + bool _animatingFocusPOI; + + UIView *_wrapperView; + + UIView *_focusIndicatorView; + UIImageView *_focusIndicatorImageView; + + UIImageView *_autoFocusIndicator; + + UITapGestureRecognizer *_tapGestureRecognizer; + UIPanGestureRecognizer *_panGestureRecognizer; + + UIView *_exposureWrapperView; + UIView *_exposureClipView; + UIView *_exposureIndicatorView; + UIImageView *_exposureIconView; + UIView *_exposureTopLine; + UIView *_exposureBottomLine; + + CGFloat _exposureValue; + + UIInterfaceOrientation _interfaceOrientation; + + bool _hideOnStop; + + bool _ignoreAutofocusForExposing; +} +@end + +@implementation TGCameraFocusCrosshairsControl + +- (instancetype)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + if (self != nil) + { + self.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + self.backgroundColor = [UIColor clearColor]; + + _wrapperView = [[UIView alloc] initWithFrame:self.bounds]; + _wrapperView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + [self addSubview:_wrapperView]; + + _tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleFocusTap:)]; + [_wrapperView addGestureRecognizer:_tapGestureRecognizer]; + + _focusIndicatorView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 75 + 90, 75 + 90)]; + _focusIndicatorView.hidden = true; + [_wrapperView addSubview:_focusIndicatorView]; + + _focusIndicatorImageView = [[UIImageView alloc] initWithFrame:CGRectMake(45, 45, 75, 75)]; + _focusIndicatorImageView.image = TGComponentsImageNamed(@"CameraFocusCrosshairs"); + _focusIndicatorImageView.alpha = 0.0; + [_focusIndicatorView addSubview:_focusIndicatorImageView]; + + _autoFocusIndicator = [[UIImageView alloc] initWithFrame:CGRectMake(CGFloor((self.bounds.size.width - 125) / 2), CGFloor((self.bounds.size.height - 125) / 2), 125, 125)]; + _autoFocusIndicator.autoresizingMask = UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleBottomMargin; + _autoFocusIndicator.backgroundColor = [UIColor clearColor]; + _autoFocusIndicator.image = TGComponentsImageNamed(@"CameraAutoFocusCrosshairs"); + _autoFocusIndicator.alpha = 0.0f; + [_wrapperView addSubview:_autoFocusIndicator]; + + if (iosMajorVersion() >= 8) + { + _exposureWrapperView = [[UIView alloc] initWithFrame:_focusIndicatorView.bounds]; + [_focusIndicatorView addSubview:_exposureWrapperView]; + + _exposureClipView = [[UIView alloc] initWithFrame:CGRectMake(45 + _focusIndicatorImageView.frame.size.width + 5, 45 + (_focusIndicatorImageView.frame.size.height - 144) / 2, 25, 144)]; + _exposureClipView.clipsToBounds = true; + [_exposureWrapperView addSubview:_exposureClipView]; + + _exposureIndicatorView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 25, 144)]; + [_exposureClipView addSubview:_exposureIndicatorView]; + + _exposureIconView = [[UIImageView alloc] initWithFrame:CGRectMake(-1, 59.5f, 25, 25)]; + _exposureIconView.image = TGComponentsImageNamed(@"CameraExposureIcon"); + [_exposureIndicatorView addSubview:_exposureIconView]; + + _exposureTopLine = [[UIView alloc] initWithFrame:CGRectMake(11, _exposureIconView.frame.origin.y - 3 - _exposureIndicatorView.frame.size.height, 1, _exposureIndicatorView.frame.size.height)]; + _exposureTopLine.alpha = 0.0f; + _exposureTopLine.backgroundColor = [TGCameraInterfaceAssets accentColor]; + [_exposureIndicatorView addSubview:_exposureTopLine]; + + _exposureBottomLine = [[UIView alloc] initWithFrame:CGRectMake(11, _exposureIconView.frame.origin.y + _exposureIconView.frame.size.height + 3, 1, _exposureIndicatorView.frame.size.height)]; + _exposureBottomLine.alpha = 0.0f; + _exposureBottomLine.backgroundColor = [TGCameraInterfaceAssets accentColor]; + [_exposureIndicatorView addSubview:_exposureBottomLine]; + + _panGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePan:)]; + [_focusIndicatorView addGestureRecognizer:_panGestureRecognizer]; + } + else + { + _hideOnStop = true; + } + } + + return self; +} + +- (void)handleFocusTap:(UITapGestureRecognizer *)gestureRecognizer +{ + if (gestureRecognizer.state == UIGestureRecognizerStateRecognized) + { + TGCameraPreviewView *previewView = self.previewView; + CGPoint previewLocation = [gestureRecognizer locationInView:previewView]; + + if (self.focusPOIChanged != nil) + self.focusPOIChanged([previewView devicePointOfInterestForPoint:previewLocation]); + + CGPoint location = [gestureRecognizer locationInView:self]; + _focusIndicatorView.frame = CGRectMake(CGFloor(location.x - _focusIndicatorView.frame.size.width / 2), CGFloor(location.y - _focusIndicatorView.frame.size.height / 2), _focusIndicatorView.frame.size.width, _focusIndicatorView.frame.size.height); + [self playFocusPOIAnimation]; + + _exposureValue = 0.0f; + [self setExposureSliderPosition:0.0f]; + [self setExposureSliderTrackHidden:true animated:false]; + [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(deactivateFocusIndicatorAnimated) object:nil]; + + [self updateExposureIndicatorPositionForOrientation:_interfaceOrientation]; + } +} + +- (void)playAutoFocusAnimation +{ + if (!_animatingFocusPOI) + { + if (self.ignoreAutofocusing || _ignoreAutofocusForExposing) + return; + + _focusIndicatorView.hidden = true; + _autoFocusIndicator.hidden = false; + + CAKeyframeAnimation *scaleAnimation = [CAKeyframeAnimation animationWithKeyPath:@"transform"]; + NSArray *scaleValues = [NSArray arrayWithObjects: + [NSValue valueWithCATransform3D:CATransform3DScale(_autoFocusIndicator.layer.transform, 2, 2, 1)], + [NSValue valueWithCATransform3D:CATransform3DScale(_autoFocusIndicator.layer.transform, 1, 1, 1)], nil]; + [scaleAnimation setValues:scaleValues]; + scaleAnimation.fillMode = kCAFillModeForwards; + scaleAnimation.duration = 0.2f; + [_autoFocusIndicator.layer addAnimation:scaleAnimation forKey:@"scale"]; + + CAKeyframeAnimation *blinkAnim = [CAKeyframeAnimation animationWithKeyPath:@"opacity"]; + blinkAnim.duration = 2.0f; + blinkAnim.autoreverses = false; + blinkAnim.fillMode = kCAFillModeForwards; + blinkAnim.repeatCount = HUGE_VALF; + + blinkAnim.keyTimes = @[ @0.0f, @0.1f, @0.2f, @0.3f, @0.4f, @0.5f, @0.6f, @0.7f, @0.8f, @0.9f, @1.0f ]; + blinkAnim.values = @[ @0.4f, @1.0f, @0.4f, @1.0f, @0.4f, @1.0f, @0.4f, @1.0f, @0.4f, @1.0f, @0.4f ]; + + [_autoFocusIndicator.layer addAnimation:blinkAnim forKey:@"opacity"]; + } + else + { + [_autoFocusIndicator.layer removeAnimationForKey:@"scale"]; + [_autoFocusIndicator.layer removeAnimationForKey:@"opacity"]; + } +} + +- (void)stopAutoFocusAnimation +{ + if (!_animatingFocusPOI) + { + if (![_autoFocusIndicator.layer.animationKeys containsObject:@"opacity"]) + return; + + [_autoFocusIndicator.layer removeAnimationForKey:@"opacity"]; + + CAKeyframeAnimation *blinkAnim = [CAKeyframeAnimation animationWithKeyPath:@"opacity"]; + blinkAnim.duration = 0.2f; + blinkAnim.autoreverses = false; + blinkAnim.fillMode = kCAFillModeForwards; + + blinkAnim.keyTimes = @[ @0.0f, @1.0f ]; + blinkAnim.values = @[ @1.0f, @0.0f ]; + + [_autoFocusIndicator.layer addAnimation:blinkAnim forKey:@"opacity"]; + } + else + { + [self stopFocusPOIAnimation]; + } +} + +- (void)playFocusPOIAnimation +{ + if (self.stopAutomatically) + [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(stopFocusPOIAnimation) object:nil]; + + _focusIndicatorView.alpha = 1.0f; + _focusIndicatorView.hidden = false; + _autoFocusIndicator.hidden = true; + _animatingFocusPOI = true; + + CAKeyframeAnimation *scaleAnimation = [CAKeyframeAnimation animationWithKeyPath:@"transform"]; + NSArray *scaleValues = [NSArray arrayWithObjects: + [NSValue valueWithCATransform3D:CATransform3DScale(_focusIndicatorView.layer.transform, 2, 2, 1)], + [NSValue valueWithCATransform3D:CATransform3DScale(_focusIndicatorView.layer.transform, 1, 1, 1)], nil]; + [scaleAnimation setValues:scaleValues]; + scaleAnimation.fillMode = kCAFillModeForwards; + scaleAnimation.duration = 0.15f; + [_focusIndicatorView.layer addAnimation:scaleAnimation forKey:@"scale"]; + + CAKeyframeAnimation *blinkAnim = [CAKeyframeAnimation animationWithKeyPath:@"opacity"]; + blinkAnim.duration = 2.0f; + blinkAnim.autoreverses = false; + blinkAnim.fillMode = kCAFillModeForwards; + blinkAnim.repeatCount = HUGE_VALF; + + blinkAnim.keyTimes = @[ @0.0f, @0.1f, @0.2f, @0.3f, @0.4f, @0.5f, @0.6f, @0.7f, @0.8f, @0.9f, @1.0f ]; + blinkAnim.values = @[ @0.6f, @1.0f, @0.6f, @1.0f, @0.6f, @1.0f, @0.6f, @1.0f, @0.6f, @1.0f, @0.6f ]; + + [_focusIndicatorImageView.layer addAnimation:blinkAnim forKey:@"opacity"]; + + if (self.stopAutomatically) + [self performSelector:@selector(stopFocusPOIAnimation) withObject:nil afterDelay:1.0f]; +} + +- (void)stopFocusPOIAnimation +{ + [_focusIndicatorImageView.layer removeAnimationForKey:@"opacity"]; + _focusIndicatorImageView.layer.opacity = 1.0f; + + if (_hideOnStop) + { + [UIView animateWithDuration:0.2f delay:0.9f options:0 animations:^ + { + _focusIndicatorView.alpha = 0.0f; + } completion:^(__unused BOOL finished) + { + _focusIndicatorView.hidden = true; + _animatingFocusPOI = false; + }]; + } + else + { + [UIView animateWithDuration:0.2f delay:0.9f options:UIViewAnimationOptionAllowUserInteraction animations:^ + { + _focusIndicatorView.alpha = 0.5f; + } completion:^(__unused BOOL finished) + { + _animatingFocusPOI = false; + }]; + } +} + +- (void)reset +{ + [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(stopFocusPOIAnimation) object:nil]; + [self stopAutoFocusAnimation]; + _focusIndicatorView.hidden = true; +} + +- (BOOL)enabled +{ + return _tapGestureRecognizer.enabled; +} + +- (void)setEnabled:(BOOL)enabled +{ + _tapGestureRecognizer.enabled = enabled; +} + +- (bool)active +{ + return !_wrapperView.hidden; +} + +- (void)setActive:(bool)active +{ + _wrapperView.hidden = !active; +} + + +- (void)setIgnoreAutofocusing:(bool)ignoreAutofocusing +{ + _ignoreAutofocusing = ignoreAutofocusing; + + if (ignoreAutofocusing) + { + _autoFocusIndicator.hidden = true; + [_autoFocusIndicator.layer removeAnimationForKey:@"scale"]; + [_autoFocusIndicator.layer removeAnimationForKey:@"opacity"]; + } +} + +#pragma mark - Exposure Control + +- (void)setFocusIndicatorActive:(bool)active animated:(bool)animated +{ + CGFloat targetAlpha = active ? 1.0f : 0.5f; + + if (animated) + { + [UIView animateWithDuration:0.3f animations:^ + { + _focusIndicatorView.alpha = targetAlpha; + }]; + } + else + { + _focusIndicatorView.alpha = targetAlpha; + } +} + +- (void)deactivateFocusIndicatorAnimated +{ + [self setFocusIndicatorActive:false animated:true]; + [self setExposureSliderTrackHidden:true animated:true]; + + if (self.endedExposureChange != nil) + self.endedExposureChange(); +} + +- (void)setExposureSliderTrackHidden:(bool)hidden animated:(bool)animated +{ + CGFloat targetAlpha = hidden ? 0.0f : 1.0f; + + if (animated) + { + [UIView animateWithDuration:0.3f animations:^ + { + _exposureTopLine.alpha = targetAlpha; + _exposureBottomLine.alpha = targetAlpha; + }]; + } + else + { + _exposureTopLine.alpha = targetAlpha; + _exposureBottomLine.alpha = targetAlpha; + } +} + +- (void)setExposureSliderPosition:(CGFloat)exposureValue +{ + _exposureIndicatorView.frame = CGRectMake(_exposureIndicatorView.frame.origin.x, + exposureValue * (_exposureIndicatorView.frame.size.height - _exposureIconView.frame.size.height) / 2, + _exposureIndicatorView.frame.size.width, _exposureIndicatorView.frame.size.height); +} + +- (void)handlePan:(UIPanGestureRecognizer *)gestureRecognizer +{ + switch (gestureRecognizer.state) + { + case UIGestureRecognizerStateBegan: + { + [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(deactivateFocusIndicatorAnimated) object:nil]; + + _ignoreAutofocusForExposing = true; + + if (self.beganExposureChange != nil) + self.beganExposureChange(); + } + case UIGestureRecognizerStateChanged: + { + CGPoint translation = [gestureRecognizer translationInView:gestureRecognizer.view]; + CGFloat delta = 0.0f; + switch (_interfaceOrientation) + { + case UIInterfaceOrientationLandscapeLeft: + delta = translation.x / 750.0f; + break; + + case UIInterfaceOrientationLandscapeRight: + delta = translation.x / -750.0f; + break; + + default: + delta = translation.y / 750.0f; + break; + } + + CGFloat newValue = MAX(-1.0f, MIN(1.0f, _exposureValue + delta)); + _exposureValue = newValue; + [self setExposureSliderPosition:newValue]; + [self setExposureSliderTrackHidden:false animated:false]; + [self setFocusIndicatorActive:true animated:false]; + + [gestureRecognizer setTranslation:CGPointZero inView:gestureRecognizer.view]; + + if (self.exposureChanged != nil) + self.exposureChanged(_exposureValue * -1); + } + break; + + case UIGestureRecognizerStateEnded: + case UIGestureRecognizerStateCancelled: + { + _ignoreAutofocusForExposing = false; + + [self performSelector:@selector(deactivateFocusIndicatorAnimated) withObject:nil afterDelay:2.0f]; + } + break; + + default: + break; + } +} + +- (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);; + switch (orientation) + { + case UIInterfaceOrientationPortraitUpsideDown: + { + if (CGRectGetMinX(_focusIndicatorView.frame) < 0) + _exposureClipView.frame = mirroredPositionFrame; + else + _exposureClipView.frame = defaultPositionFrame; + } + break; + + case UIInterfaceOrientationLandscapeLeft: + { + if (CGRectGetMinY(_focusIndicatorView.frame) < 0) + _exposureClipView.frame = mirroredPositionFrame; + else + _exposureClipView.frame = defaultPositionFrame; + } + break; + + case UIInterfaceOrientationLandscapeRight: + { + if (CGRectGetMaxY(_focusIndicatorView.frame) > self.frame.size.height) + _exposureClipView.frame = mirroredPositionFrame; + else + _exposureClipView.frame = defaultPositionFrame; + } + break; + + default: + { + if (CGRectGetMaxX(_focusIndicatorView.frame) > self.frame.size.width) + _exposureClipView.frame = mirroredPositionFrame; + else + _exposureClipView.frame = defaultPositionFrame; + } + break; + } +} + +- (void)setInterfaceOrientation:(UIInterfaceOrientation)orientation animated:(bool)animated +{ + if (orientation == UIInterfaceOrientationUnknown || orientation == _interfaceOrientation) + return; + + _interfaceOrientation = orientation; + + if (animated) + { + [UIView animateWithDuration:0.25f delay:0.0f options:UIViewAnimationOptionCurveLinear animations:^ + { + _exposureWrapperView.alpha = 0.0f; + } completion:^(__unused BOOL finished) + { + _exposureWrapperView.transform = CGAffineTransformMakeRotation(TGRotationForInterfaceOrientation(orientation)); + [self updateExposureIndicatorPositionForOrientation:orientation]; + + [UIView animateWithDuration:0.2f delay:0.0f options:UIViewAnimationOptionCurveEaseInOut animations:^ + { + _exposureWrapperView.alpha = 1.0f; + } completion:nil]; + }]; + } + else + { + _exposureWrapperView.transform = CGAffineTransformMakeRotation(TGRotationForInterfaceOrientation(orientation)); + [self updateExposureIndicatorPositionForOrientation:orientation]; + } +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGCameraInterfaceAssets.h b/submodules/LegacyComponents/LegacyComponents/TGCameraInterfaceAssets.h new file mode 100644 index 0000000000..ab8c131569 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGCameraInterfaceAssets.h @@ -0,0 +1,16 @@ +#import +#import + +@interface TGCameraInterfaceAssets : NSObject + ++ (UIColor *)normalColor; ++ (UIColor *)accentColor; ++ (UIColor *)redColor; + ++ (UIColor *)panelBackgroundColor; ++ (UIColor *)transparentPanelBackgroundColor; ++ (UIColor *)transparentOverlayBackgroundColor; + ++ (UIFont *)normalFontOfSize:(CGFloat)size; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGCameraInterfaceAssets.m b/submodules/LegacyComponents/LegacyComponents/TGCameraInterfaceAssets.m new file mode 100644 index 0000000000..68b4fc84d3 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGCameraInterfaceAssets.m @@ -0,0 +1,42 @@ +#import "TGCameraInterfaceAssets.h" + +#import "LegacyComponentsInternal.h" + +@implementation TGCameraInterfaceAssets + ++ (UIColor *)normalColor +{ + return [UIColor whiteColor]; +} + ++ (UIColor *)accentColor +{ + return UIColorRGB(0xffcc00); +} + ++ (UIColor *)redColor +{ + return UIColorRGB(0xf53333); +} + ++ (UIColor *)panelBackgroundColor +{ + return [UIColor blackColor]; +} + ++ (UIColor *)transparentPanelBackgroundColor +{ + return [UIColor colorWithWhite:0.0f alpha:0.5]; +} + ++ (UIColor *)transparentOverlayBackgroundColor +{ + return [UIColor colorWithWhite:0.0f alpha:0.7]; +} + ++ (UIFont *)normalFontOfSize:(CGFloat)size +{ + return [UIFont fontWithName:@"DINAlternate-Bold" size:size]; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGCameraMainPhoneView.h b/submodules/LegacyComponents/LegacyComponents/TGCameraMainPhoneView.h new file mode 100644 index 0000000000..fd0f27557b --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGCameraMainPhoneView.h @@ -0,0 +1,7 @@ +#import + +#import + +@interface TGCameraMainPhoneView : TGCameraMainView + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGCameraMainPhoneView.m b/submodules/LegacyComponents/LegacyComponents/TGCameraMainPhoneView.m new file mode 100644 index 0000000000..8530d6870d --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGCameraMainPhoneView.m @@ -0,0 +1,864 @@ +#import "TGCameraMainPhoneView.h" + +#import +#import "LegacyComponentsInternal.h" +#import "TGImageUtils.h" +#import "TGFont.h" + +#import + +#import +#import +#import + +#import "TGModernButton.h" +#import "TGCameraShutterButton.h" +#import "TGCameraModeControl.h" +#import "TGCameraFlashControl.h" +#import "TGCameraFlashActiveView.h" +#import "TGCameraFlipButton.h" +#import "TGCameraTimeCodeView.h" +#import "TGCameraZoomView.h" +#import "TGCameraSegmentsView.h" + +#import "TGMenuView.h" + +#import "TGMediaPickerPhotoCounterButton.h" +#import "TGMediaPickerPhotoStripView.h" + +@interface TGCameraTopPanelView : UIView + +@property (nonatomic, copy) bool(^isPointInside)(CGPoint point); + +@end + +@implementation TGCameraTopPanelView + +- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event +{ + if (self.hidden) + return [super pointInside:point withEvent:event]; + + CGRect relativeFrame = self.bounds; + bool insideBounds = CGRectContainsPoint(relativeFrame, point); + + bool additionalCheck = false; + if (self.isPointInside != nil) + additionalCheck = self.isPointInside(point); + + return insideBounds || additionalCheck; +} + +@end + +@interface TGCameraMainPhoneView () +{ + TGCameraTopPanelView *_topPanelView; + UIView *_topPanelBackgroundView; + UIView *_bottomPanelView; + UIView *_bottomPanelBackgroundView; + + UIView *_topDocumentFrameView; + UIView *_bottomDocumentFrameView; + + UIView *_videoLandscapePanelView; + + TGCameraFlashControl *_flashControl; + TGCameraFlashActiveView *_flashActiveView; + + TGCameraFlipButton *_topFlipButton; + + bool _hasResults; + + CGFloat _topPanelOffset; + CGFloat _topPanelHeight; + + CGFloat _bottomPanelOffset; + CGFloat _bottomPanelHeight; + + CGFloat _modeControlOffset; + CGFloat _modeControlHeight; + + CGFloat _counterOffset; + + bool _displayedTooltip; + TGMenuContainerView *_tooltipContainerView; + NSTimer *_tooltipTimer; +} +@end + +@implementation TGCameraMainPhoneView + +@synthesize requestedVideoRecordingDuration; +@synthesize cameraFlipped; +@synthesize cameraModeChanged; +@synthesize flashModeChanged; +@synthesize focusPointChanged; +@synthesize expositionChanged; +@synthesize shutterPressed; +@synthesize shutterReleased; +@synthesize cancelPressed; +@synthesize actionHandle = _actionHandle; + +- (instancetype)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + if (self != nil) + { + _actionHandle = [[ASHandle alloc] initWithDelegate:self releaseOnMainThread:true]; + + CGFloat shutterButtonWidth = 66.0f; + CGSize screenSize = TGScreenSize(); + CGFloat widescreenWidth = MAX(screenSize.width, screenSize.height); + if (widescreenWidth == 896.0f) + { + _topPanelOffset = 33.0f; + _topPanelHeight = 44.0f; + _bottomPanelOffset = 94.0f; + _bottomPanelHeight = 123.0f; + _modeControlOffset = 0.0f; + _modeControlHeight = 52.0f; + _counterOffset = 7.0f; + shutterButtonWidth = 72.0f; + } + else if (widescreenWidth == 812.0f) + { + _topPanelOffset = 33.0f; + _topPanelHeight = 44.0f; + _bottomPanelOffset = 63.0f; + _bottomPanelHeight = 123.0f; + _modeControlOffset = 3.0f; + _modeControlHeight = 40.0f; + _counterOffset = 7.0f; + shutterButtonWidth = 70.0f; + } + else if (widescreenWidth >= 736.0f - FLT_EPSILON) + { + _topPanelHeight = 44.0f; + _bottomPanelHeight = 140.0f; + _modeControlHeight = 50.0f; + _counterOffset = 8.0f; + shutterButtonWidth = 70.0f; + } + else if (widescreenWidth >= 667.0f - FLT_EPSILON) + { + _topPanelHeight = 44.0f; + _bottomPanelHeight = 123.0f; + _modeControlOffset = 4.0f; + _modeControlHeight = 36.0f; + _counterOffset = 6.0f; + shutterButtonWidth = 70.0f; + } + else + { + _topPanelHeight = 40.0f; + _bottomPanelHeight = 101.0f; + _modeControlHeight = 31.0f; + _counterOffset = 8.0f; + } + + __weak TGCameraMainPhoneView *weakSelf = self; + + _topPanelView = [[TGCameraTopPanelView alloc] init]; + _topPanelView.clipsToBounds = false; + _topPanelView.isPointInside = ^bool(CGPoint point) + { + __strong TGCameraMainPhoneView *strongSelf = weakSelf; + if (strongSelf == nil) + return false; + + CGRect rect = [strongSelf->_topPanelView convertRect:strongSelf->_flashControl.frame fromView:strongSelf->_flashControl.superview]; + return CGRectContainsPoint(rect, point); + }; + [self addSubview:_topPanelView]; + + _topPanelBackgroundView = [[UIView alloc] initWithFrame:_topPanelView.bounds]; + _topPanelBackgroundView.backgroundColor = [TGCameraInterfaceAssets transparentPanelBackgroundColor]; + [_topPanelView addSubview:_topPanelBackgroundView]; + + _bottomPanelView = [[UIView alloc] init]; + [self addSubview:_bottomPanelView]; + + _bottomPanelBackgroundView = [[UIView alloc] initWithFrame:_bottomPanelView.bounds]; + _bottomPanelBackgroundView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + _bottomPanelBackgroundView.backgroundColor = [TGCameraInterfaceAssets transparentPanelBackgroundColor]; + [_bottomPanelView addSubview:_bottomPanelBackgroundView]; + + _cancelButton = [[TGModernButton alloc] initWithFrame:CGRectMake(0, 0, 60, 44)]; + _cancelButton.backgroundColor = [UIColor clearColor]; + _cancelButton.contentHorizontalAlignment = UIControlContentHorizontalAlignmentLeft; + _cancelButton.exclusiveTouch = true; + _cancelButton.titleLabel.font = TGSystemFontOfSize(18); + _cancelButton.contentEdgeInsets = UIEdgeInsetsMake(0, 20, 0, 0); + [_cancelButton setTitle:TGLocalized(@"Common.Cancel") forState:UIControlStateNormal]; + [_cancelButton setTintColor:[TGCameraInterfaceAssets normalColor]]; + [_cancelButton sizeToFit]; + _cancelButton.frame = CGRectMake(0, 0, MAX(60.0f, _cancelButton.frame.size.width), 44); + [_cancelButton addTarget:self action:@selector(cancelButtonPressed) forControlEvents:UIControlEventTouchUpInside]; + [_bottomPanelView addSubview:_cancelButton]; + + _doneButton = [[TGModernButton alloc] initWithFrame:CGRectMake(0, 0, 60, 44)]; + _doneButton.backgroundColor = [UIColor clearColor]; + _doneButton.contentHorizontalAlignment = UIControlContentHorizontalAlignmentRight; + _doneButton.exclusiveTouch = true; + _doneButton.hidden = true; + _doneButton.titleLabel.font = TGMediumSystemFontOfSize(18); + _doneButton.contentEdgeInsets = UIEdgeInsetsMake(0, 0, 0, 20); + [_doneButton setTitle:TGLocalized(@"Common.Done") forState:UIControlStateNormal]; + [_doneButton setTintColor:[TGCameraInterfaceAssets normalColor]]; + [_doneButton sizeToFit]; + _doneButton.frame = CGRectMake(0, 0, MAX(60.0f, _doneButton.frame.size.width), 44); + [_doneButton addTarget:self action:@selector(doneButtonPressed) forControlEvents:UIControlEventTouchUpInside]; + [_bottomPanelView addSubview:_doneButton]; + + _shutterButton = [[TGCameraShutterButton alloc] initWithFrame:CGRectMake((frame.size.width - shutterButtonWidth) / 2, 10, shutterButtonWidth, shutterButtonWidth)]; + [_shutterButton addTarget:self action:@selector(shutterButtonReleased) forControlEvents:UIControlEventTouchUpInside]; + [_shutterButton addTarget:self action:@selector(shutterButtonPressed) forControlEvents:UIControlEventTouchDown]; + [_bottomPanelView addSubview:_shutterButton]; + + _modeControl = [[TGCameraModeControl alloc] initWithFrame:CGRectMake(0, 0, frame.size.width, _modeControlHeight)]; + [_bottomPanelView addSubview:_modeControl]; + + _flipButton = [[TGCameraFlipButton alloc] initWithFrame:CGRectMake(0, 0, 56, 56) large:true]; + [_flipButton addTarget:self action:@selector(flipButtonPressed) forControlEvents:UIControlEventTouchUpInside]; + [_bottomPanelView addSubview:_flipButton]; + + _flashControl = [[TGCameraFlashControl alloc] initWithFrame:CGRectMake(0, 0, frame.size.width, TGCameraFlashControlHeight)]; + [_topPanelView addSubview:_flashControl]; + + _topFlipButton = [[TGCameraFlipButton alloc] initWithFrame:CGRectMake(0, 0, 44, 44) large:false]; + _topFlipButton.hidden = true; + [_topFlipButton addTarget:self action:@selector(flipButtonPressed) forControlEvents:UIControlEventTouchUpInside]; + [_topPanelView addSubview:_topFlipButton]; + + _timecodeView = [[TGCameraTimeCodeView alloc] initWithFrame:CGRectMake((frame.size.width - 120) / 2, 12, 120, 20)]; + _timecodeView.hidden = true; + _timecodeView.requestedRecordingDuration = ^NSTimeInterval + { + __strong TGCameraMainPhoneView *strongSelf = weakSelf; + if (strongSelf == nil || strongSelf.requestedVideoRecordingDuration == nil) + return 0.0; + + return strongSelf.requestedVideoRecordingDuration(); + }; + [_topPanelView addSubview:_timecodeView]; + + _videoLandscapePanelView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 274, 44)]; + _videoLandscapePanelView.alpha = 0.0f; + _videoLandscapePanelView.backgroundColor = [TGCameraInterfaceAssets transparentPanelBackgroundColor]; + _videoLandscapePanelView.hidden = true; + _videoLandscapePanelView.layer.cornerRadius = 3.5f; + [self addSubview:_videoLandscapePanelView]; + + _flashActiveView = [[TGCameraFlashActiveView alloc] initWithFrame:CGRectMake((frame.size.width - 40) / 2, frame.size.height - _bottomPanelHeight - 37, 40, 21)]; + [self addSubview:_flashActiveView]; + + _zoomView = [[TGCameraZoomView alloc] initWithFrame:CGRectMake(10, frame.size.height - _bottomPanelHeight - _bottomPanelOffset - 18, frame.size.width - 20, 1.5f)]; + _zoomView.activityChanged = ^(bool active) + { + __strong TGCameraMainPhoneView *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [UIView animateWithDuration:0.3f delay:0.0f options:UIViewAnimationOptionCurveEaseInOut animations:^ + { + [strongSelf _layoutFlashActiveViewForInterfaceOrientation:strongSelf->_interfaceOrientation zoomViewHidden:!active]; + } completion:nil]; + }; + [self addSubview:_zoomView]; + + _flashControl.becameActive = ^ + { + __strong TGCameraMainPhoneView *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + if (strongSelf->_modeControl.cameraMode == PGCameraModeVideo) + [strongSelf->_timecodeView setHidden:true animated:true]; + }; + + _flashControl.modeChanged = ^(PGCameraFlashMode mode) + { + __strong TGCameraMainPhoneView *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + if (strongSelf.flashModeChanged != nil) + strongSelf.flashModeChanged(mode); + + if (strongSelf->_modeControl.cameraMode == PGCameraModeVideo) + [strongSelf->_timecodeView setHidden:false animated:true]; + }; + + _modeControl.modeChanged = ^(PGCameraMode mode, PGCameraMode previousMode) + { + __strong TGCameraMainPhoneView *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + bool change = true; + if (strongSelf.cameraShouldLeaveMode != nil) + change = strongSelf.cameraShouldLeaveMode(previousMode); + + void (^changeBlock)(void) = ^ + { + if (strongSelf.cameraModeChanged != nil) + strongSelf.cameraModeChanged(mode); + + [strongSelf updateForCameraModeChangeWithPreviousMode:previousMode]; + }; + + changeBlock(); + }; + + + _selectedPhotosView = [[TGMediaPickerPhotoStripView alloc] initWithFrame:CGRectZero]; + _selectedPhotosView.interfaceOrientation = UIInterfaceOrientationPortrait; + _selectedPhotosView.removable = true; + _selectedPhotosView.itemSelected = ^(NSInteger index) + { + __strong TGCameraMainPhoneView *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [strongSelf->_photoCounterButton setSelected:false animated:true]; + [strongSelf->_selectedPhotosView setHidden:true animated:true]; + + if (strongSelf.resultPressed != nil) + strongSelf.resultPressed(index); + }; + _selectedPhotosView.itemRemoved = ^(NSInteger index) + { + __strong TGCameraMainPhoneView *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + if (strongSelf.itemRemoved != nil) + strongSelf.itemRemoved(index); + }; + _selectedPhotosView.hidden = true; + [self addSubview:_selectedPhotosView]; + + _photoCounterButton = [[TGMediaPickerPhotoCounterButton alloc] initWithFrame:CGRectMake(0, 0, 64, 38)]; + [_photoCounterButton addTarget:self action:@selector(photoCounterButtonPressed) forControlEvents:UIControlEventTouchUpInside]; + _photoCounterButton.userInteractionEnabled = false; + [_bottomPanelView addSubview:_photoCounterButton]; + } + return self; +} + +- (void)dealloc +{ + [_actionHandle reset]; +} + +- (void)setResults:(NSArray *)results +{ + if (results.count == 0) + { + _hasResults = false; + _topFlipButton.hidden = true; + _flipButton.hidden = false; + _doneButton.hidden = true; + } + else + { + _hasResults = true; + _topFlipButton.hidden = false; + _flipButton.hidden = true; + _doneButton.hidden = false; + } +} + +- (void)setupTooltip +{ + bool displayed = [[[NSUserDefaults standardUserDefaults] objectForKey:@"TG_displayedCameraHoldToVideoTooltip_v0"] boolValue]; + if (displayed) + return; + + if (_tooltipContainerView != nil) + return; + + _tooltipTimer = [TGTimerTarget scheduledMainThreadTimerWithTarget:self action:@selector(tooltipTimerTick) interval:2.5 repeat:false]; + + _tooltipContainerView = [[TGMenuContainerView alloc] initWithFrame:CGRectMake(0.0f, 0.0f, self.frame.size.width, self.frame.size.height)]; + [self addSubview:_tooltipContainerView]; + + NSMutableArray *actions = [[NSMutableArray alloc] init]; + [actions addObject:[[NSDictionary alloc] initWithObjectsAndKeys:TGLocalized(@"Camera.TapAndHoldForVideo"), @"title", nil]]; + + [_tooltipContainerView.menuView setButtonsAndActions:actions watcherHandle:_actionHandle]; + [_tooltipContainerView.menuView sizeToFit]; + _tooltipContainerView.menuView.buttonHighlightDisabled = true; + + CGRect frame = [_shutterButton convertRect:_shutterButton.bounds toView:self]; + frame = CGRectOffset(frame, 0.0f, 1.0f); + [_tooltipContainerView showMenuFromRect:frame animated:false]; + + [[NSUserDefaults standardUserDefaults] setObject:@true forKey:@"TG_displayedCameraHoldToVideoTooltip_v0"]; +} + +- (void)tooltipTimerTick +{ + [_tooltipTimer invalidate]; + _tooltipTimer = nil; + + [_tooltipContainerView hideMenu]; +} + +- (void)actionStageActionRequested:(NSString *)action options:(id)__unused options +{ + if ([action isEqualToString:@"menuAction"]) + { + [_tooltipTimer invalidate]; + _tooltipTimer = nil; + + [_tooltipContainerView hideMenu]; + } +} + +- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event +{ + UIView *view = [super hitTest:point withEvent:event]; + + if ([view isDescendantOfView:_topPanelView] || [view isDescendantOfView:_bottomPanelView] || [view isDescendantOfView:_videoLandscapePanelView] || [view isDescendantOfView:_tooltipContainerView] || [view isDescendantOfView:_selectedPhotosView]) + return view; + + return nil; +} + +#pragma mark - Actions + +- (void)shutterButtonReleased +{ + [super shutterButtonReleased]; + + [_flashControl dismissAnimated:true]; +} + +- (void)updateForCameraModeChangeWithPreviousMode:(PGCameraMode)previousMode +{ + [super updateForCameraModeChangeWithPreviousMode:previousMode]; + + UIInterfaceOrientation orientation = _interfaceOrientation; + PGCameraMode cameraMode = _modeControl.cameraMode; + + if (UIInterfaceOrientationIsLandscape(orientation) && !((cameraMode == PGCameraModePhoto && previousMode == PGCameraModeSquare) || (cameraMode == PGCameraModeSquare && previousMode == PGCameraModePhoto))) + { + if (cameraMode == PGCameraModeVideo) + _timecodeView.hidden = true; + + [UIView animateWithDuration:0.25f delay:0.0f options:UIViewAnimationOptionCurveLinear animations:^ + { + _topPanelView.alpha = 0.0f; + _videoLandscapePanelView.alpha = 0.0f; + } completion:^(__unused BOOL finished) + { + if (cameraMode == PGCameraModeVideo) + { + _timecodeView.hidden = false; + _flashControl.transform = CGAffineTransformIdentity; + _flashControl.interfaceOrientation = UIInterfaceOrientationPortrait; + [self _layoutTopPanelViewForInterfaceOrientation:orientation]; + } + else + { + _flashControl.transform = CGAffineTransformMakeRotation(TGRotationForInterfaceOrientation(orientation)); + _flashControl.interfaceOrientation = orientation; + [self _layoutTopPanelViewForInterfaceOrientation:UIInterfaceOrientationPortrait]; + } + + if (cameraMode == PGCameraModeVideo) + [self _attachControlsToLandscapePanel]; + else + [self _attachControlsToTopPanel]; + + [self _layoutTopPanelSubviewsForInterfaceOrientation:orientation]; + [_flashControl dismissAnimated:false]; + + [UIView animateWithDuration:0.2f delay:0.0f options:UIViewAnimationOptionCurveEaseInOut animations:^ + { + if (cameraMode == PGCameraModeVideo) + _videoLandscapePanelView.alpha = 1.0f; + else + _topPanelView.alpha = 1.0f; + } completion:nil]; + }]; + } +} + +#pragma mark - Flash + +- (void)setFlashMode:(PGCameraFlashMode)mode +{ + [_flashControl setMode:mode]; +} + +- (void)setFlashActive:(bool)active +{ + [_flashActiveView setActive:active animated:true]; +} + +- (void)setFlashUnavailable:(bool)unavailable +{ + [_flashControl setFlashUnavailable:unavailable]; +} + +- (void)setHasFlash:(bool)hasFlash +{ + if (!hasFlash) + [_flashActiveView setActive:false animated:true]; + + [_flashControl setHidden:!hasFlash animated:true]; +} + +#pragma mark - + +- (void)setDocumentFrameHidden:(bool)hidden +{ + if (!hidden) + { + if (_topDocumentFrameView == nil) + { + _topDocumentFrameView = [[UIView alloc] init]; + _topDocumentFrameView.backgroundColor = [TGCameraInterfaceAssets transparentOverlayBackgroundColor]; + [self addSubview:_topDocumentFrameView]; + + _bottomDocumentFrameView = [[UIView alloc] init]; + _bottomDocumentFrameView.backgroundColor = [TGCameraInterfaceAssets transparentOverlayBackgroundColor]; + [self addSubview:_bottomDocumentFrameView]; + + [self setNeedsLayout]; + } + } + else + { + _topDocumentFrameView.hidden = true; + _bottomDocumentFrameView.hidden = true; + } +} + +#pragma mark - Layout + +- (void)setInterfaceHiddenForVideoRecording:(bool)hidden animated:(bool)animated +{ + bool hasDoneButton = _hasResults; + + if (animated) + { + if (!hidden) + { + _modeControl.hidden = false; + _cancelButton.hidden = false; + _flashControl.hidden = false; + _flipButton.hidden = hasDoneButton; + _bottomPanelBackgroundView.hidden = false; + _topFlipButton.hidden = !hasDoneButton; + } + + [UIView animateWithDuration:0.25 animations:^ + { + CGFloat alpha = hidden ? 0.0f : 1.0f; + _modeControl.alpha = alpha; + _cancelButton.alpha = alpha; + _flashControl.alpha = alpha; + _flipButton.alpha = alpha; + _topFlipButton.alpha = alpha; + _bottomPanelBackgroundView.alpha = alpha; + + if (hasDoneButton) + _doneButton.alpha = alpha; + } completion:^(BOOL finished) + { + if (finished) + { + _modeControl.hidden = hidden; + _cancelButton.hidden = hidden; + _flashControl.hidden = hidden; + _flipButton.hidden = hidden || hasDoneButton; + _topFlipButton.hidden = hidden || !hasDoneButton; + _bottomPanelBackgroundView.hidden = hidden; + + if (hasDoneButton) + _doneButton.hidden = hidden; + } + }]; + } + else + { + [_modeControl setHidden:hidden animated:false]; + + CGFloat alpha = hidden ? 0.0f : 1.0f; + _modeControl.hidden = hidden; + _modeControl.alpha = alpha; + _cancelButton.hidden = hidden; + _cancelButton.alpha = alpha; + _flashControl.hidden = hidden; + _flashControl.alpha = alpha; + _flipButton.hidden = hidden || hasDoneButton; + _flipButton.alpha = alpha; + _topFlipButton.hidden = hidden || !hasDoneButton; + _topFlipButton.alpha = alpha; + _bottomPanelBackgroundView.hidden = hidden; + _bottomPanelBackgroundView.alpha = alpha; + + if (hasDoneButton) + { + _doneButton.hidden = hidden; + _doneButton.alpha = alpha; + } + } + + if (hidden && _photoCounterButton.selected) + { + [_photoCounterButton setSelected:false animated:true]; + [_selectedPhotosView setHidden:true animated:true]; + } + [_photoCounterButton setHidden:hidden animated:animated]; +} + +- (void)setInterfaceOrientation:(UIInterfaceOrientation)orientation animated:(bool)animated +{ + if (orientation == UIInterfaceOrientationUnknown || orientation == _interfaceOrientation) + return; + + _interfaceOrientation = orientation; + + if (animated) + { + [UIView animateWithDuration:0.25 delay:0.0 options:UIViewAnimationOptionCurveLinear animations:^ + { + _flashActiveView.alpha = 0.0f; + + if (_modeControl.cameraMode == PGCameraModeVideo) + { + _topPanelView.alpha = 0.0f; + _videoLandscapePanelView.alpha = 0.0f; + } + else + { + _flashControl.alpha = 0.0f; + } + + _topFlipButton.transform = CGAffineTransformMakeRotation(TGRotationForInterfaceOrientation(orientation)); + _flipButton.transform = CGAffineTransformMakeRotation(TGRotationForInterfaceOrientation(orientation)); + } completion:^(__unused BOOL finished) + { + [self _layoutFlashActiveViewForInterfaceOrientation:orientation zoomViewHidden:!_zoomView.isActive]; + + if (_modeControl.cameraMode == PGCameraModeVideo) + { + _flashControl.transform = CGAffineTransformIdentity; + _flashControl.interfaceOrientation = UIInterfaceOrientationPortrait; + + [self _layoutTopPanelViewForInterfaceOrientation:orientation]; + + if (UIInterfaceOrientationIsLandscape(orientation)) + [self _attachControlsToLandscapePanel]; + else + [self _attachControlsToTopPanel]; + + _timecodeView.hidden = false; + } + else + { + _flashControl.transform = CGAffineTransformMakeRotation(TGRotationForInterfaceOrientation(orientation)); + _flashControl.interfaceOrientation = orientation; + } + + [self _layoutTopPanelSubviewsForInterfaceOrientation:orientation]; + + [_flashControl dismissAnimated:false]; + + [UIView animateWithDuration:0.2 delay:0.0 options:UIViewAnimationOptionCurveEaseInOut animations:^ + { + _flashActiveView.alpha = 1.0f; + + if (_modeControl.cameraMode == PGCameraModeVideo) + { + if (UIInterfaceOrientationIsLandscape(orientation)) + _videoLandscapePanelView.alpha = 1.0f; + else + _topPanelView.alpha = 1.0f; + } + else + { + _flashControl.alpha = 1.0f; + } + } completion:nil]; + }]; + } + else + { + [_flashControl dismissAnimated:false]; + + _topFlipButton.transform = CGAffineTransformMakeRotation(TGRotationForInterfaceOrientation(orientation)); + _flipButton.transform = CGAffineTransformMakeRotation(TGRotationForInterfaceOrientation(orientation)); + _flashControl.transform = CGAffineTransformMakeRotation(TGRotationForInterfaceOrientation(orientation)); + _flashControl.interfaceOrientation = orientation; + + [self _layoutTopPanelSubviewsForInterfaceOrientation:orientation]; + + [self _layoutFlashActiveViewForInterfaceOrientation:orientation zoomViewHidden:!_zoomView.isActive]; + + if (_modeControl.cameraMode == PGCameraModeVideo) + _timecodeView.hidden = false; + } +} + +- (void)_layoutFlashActiveViewForInterfaceOrientation:(UIInterfaceOrientation)orientation zoomViewHidden:(bool)zoomViewHidden +{ + CGFloat zoomOffset = 0; + if (!zoomViewHidden) + zoomOffset -= 23; + + _flashActiveView.transform = CGAffineTransformMakeRotation(TGRotationForInterfaceOrientation(orientation)); + switch (orientation) + { + case UIInterfaceOrientationPortraitUpsideDown: + { + _flashActiveView.frame = CGRectMake((self.frame.size.width - 40) / 2, _topPanelHeight + 16, 40, 21); + } + break; + + case UIInterfaceOrientationLandscapeLeft: + { + _flashActiveView.frame = CGRectMake(self.frame.size.width - 37, _topPanelHeight + (self.frame.size.height - _topPanelHeight - _bottomPanelHeight - _bottomPanelOffset - 40) / 2, 21, 40); + } + break; + + case UIInterfaceOrientationLandscapeRight: + { + _flashActiveView.frame = CGRectMake(16, _topPanelHeight + (self.frame.size.height - _topPanelHeight - _bottomPanelHeight - _bottomPanelOffset - 40) / 2, 21, 40); + } + break; + + default: + { + _flashActiveView.frame = CGRectMake((self.frame.size.width - 40) / 2, self.frame.size.height - _bottomPanelHeight - _bottomPanelOffset - 37 + zoomOffset, 40, 21); + } + break; + } +} + +- (void)_layoutTopPanelViewForInterfaceOrientation:(UIInterfaceOrientation)orientation +{ + CGAffineTransform transform = CGAffineTransformMakeRotation(TGRotationForInterfaceOrientation(orientation)); + + switch (orientation) + { + case UIInterfaceOrientationLandscapeLeft: + { + _videoLandscapePanelView.hidden = false; + _topPanelView.hidden = true; + + _videoLandscapePanelView.transform = transform; + _videoLandscapePanelView.frame = CGRectMake(3, (self.frame.size.height - _videoLandscapePanelView.frame.size.height) / 2, _videoLandscapePanelView.frame.size.width, _videoLandscapePanelView.frame.size.height); + } + break; + case UIInterfaceOrientationLandscapeRight: + { + _videoLandscapePanelView.hidden = false; + _topPanelView.hidden = true; + + _videoLandscapePanelView.transform = transform; + _videoLandscapePanelView.frame = CGRectMake(self.frame.size.width - _videoLandscapePanelView.frame.size.width - 3, (self.frame.size.height - _videoLandscapePanelView.frame.size.height) / 2, _videoLandscapePanelView.frame.size.width, _videoLandscapePanelView.frame.size.height); + } + break; + + case UIInterfaceOrientationPortraitUpsideDown: + { + _videoLandscapePanelView.hidden = true; + _topPanelView.hidden = false; + + _topPanelView.transform = transform; + _topPanelView.frame = CGRectMake(0, 0, _topPanelView.frame.size.width, _topPanelView.frame.size.height); + } + break; + + default: + { + _videoLandscapePanelView.hidden = true; + _topPanelView.hidden = false; + + _topPanelView.transform = transform; + _topPanelView.frame = CGRectMake(0, 0, _topPanelView.frame.size.width, _topPanelView.frame.size.height); + } + break; + } +} + +- (void)_attachControlsToTopPanel +{ + [_topPanelView addSubview:_flashControl]; + [_topPanelView addSubview:_timecodeView]; +} + +- (void)_attachControlsToLandscapePanel +{ + [_videoLandscapePanelView addSubview:_flashControl]; + [_videoLandscapePanelView addSubview:_timecodeView]; +} + +- (void)_layoutTopPanelSubviewsForInterfaceOrientation:(UIInterfaceOrientation)orientation +{ + UIView *superview = _flashControl.superview; + CGSize superviewSize = superview.frame.size; + + if (superview == _videoLandscapePanelView && superviewSize.width < superviewSize.height) + superviewSize = CGSizeMake(superviewSize.height, superviewSize.width); + + if (UIInterfaceOrientationIsLandscape(orientation) && _flashControl.interfaceOrientation == orientation && _flashControl.superview == _topPanelView) + { + if (orientation == UIInterfaceOrientationLandscapeLeft) + _flashControl.frame = CGRectMake(7, 0, TGCameraFlashControlHeight, 370); + else if (orientation == UIInterfaceOrientationLandscapeRight) + _flashControl.frame = CGRectMake(7, 0, TGCameraFlashControlHeight, 370); + } + else + { + _flashControl.frame = CGRectMake(0, (superviewSize.height - TGCameraFlashControlHeight) / 2, superviewSize.width, TGCameraFlashControlHeight); + } + _timecodeView.frame = CGRectMake((superviewSize.width - 120) / 2, (superviewSize.height - 20) / 2, 120, 20); +} + +- (void)layoutPreviewRelativeViews +{ +} + +- (void)layoutSubviews +{ + _topPanelView.frame = CGRectMake(0, _topPanelOffset, self.frame.size.width, _topPanelHeight); + _topPanelBackgroundView.frame = CGRectMake(0.0f, -_topPanelOffset, self.frame.size.width, _topPanelHeight + _topPanelOffset); + [self _layoutTopPanelSubviewsForInterfaceOrientation:_interfaceOrientation]; + + _bottomPanelView.frame = CGRectMake(0, self.frame.size.height - _bottomPanelHeight - _bottomPanelOffset, self.frame.size.width, _bottomPanelHeight + _bottomPanelOffset); + + CGFloat documentFrameHeight = self.frame.size.width * 0.704f; + CGFloat documentTopEdge = CGRectGetMidY(self.previewViewFrame) - documentFrameHeight / 2.0f; + CGFloat documentBottomEdge = CGRectGetMidY(self.previewViewFrame) + documentFrameHeight / 2.0f; + + _topDocumentFrameView.frame = CGRectMake(0.0f, CGRectGetMaxY(_topPanelView.frame), self.frame.size.width, documentTopEdge - CGRectGetMaxY(_topPanelView.frame)); + _bottomDocumentFrameView.frame = CGRectMake(0.0f, documentBottomEdge, self.frame.size.width, CGRectGetMinY(_bottomPanelView.frame) - documentBottomEdge); + + _modeControl.frame = CGRectMake(0, _modeControlOffset, self.frame.size.width, _modeControlHeight); + _shutterButton.frame = CGRectMake(round((self.frame.size.width - _shutterButton.frame.size.width) / 2), _modeControlHeight + _modeControlOffset, _shutterButton.frame.size.width, _shutterButton.frame.size.height); + _cancelButton.frame = CGRectMake(0, round(_shutterButton.center.y - _cancelButton.frame.size.height / 2.0f), _cancelButton.frame.size.width, _cancelButton.frame.size.height); + _doneButton.frame = CGRectMake(_bottomPanelView.frame.size.width - _doneButton.frame.size.width, round(_shutterButton.center.y - _doneButton.frame.size.height / 2.0f), _doneButton.frame.size.width, _doneButton.frame.size.height); + + _flipButton.frame = CGRectMake(self.frame.size.width - _flipButton.frame.size.width - 4.0f - 7.0f, round(_shutterButton.center.y - _flipButton.frame.size.height / 2.0f), _flipButton.frame.size.width, _flipButton.frame.size.height); + + _topFlipButton.frame = CGRectMake(self.frame.size.width - _topFlipButton.frame.size.width - 4.0f, 0.0f, _topFlipButton.frame.size.width, _topFlipButton.frame.size.height); + + 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); + + if (!_displayedTooltip && _modeControl.superview != nil) + { + _displayedTooltip = true; + [self setupTooltip]; + } +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGCameraMainTabletView.h b/submodules/LegacyComponents/LegacyComponents/TGCameraMainTabletView.h new file mode 100644 index 0000000000..ba2339132f --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGCameraMainTabletView.h @@ -0,0 +1,7 @@ +#import + +#import + +@interface TGCameraMainTabletView : TGCameraMainView + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGCameraMainTabletView.m b/submodules/LegacyComponents/LegacyComponents/TGCameraMainTabletView.m new file mode 100644 index 0000000000..03b00dc765 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGCameraMainTabletView.m @@ -0,0 +1,304 @@ +#import "TGCameraMainTabletView.h" + +#import +#import "LegacyComponentsInternal.h" +#import "TGFont.h" + +#import "UIControl+HitTestEdgeInsets.h" + +#import "TGCameraInterfaceAssets.h" + +#import +#import "TGCameraShutterButton.h" +#import "TGCameraModeControl.h" +#import "TGCameraFlipButton.h" +#import "TGCameraTimeCodeView.h" +#import "TGCameraZoomView.h" + +#import "TGPhotoEditorUtils.h" + +#import "TGMediaPickerPhotoCounterButton.h" +#import "TGMediaPickerPhotoStripView.h" + +const CGFloat TGCameraTabletPanelViewWidth = 102.0f; + +@interface TGCameraMainTabletView () +{ + UIView *_panelView; + UIView *_panelBackgroundView; + + bool _hasResults; +} +@end + +@implementation TGCameraMainTabletView + +@synthesize cameraFlipped; +@synthesize cameraModeChanged; +@synthesize flashModeChanged; +@synthesize focusPointChanged; +@synthesize expositionChanged; +@synthesize shutterPressed; +@synthesize shutterReleased; +@synthesize cancelPressed; + +- (instancetype)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + if (self != nil) + { + _panelView = [[UIView alloc] initWithFrame:CGRectMake(frame.size.width - TGCameraTabletPanelViewWidth, 0, TGCameraTabletPanelViewWidth, frame.size.height)]; + [self addSubview:_panelView]; + + _panelBackgroundView = [[UIView alloc] initWithFrame:_panelView.bounds]; + _panelBackgroundView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + _panelBackgroundView.backgroundColor = [TGCameraInterfaceAssets transparentPanelBackgroundColor]; + [_panelView addSubview:_panelBackgroundView]; + + _cancelButton = [[TGModernButton alloc] initWithFrame:CGRectMake(0, 0, 60, 44)]; + _cancelButton.backgroundColor = [UIColor clearColor]; + _cancelButton.exclusiveTouch = true; + _cancelButton.titleLabel.font = TGSystemFontOfSize(18); + [_cancelButton setTitle:TGLocalized(@"Common.Cancel") forState:UIControlStateNormal]; + [_cancelButton setTintColor:[TGCameraInterfaceAssets normalColor]]; + [_cancelButton sizeToFit]; + _cancelButton.frame = CGRectMake(0, 0.5f, MAX(60.0f, _cancelButton.frame.size.width), 44); + [_cancelButton addTarget:self action:@selector(cancelButtonPressed) forControlEvents:UIControlEventTouchUpInside]; + [_panelView addSubview:_cancelButton]; + + _doneButton = [[TGModernButton alloc] initWithFrame:CGRectMake(0, 0, 60, 44)]; + _doneButton.hidden = true; + _doneButton.backgroundColor = [UIColor clearColor]; + _doneButton.exclusiveTouch = true; + _doneButton.titleLabel.font = TGMediumSystemFontOfSize(18); + [_doneButton setTitle:TGLocalized(@"Common.Done") forState:UIControlStateNormal]; + [_doneButton setTintColor:[TGCameraInterfaceAssets normalColor]]; + [_doneButton sizeToFit]; + _doneButton.frame = CGRectMake(0, 0.5f, MAX(60.0f, _doneButton.frame.size.width), 44); + [_doneButton addTarget:self action:@selector(doneButtonPressed) forControlEvents:UIControlEventTouchUpInside]; + [_panelView addSubview:_doneButton]; + + _shutterButton = [[TGCameraShutterButton alloc] initWithFrame:CGRectMake(0, 0, 66, 66)]; + [_shutterButton addTarget:self action:@selector(shutterButtonPressed) forControlEvents:UIControlEventTouchDown]; + [_shutterButton addTarget:self action:@selector(shutterButtonReleased) forControlEvents:UIControlEventTouchUpInside]; + [_panelView addSubview:_shutterButton]; + + _modeControl = [[TGCameraModeControl alloc] initWithFrame:CGRectMake(0, 0, _panelView.frame.size.width, 260)]; + [_panelView addSubview:_modeControl]; + + __weak TGCameraMainTabletView *weakSelf = self; + + _timecodeView = [[TGCameraTimeCodeView alloc] initWithFrame:CGRectMake((frame.size.width - 120) / 2, frame.size.height / 4 - 10, 120, 20)]; + _timecodeView.hidden = true; + _timecodeView.requestedRecordingDuration = ^NSTimeInterval + { + __strong TGCameraMainTabletView *strongSelf = weakSelf; + if (strongSelf == nil || strongSelf.requestedVideoRecordingDuration == nil) + return 0.0; + + return strongSelf.requestedVideoRecordingDuration(); + }; + [_panelView addSubview:_timecodeView]; + + _flipButton = [[TGCameraFlipButton alloc] initWithFrame:CGRectMake(0, 0, 44, 44) large:true]; + [_flipButton addTarget:self action:@selector(flipButtonPressed) forControlEvents:UIControlEventTouchUpInside]; + [_panelView addSubview:_flipButton]; + + _zoomView = [[TGCameraZoomView alloc] initWithFrame:CGRectMake(10, frame.size.height - 18, frame.size.width - 20, 1.5f)]; + [self addSubview:_zoomView]; + + _modeControl.modeChanged = ^(PGCameraMode mode, __unused PGCameraMode previousMode) + { + __strong TGCameraMainTabletView *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + if (strongSelf.cameraModeChanged != nil) + strongSelf.cameraModeChanged(mode); + + if (mode == PGCameraModePhoto) + { + [strongSelf->_shutterButton setButtonMode:TGCameraShutterButtonNormalMode animated:true]; + [strongSelf->_timecodeView setHidden:true animated:true]; + } + else if (mode == PGCameraModeVideo) + { + [strongSelf->_shutterButton setButtonMode:TGCameraShutterButtonVideoMode animated:true]; + [strongSelf->_timecodeView setHidden:false animated:true]; + } + }; + + _selectedPhotosView = [[TGMediaPickerPhotoStripView alloc] initWithFrame:CGRectZero]; + _selectedPhotosView.interfaceOrientation = UIInterfaceOrientationPortrait; + _selectedPhotosView.removable = true; + _selectedPhotosView.itemSelected = ^(NSInteger index) + { + __strong TGCameraMainTabletView *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [strongSelf->_photoCounterButton setSelected:false animated:true]; + [strongSelf->_selectedPhotosView setHidden:true animated:true]; + + if (strongSelf.resultPressed != nil) + strongSelf.resultPressed(index); + }; + _selectedPhotosView.itemRemoved = ^(NSInteger index) + { + __strong TGCameraMainTabletView *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + if (strongSelf.itemRemoved != nil) + strongSelf.itemRemoved(index); + }; + _selectedPhotosView.hidden = true; + [self addSubview:_selectedPhotosView]; + + _photoCounterButton = [[TGMediaPickerPhotoCounterButton alloc] initWithFrame:CGRectMake(0, 0, 64, 38)]; + [_photoCounterButton addTarget:self action:@selector(photoCounterButtonPressed) forControlEvents:UIControlEventTouchUpInside]; + _photoCounterButton.userInteractionEnabled = false; + [_panelView addSubview:_photoCounterButton]; + } + return self; +} + +- (void)setResults:(NSArray *)results +{ + if (results.count == 0) + { + _hasResults = false; + _doneButton.hidden = true; + } + else + { + _hasResults = true; + _doneButton.hidden = false; + } + + TGDispatchAfter(0.2, dispatch_get_main_queue(), ^ + { + [self setNeedsLayout]; + }); +} + +- (void)resultButtonPressed +{ + if (self.resultPressed != nil) + self.resultPressed(-1); +} + +- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event +{ + UIView *view = [super hitTest:point withEvent:event]; + + if ([view isDescendantOfView:_panelView] || [view isDescendantOfView:_selectedPhotosView]) + return view; + + return nil; +} + +- (void)setInterfaceHiddenForVideoRecording:(bool)hidden animated:(bool)animated +{ + bool hasDoneButton = _hasResults; + + if (animated) + { + if (!hidden) + { + _modeControl.hidden = false; + _cancelButton.hidden = false; + _flipButton.hidden = false; + _panelBackgroundView.hidden = false; + } + + [UIView animateWithDuration:0.25f + animations:^ + { + CGFloat alpha = hidden ? 0.0f : 1.0f; + _modeControl.alpha = alpha; + _cancelButton.alpha = alpha; + _flipButton.alpha = alpha; + _panelBackgroundView.alpha = alpha; + + if (hasDoneButton) + _doneButton.alpha = alpha; + } completion:^(BOOL finished) + { + if (finished) + { + _modeControl.hidden = hidden; + _cancelButton.hidden = hidden; + _flipButton.hidden = hidden; + _panelBackgroundView.hidden = hidden; + + if (hasDoneButton) + _doneButton.hidden = hidden; + } + }]; + } + else + { + [_modeControl setHidden:hidden animated:false]; + + CGFloat alpha = hidden ? 0.0f : 1.0f; + _modeControl.hidden = hidden; + _modeControl.alpha = alpha; + _cancelButton.hidden = hidden; + _cancelButton.alpha = alpha; + _flipButton.hidden = hidden; + _flipButton.alpha = alpha; + _panelBackgroundView.hidden = hidden; + _panelBackgroundView.alpha = alpha; + + if (hasDoneButton) + { + _doneButton.hidden = hidden; + _doneButton.alpha = alpha; + } + } + + if (hidden && _photoCounterButton.selected) + { + [_photoCounterButton setSelected:false animated:true]; + [_selectedPhotosView setHidden:true animated:true]; + } + [_photoCounterButton setHidden:hidden animated:animated]; +} + +- (void)layoutSubviews +{ + CGSize referenceSize = self.frame.size; + if (UIInterfaceOrientationIsLandscape(_interfaceOrientation)) + referenceSize = CGSizeMake(referenceSize.height, referenceSize.width); + + _panelView.frame = CGRectMake(referenceSize.width - TGCameraTabletPanelViewWidth, 0, TGCameraTabletPanelViewWidth, referenceSize.height); + _shutterButton.frame = CGRectMake((_panelView.frame.size.width - _shutterButton.frame.size.width) / 2, + (_panelView.frame.size.height - _shutterButton.frame.size.height) / 2, + _shutterButton.frame.size.width, _shutterButton.frame.size.height); + + CGFloat flipButtonPosition = 0.0f; + CGFloat cancelButtonPosition = _panelView.frame.size.height - _cancelButton.frame.size.height - 7; + if (!_doneButton.hidden) + { + flipButtonPosition = _panelView.frame.size.height / 8.0f - _flipButton.frame.size.height / 2.0f; + cancelButtonPosition = 7.0f; + } + _flipButton.frame = CGRectMake((_panelView.frame.size.width - _flipButton.frame.size.width) / 2, flipButtonPosition, _flipButton.frame.size.width, _flipButton.frame.size.height); + + _doneButton.frame = CGRectMake((_panelView.frame.size.width - _doneButton.frame.size.width) / 2, _panelView.frame.size.height - _doneButton.frame.size.height - 7, _doneButton.frame.size.width, _doneButton.frame.size.height); + + _cancelButton.frame = CGRectMake((_panelView.frame.size.width - _cancelButton.frame.size.width) / 2, cancelButtonPosition, _cancelButton.frame.size.width, _cancelButton.frame.size.height); + + _modeControl.frame = CGRectMake(_modeControl.frame.origin.x, CGFloor(referenceSize.height / 4 * 3 - _modeControl.frame.size.height / 2 - 12), _modeControl.frame.size.width, _modeControl.frame.size.height); + + _timecodeView.frame = CGRectMake((_panelView.frame.size.width - _timecodeView.frame.size.width) / 2, _panelView.frame.size.height / 4 - _timecodeView.frame.size.height / 2, _timecodeView.frame.size.width, _timecodeView.frame.size.height); + + _zoomView.frame = CGRectMake(10, referenceSize.height - 18, referenceSize.width - 20 - _panelView.frame.size.width, 1.5f); + + CGFloat photosViewSize = TGPhotoThumbnailSizeForCurrentScreen().height + 4 * 2; + _photoCounterButton.frame = CGRectMake((_panelView.frame.size.width - 64) / 2.0f, _panelView.frame.size.height - _cancelButton.frame.size.height - 52.0f, 64, 38); + _selectedPhotosView.frame = CGRectMake(4.0f, referenceSize.height - photosViewSize - 30.0f, referenceSize.width - 4.0f * 2.0f - _panelView.frame.size.width, photosViewSize); +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGCameraMainView.h b/submodules/LegacyComponents/LegacyComponents/TGCameraMainView.h new file mode 100644 index 0000000000..8a3484ddd7 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGCameraMainView.h @@ -0,0 +1,105 @@ +#import +#import +#import +#import + +@class SSignal; +@class TGModernButton; +@class TGCameraShutterButton; +@class TGCameraModeControl; +@class TGCameraFlipButton; +@class TGCameraTimeCodeView; +@class TGCameraZoomView; +@class TGCameraSegmentsView; +@class TGMediaPickerPhotoCounterButton; +@class TGMediaPickerPhotoStripView; +@class TGMediaPickerGallerySelectedItemsModel; +@class TGMediaEditingContext; + +@interface TGCameraMainView : UIView +{ + UIInterfaceOrientation _interfaceOrientation; + + TGCameraShutterButton *_shutterButton; + TGCameraModeControl *_modeControl; + + TGCameraFlipButton *_flipButton; + TGCameraTimeCodeView *_timecodeView; + + TGMediaPickerPhotoCounterButton *_photoCounterButton; + TGMediaPickerPhotoStripView *_selectedPhotosView; + + TGCameraZoomView *_zoomView; + +@public + TGModernButton *_cancelButton; + TGModernButton *_doneButton; +} + +@property (nonatomic, copy) void(^cameraFlipped)(void); +@property (nonatomic, copy) bool(^cameraShouldLeaveMode)(PGCameraMode mode); +@property (nonatomic, copy) void(^cameraModeChanged)(PGCameraMode mode); +@property (nonatomic, copy) void(^flashModeChanged)(PGCameraFlashMode mode); + +@property (nonatomic, copy) void(^focusPointChanged)(CGPoint point); +@property (nonatomic, copy) void(^expositionChanged)(CGFloat value); + +@property (nonatomic, copy) void(^shutterPressed)(bool fromHardwareButton); +@property (nonatomic, copy) void(^shutterReleased)(bool fromHardwareButton); +@property (nonatomic, copy) void(^cancelPressed)(void); +@property (nonatomic, copy) void(^donePressed)(void); +@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; + +- (void)setDocumentFrameHidden:(bool)hidden; +- (void)setCameraMode:(PGCameraMode)mode; +- (void)updateForCameraModeChangeWithPreviousMode:(PGCameraMode)previousMode; +- (void)updateForCameraModeChangeAfterResize; + +- (void)setFlashMode:(PGCameraFlashMode)mode; +- (void)setFlashActive:(bool)active; +- (void)setFlashUnavailable:(bool)unavailable; +- (void)setHasFlash:(bool)hasFlash; + +- (void)setHasZoom:(bool)hasZoom; +- (void)setZoomLevel:(CGFloat)zoomLevel displayNeeded:(bool)displayNeeded; +- (void)zoomChangingEnded; + +- (void)setHasModeControl:(bool)hasModeControl; + +- (void)setShutterButtonHighlighted:(bool)highlighted; +- (void)setShutterButtonEnabled:(bool)enabled; + +- (void)setDoneButtonHidden:(bool)hidden animated:(bool)animated; + +- (void)shutterButtonPressed; +- (void)shutterButtonReleased; +- (void)flipButtonPressed; +- (void)cancelButtonPressed; +- (void)doneButtonPressed; + +- (void)setRecordingVideo:(bool)recordingVideo animated:(bool)animated; +- (void)setInterfaceHiddenForVideoRecording:(bool)hidden animated:(bool)animated; + +@property (nonatomic, weak) TGMediaEditingContext *editingContext; + +@property (nonatomic, copy) SSignal *(^thumbnailSignalForItem)(id item); +- (void)setResults:(NSArray *)results; +- (void)setSelectedItemsModel:(TGMediaPickerGallerySelectedItemsModel *)selectedItemsModel; +- (void)updateSelectionInterface:(NSUInteger)selectedCount counterVisible:(bool)counterVisible animated:(bool)animated; +- (void)updateSelectedPhotosView:(bool)reload incremental:(bool)incremental add:(bool)add index:(NSInteger)index; + +- (UIInterfaceOrientation)interfaceOrientation; +- (void)setInterfaceOrientation:(UIInterfaceOrientation)orientation animated:(bool)animated; + +- (void)layoutPreviewRelativeViews; + +- (void)photoCounterButtonPressed; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGCameraMainView.m b/submodules/LegacyComponents/LegacyComponents/TGCameraMainView.m new file mode 100644 index 0000000000..53d97a46cd --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGCameraMainView.m @@ -0,0 +1,310 @@ +#import "TGCameraMainView.h" + +#import "LegacyComponentsInternal.h" + +#import + +#import "TGCameraShutterButton.h" +#import "TGCameraModeControl.h" +#import "TGCameraTimeCodeView.h" +#import "TGCameraZoomView.h" +#import "TGCameraSegmentsView.h" + +#import "TGMediaPickerPhotoCounterButton.h" +#import "TGMediaPickerPhotoStripView.h" + +@interface TGCameraMainView () +{ + +} +@end + +@implementation TGCameraMainView + +@dynamic thumbnailSignalForItem; +@dynamic editingContext; + +#pragma mark - Mode + +- (void)setInterfaceHiddenForVideoRecording:(bool)__unused hidden animated:(bool)__unused animated +{ +} + +- (void)setCameraMode:(PGCameraMode)mode +{ + PGCameraMode previousMode = _modeControl.cameraMode; + [_modeControl setCameraMode:mode animated:true]; + [self updateForCameraModeChangeWithPreviousMode:previousMode]; +} + +- (void)updateForCameraModeChangeWithPreviousMode:(PGCameraMode)__unused previousMode +{ + switch (_modeControl.cameraMode) + { + case PGCameraModePhoto: + case PGCameraModeSquare: + { + [_shutterButton setButtonMode:TGCameraShutterButtonNormalMode animated:true]; + [_timecodeView setHidden:true animated:true]; + } + break; + + case PGCameraModeVideo: + { + [_shutterButton setButtonMode:TGCameraShutterButtonVideoMode animated:true]; + [_timecodeView setHidden:false animated:true]; + } + break; + + case PGCameraModeClip: + { + [_shutterButton setButtonMode:TGCameraShutterButtonVideoMode animated:true]; + [_timecodeView setHidden:true animated:true]; + + } + break; + + default: + break; + } + + [_zoomView hideAnimated:true]; +} + +- (void)updateForCameraModeChangeAfterResize +{ +} + +- (void)setHasModeControl:(bool)hasModeControl +{ + if (!hasModeControl) + [_modeControl removeFromSuperview]; +} + +#pragma mark - Flash + +- (void)setHasFlash:(bool)__unused hasFlash +{ + +} + +- (void)setFlashMode:(PGCameraFlashMode)__unused mode +{ + +} + +- (void)setFlashActive:(bool)__unused active +{ + +} + +- (void)setFlashUnavailable:(bool)__unused unavailable +{ + +} + +#pragma mark - Actions + +- (void)setDoneButtonHidden:(bool)hidden animated:(bool)animated +{ + if (animated) + { + _doneButton.hidden = false; + [UIView animateWithDuration:0.3 animations:^ + { + _doneButton.alpha = hidden ? 0.0f : 1.0f; + } completion:^(BOOL finished) + { + if (finished) + _doneButton.hidden = hidden; + }]; + } + else + { + _doneButton.hidden = hidden; + _doneButton.alpha = hidden ? 0.0f : 1.0f; + } +} + +- (void)setShutterButtonHighlighted:(bool)highlighted +{ + [_shutterButton setHighlighted:highlighted]; +} + +- (void)setShutterButtonEnabled:(bool)enabled +{ + [_shutterButton setEnabled:enabled animated:true]; +} + +- (void)shutterButtonPressed +{ + if (self.shutterPressed != nil) + self.shutterPressed(false); +} + +- (void)shutterButtonReleased +{ + if (self.shutterReleased != nil) + self.shutterReleased(false); +} + +- (void)cancelButtonPressed +{ + if (self.cancelPressed != nil) + self.cancelPressed(); +} + +- (void)doneButtonPressed +{ + [_photoCounterButton setSelected:false animated:true]; + [_selectedPhotosView setHidden:true animated:true]; + + if (self.resultPressed != nil) + self.resultPressed(-1); +} + +- (void)flipButtonPressed +{ + if (self.cameraFlipped != nil) + self.cameraFlipped(); +} + +#pragma mark - Zoom + +- (void)setZoomLevel:(CGFloat)zoomLevel displayNeeded:(bool)displayNeeded +{ + [_zoomView setZoomLevel:zoomLevel displayNeeded:displayNeeded]; +} + +- (void)zoomChangingEnded +{ + [_zoomView interactionEnded]; +} + +- (void)setHasZoom:(bool)hasZoom +{ + if (!hasZoom) + [_zoomView hideAnimated:true]; +} + +#pragma mark - Video + +- (void)setRecordingVideo:(bool)recordingVideo animated:(bool)animated +{ + [_shutterButton setButtonMode:recordingVideo ? TGCameraShutterButtonRecordingMode : TGCameraShutterButtonVideoMode animated:animated]; + if (recordingVideo) + { + [_timecodeView startRecording]; + } + else + { + [_timecodeView stopRecording]; + [_timecodeView reset]; + } + [self setInterfaceHiddenForVideoRecording:recordingVideo animated:animated]; +} + +#pragma mark - + +- (UIInterfaceOrientation)interfaceOrientation +{ + return _interfaceOrientation; +} + +- (void)setInterfaceOrientation:(UIInterfaceOrientation)orientation animated:(bool)__unused animated +{ + _interfaceOrientation = orientation; +} + +#pragma mark - + +- (void)showMomentCaptureDismissWarningWithCompletion:(void (^)(bool dismiss))completion +{ + if (completion != nil) + completion(true); +} + +- (void)layoutPreviewRelativeViews +{ + +} + +#pragma mark - + +- (void)setDocumentFrameHidden:(bool)hidden +{ + +} + +- (void)setThumbnailSignalForItem:(SSignal *(^)(id))thumbnailSignalForItem +{ + [_selectedPhotosView setThumbnailSignalForItem:thumbnailSignalForItem]; +} + +- (void)setSelectedItemsModel:(TGMediaPickerGallerySelectedItemsModel *)selectedItemsModel +{ + _selectedPhotosView.selectedItemsModel = selectedItemsModel; + [_selectedPhotosView reloadData]; + + if (selectedItemsModel != nil && _selectedPhotosView != nil) + _photoCounterButton.userInteractionEnabled = true; +} + +- (void)photoCounterButtonPressed +{ + [_photoCounterButton setSelected:!_photoCounterButton.selected animated:true]; + [_selectedPhotosView setHidden:!_photoCounterButton.selected animated:true]; +} + +- (void)updateSelectionInterface:(NSUInteger)selectedCount counterVisible:(bool)counterVisible animated:(bool)animated +{ + if (counterVisible) + { + bool animateCount = animated && !(counterVisible && _photoCounterButton.internalHidden); + [_photoCounterButton setSelectedCount:selectedCount animated:animateCount]; + [_photoCounterButton setInternalHidden:false animated:animated completion:nil]; + } + else + { + bool animate = animated || (selectedCount == 0 && !counterVisible); + __weak TGMediaPickerPhotoCounterButton *weakButton = _photoCounterButton; + [_photoCounterButton setInternalHidden:true animated:animate completion:^ + { + __strong TGMediaPickerPhotoCounterButton *strongButton = weakButton; + if (strongButton != nil) + { + strongButton.selected = false; + [strongButton setSelectedCount:selectedCount animated:false]; + } + }]; + [_selectedPhotosView setHidden:true animated:animated]; + } +} + +- (void)updateSelectedPhotosView:(bool)reload incremental:(bool)incremental add:(bool)add index:(NSInteger)index +{ + if (_selectedPhotosView == nil) + return; + + if (!reload) + return; + + if (incremental) + { + if (add) + [_selectedPhotosView insertItemAtIndex:index]; + else + [_selectedPhotosView deleteItemAtIndex:index]; + } + else + { + [_selectedPhotosView reloadData]; + } +} + +- (void)setEditingContext:(TGMediaEditingContext *)editingContext +{ + _selectedPhotosView.editingContext = editingContext; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGCameraModeControl.h b/submodules/LegacyComponents/LegacyComponents/TGCameraModeControl.h new file mode 100644 index 0000000000..3aa666a8b0 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGCameraModeControl.h @@ -0,0 +1,13 @@ +#import +#import + +@interface TGCameraModeControl : UIControl + +@property (nonatomic, copy) void(^modeChanged)(PGCameraMode mode, PGCameraMode previousMode); + +@property (nonatomic, assign) PGCameraMode cameraMode; +- (void)setCameraMode:(PGCameraMode)mode animated:(bool)animated; + +- (void)setHidden:(bool)hidden animated:(bool)animated; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGCameraModeControl.m b/submodules/LegacyComponents/LegacyComponents/TGCameraModeControl.m new file mode 100644 index 0000000000..e5ad19e217 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGCameraModeControl.m @@ -0,0 +1,289 @@ +#import "TGCameraModeControl.h" + +#import "LegacyComponentsInternal.h" +#import "TGCameraInterfaceAssets.h" + +#import "UIControl+HitTestEdgeInsets.h" + +const CGFloat TGCameraModeControlVerticalInteritemSpace = 29.0f; + +@interface TGCameraModeControl () +{ + UIImageView *_dotView; + UIControl *_wrapperView; + + CGFloat _kerning; + NSArray *_buttons; + + UIView *_maskView; + CAGradientLayer *_maskLayer; +} +@end + +@implementation TGCameraModeControl + +- (instancetype)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + if (self != nil) + { + static UIImage *dotImage = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^ + { + UIGraphicsBeginImageContextWithOptions(CGSizeMake(6, 6), false, 0.0f); + CGContextRef context = UIGraphicsGetCurrentContext(); + + CGContextSetFillColorWithColor(context, [TGCameraInterfaceAssets accentColor].CGColor); + CGContextFillEllipseInRect(context, CGRectMake(0, 0, 6, 6)); + + dotImage = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + }); + + _dotView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 6, 6)]; + _dotView.image = dotImage; + //[self addSubview:_dotView]; + + if (frame.size.width > frame.size.height) + _kerning = 3.5f; + else + _kerning = 2.0f; + + _maskView = [[UIView alloc] initWithFrame:self.bounds]; + _maskView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + [self addSubview:_maskView]; + + _wrapperView = [[UIControl alloc] initWithFrame:CGRectZero]; + _wrapperView.backgroundColor = [UIColor clearColor]; + _wrapperView.hitTestEdgeInsets = UIEdgeInsetsMake(-10, -10, -10, -10); + _wrapperView.opaque = false; + [_maskView addSubview:_wrapperView]; + + _buttons = @ + [ + [self _createButtonForMode:PGCameraModeVideo title:TGLocalized(@"Camera.VideoMode")], + [self _createButtonForMode:PGCameraModePhoto title:TGLocalized(@"Camera.PhotoMode")], + // [self _createButtonForMode:PGCameraModeSquare title:TGLocalized(@"Camera.SquareMode")], + // [self _createButtonForMode:PGCameraModeClip title:TGLocalized(@"Camera.MomentMode")] + ]; + + for (UIButton *button in _buttons) + [_wrapperView addSubview:button]; + + if (frame.size.width > frame.size.height) + { + CGFloat leftOffset = 0; + for (UIButton *button in _buttons) + { + button.frame = CGRectMake(leftOffset, 0, CGFloor(button.frame.size.width), 20.0f); + leftOffset += button.frame.size.width + [TGCameraModeControl _buttonHorizontalSpacing]; + } + + _wrapperView.frame = CGRectMake(0, 0, leftOffset - [TGCameraModeControl _buttonHorizontalSpacing], 20); + + _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.startPoint = CGPointMake(0.0f, 0.5f); + _maskLayer.endPoint = CGPointMake(1.0f, 0.5f); + _maskView.layer.mask = _maskLayer; + } + else + { + CGFloat topOffset = 0; + for (UIButton *button in _buttons) + { + button.frame = CGRectMake(0, topOffset, CGFloor(button.frame.size.width), CGFloor(button.frame.size.height)); + topOffset += button.frame.size.height + TGCameraModeControlVerticalInteritemSpace; + } + + _wrapperView.frame = CGRectMake(33, 0, self.frame.size.width, topOffset - TGCameraModeControlVerticalInteritemSpace); + } + + self.cameraMode = PGCameraModePhoto; + } + return self; +} + ++ (UIFont *)_buttonFont +{ + return [UIFont fontWithName:@"SFCompactText-Regular" size:14]; +} + ++ (CGFloat)_buttonHorizontalSpacing +{ + //return 22; + return 19; +} + ++ (CGFloat)_buttonVerticalSpacing +{ + return 19; +} + +- (UIButton *)_createButtonForMode:(PGCameraMode)mode title:(NSString *)title +{ + UIButton *button = [[UIButton alloc] initWithFrame:CGRectMake(0, 0, 64, 20)]; + button.backgroundColor = [UIColor clearColor]; + 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:[button attributedTitleForState:UIControlStateSelected] forState:UIControlStateHighlighted | UIControlStateSelected]; + [button sizeToFit]; + [button addTarget:self action:@selector(buttonPressed:) forControlEvents:UIControlEventTouchUpInside]; + + return button; +} + +- (void)setCameraMode:(PGCameraMode)mode +{ + _cameraMode = mode; + [self setCameraMode:mode animated:false]; +} + +- (void)setCameraMode:(PGCameraMode)mode animated:(bool)animated +{ + _cameraMode = mode; + + CGFloat targetPosition = 0; + CGRect targetFrame = CGRectZero; + + if (self.frame.size.width > self.frame.size.height) + { + targetPosition = [self _buttonForMode:self.cameraMode].center.x - _wrapperView.frame.size.width / 2; + targetFrame = CGRectMake((self.frame.size.width - _wrapperView.frame.size.width) / 2 - targetPosition + 1, (self.frame.size.height - _wrapperView.frame.size.height) / 2, _wrapperView.frame.size.width, _wrapperView.frame.size.height); + } + else + { + targetPosition = [self _buttonForMode:self.cameraMode].center.y - _wrapperView.frame.size.height / 2; + targetFrame = CGRectMake(33, (self.frame.size.height - _wrapperView.frame.size.height) / 2 - targetPosition + 1, _wrapperView.frame.size.width, _wrapperView.frame.size.height); + } + + if (animated) + { + self.userInteractionEnabled = false; + [self _updateButtonsHighlight]; + + [UIView animateWithDuration:0.3f delay:0.0f options:UIViewAnimationOptionCurveEaseInOut animations:^ + { + _wrapperView.frame = targetFrame; + + if (self.frame.size.width > self.frame.size.height) + [self _layoutItemTransformationsForTargetFrame:targetFrame]; + } completion:^(BOOL finished) + { + if (finished) + self.userInteractionEnabled = true; + }]; + } + else + { + [self _updateButtonsHighlight]; + + if (self.frame.size.width > self.frame.size.height) + [self _layoutItemTransformationsForTargetFrame:targetFrame]; + + _wrapperView.frame = targetFrame; + } +} + +- (void)_layoutItemTransformationsForTargetFrame:(CGRect)targetFrame +{ + CGFloat targetCenter = targetFrame.origin.x - self.frame.size.width / 2; + + for (UIButton *button in _buttons) + button.layer.transform = [self _transformForItemWithOffset:targetCenter + button.center.x]; +} + +- (CATransform3D)_transformForItemWithOffset:(CGFloat)offset +{ + CGFloat angle = ABS(offset / _wrapperView.frame.size.width * 0.99f); + CGFloat sign = offset > 0 ? 1.0f : -1.0f; + + CATransform3D transform = CATransform3DTranslate(CATransform3DIdentity, -28 * angle * angle * sign, 0.0f, 0.0f); + transform = CATransform3DRotate(transform, angle, 0.0f, sign, 0.0f); + return transform; +} + +- (UIButton *)_currentModeButton +{ + return [self _buttonForMode:_cameraMode]; +} + +- (UIButton *)_buttonForMode:(PGCameraMode)mode +{ + for (UIButton *button in _wrapperView.subviews) + { + if (button.tag == mode) + return button; + } + + return nil; +} + +- (void)_updateButtonsHighlight +{ + for (UIButton *button in _buttons) + button.selected = (_cameraMode == button.tag); +} + +- (void)buttonPressed:(UIButton *)sender +{ + PGCameraMode previousMode = self.cameraMode; + [self setCameraMode:(int)sender.tag animated:true]; + + if ((PGCameraMode)sender.tag != previousMode && self.modeChanged != nil) + self.modeChanged((PGCameraMode)sender.tag, previousMode); +} + +- (void)setHidden:(BOOL)hidden +{ + self.alpha = hidden ? 0.0f : 1.0f; + super.hidden = hidden; +} + +- (void)setHidden:(bool)hidden animated:(bool)animated +{ + if (animated) + { + super.hidden = false; + self.userInteractionEnabled = false; + + [UIView animateWithDuration:0.25f animations:^ + { + self.alpha = hidden ? 0.0f : 1.0f; + } completion:^(BOOL finished) + { + self.userInteractionEnabled = true; + + if (finished) + self.hidden = hidden; + }]; + } + else + { + self.alpha = hidden ? 0.0f : 1.0f; + super.hidden = hidden; + } +} + +#pragma mark - Layout + +- (void)layoutSubviews +{ + if (self.frame.size.width > self.frame.size.height) + { + _dotView.frame = CGRectMake((self.frame.size.width - _dotView.frame.size.width) / 2, self.frame.size.height / 2 - 12, _dotView.frame.size.width, _dotView.frame.size.height); + _maskLayer.frame = CGRectMake(0, 0, _maskView.frame.size.width, _maskView.frame.size.height); + } + else + { + _dotView.frame = CGRectMake(13, (self.frame.size.height - _dotView.frame.size.height) / 2, _dotView.frame.size.width, _dotView.frame.size.height); + } +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGCameraPhotoPreviewController.h b/submodules/LegacyComponents/LegacyComponents/TGCameraPhotoPreviewController.h new file mode 100644 index 0000000000..e6fc992961 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGCameraPhotoPreviewController.h @@ -0,0 +1,31 @@ +#import +#import + +@class PGCameraShotMetadata; +@class PGPhotoEditorValues; +@class TGSuggestionContext; + +@interface TGCameraPhotoPreviewController : TGOverlayController + +@property (nonatomic, assign) bool allowCaptions; + +@property (nonatomic, copy) CGRect(^beginTransitionIn)(void); +@property (nonatomic, copy) CGRect(^beginTransitionOut)(CGRect referenceFrame); + +@property (nonatomic, copy) void(^finishedTransitionIn)(void); + +@property (nonatomic, copy) void (^photoEditorShown)(void); +@property (nonatomic, copy) void (^photoEditorHidden)(void); + +@property (nonatomic, copy) void(^retakePressed)(void); +@property (nonatomic, copy) void(^sendPressed)(TGOverlayController *controller, UIImage *resultImage, NSString *caption, NSArray *entities, NSArray *stickers, NSNumber *timer); + +@property (nonatomic, strong) TGSuggestionContext *suggestionContext; +@property (nonatomic, assign) bool shouldStoreAssets; +@property (nonatomic, assign) bool hasTimer; + +- (instancetype)initWithContext:(id)context image:(UIImage *)image metadata:(PGCameraShotMetadata *)metadata recipientName:(NSString *)recipientName saveCapturedMedia:(bool)saveCapturedMedia saveEditedPhotos:(bool)saveEditedPhotos; +- (instancetype)initWithContext:(id)context image:(UIImage *)image metadata:(PGCameraShotMetadata *)metadata recipientName:(NSString *)recipientName backButtonTitle:(NSString *)backButtonTitle doneButtonTitle:(NSString *)doneButtonTitle saveCapturedMedia:(bool)saveCapturedMedia saveEditedPhotos:(bool)saveEditedPhotos; + + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGCameraPhotoPreviewController.m b/submodules/LegacyComponents/LegacyComponents/TGCameraPhotoPreviewController.m new file mode 100644 index 0000000000..aa650c39ee --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGCameraPhotoPreviewController.m @@ -0,0 +1,1193 @@ +#import "TGCameraPhotoPreviewController.h" + +#import "LegacyComponentsInternal.h" + +#import + +#import +#import +#import +#import + +#import "TGImageView.h" +#import + +#import +#import "TGPhotoEditorController.h" +#import "TGPhotoEditorTabController.h" +#import "TGPhotoToolbarView.h" +#import "TGPhotoEditorButton.h" +#import + +#import "TGSecretTimerMenu.h" + +#import +#import +#import + +#import "TGPhotoEditorInterfaceAssets.h" +#import "TGPhotoCaptionInputMixin.h" + +@interface TGCameraPhotoPreviewWrapperView : UIView + +@end + +@implementation TGCameraPhotoPreviewWrapperView + +- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event +{ + UIView *view = [super hitTest:point withEvent:event]; + if (view != self) + return view; + + return nil; +} + +@end + +@interface TGCameraPhotoPreviewController () +{ + TGMediaEditingContext *_editingContext; + + UIImage *_image; + PGCameraShotMetadata *_metadata; + + TGCameraPhotoPreviewWrapperView *_wrapperView; + UIView *_transitionParentView; + TGModernGalleryZoomableScrollView *_scrollView; + TGImageView *_imageView; + UIView *_temporaryRepView; + CGSize _imageSize; + + UIImageView *_arrowView; + UILabel *_recipientLabel; + + TGPhotoToolbarView *_portraitToolbarView; + TGPhotoToolbarView *_landscapeToolbarView; + + bool _transitionInProgress; + bool _dismissing; + bool _appeared; + + NSString *_recipientName; + NSString *_backButtonTitle; + NSString *_doneButtonTitle; + + TGPhotoCaptionInputMixin *_captionMixin; + CGFloat _scrollViewVerticalOffset; + + bool _saveCapturedMedia; + bool _saveEditedPhotos; + + id _context; +} + +@property (nonatomic, weak) TGPhotoEditorController *editorController; + +@end + +@implementation TGCameraPhotoPreviewController + +- (instancetype)initWithContext:(id)context image:(UIImage *)image metadata:(PGCameraShotMetadata *)metadata recipientName:(NSString *)recipientName saveCapturedMedia:(bool)saveCapturedMedia saveEditedPhotos:(bool)saveEditedPhotos +{ + return [self initWithContext:context image:image metadata:metadata recipientName:recipientName backButtonTitle:TGLocalized(@"Camera.Retake") doneButtonTitle:TGLocalized(@"MediaPicker.Send") saveCapturedMedia:saveCapturedMedia saveEditedPhotos:saveEditedPhotos]; +} + +- (instancetype)initWithContext:(id)context image:(UIImage *)image metadata:(PGCameraShotMetadata *)metadata recipientName:(NSString *)recipientName backButtonTitle:(NSString *)backButtonTitle doneButtonTitle:(NSString *)doneButtonTitle saveCapturedMedia:(bool)saveCapturedMedia saveEditedPhotos:(bool)saveEditedPhotos +{ + self = [super initWithContext:context]; + if (self != nil) + { + _context = context; + _image = image; + _metadata = metadata; + _imageSize = image.size; + _recipientName = recipientName; + + _editingContext = [[TGMediaEditingContext alloc] init]; + + self.automaticallyManageScrollViewInsets = false; + + _backButtonTitle = backButtonTitle; + _doneButtonTitle = doneButtonTitle; + + _saveCapturedMedia = saveCapturedMedia; + _saveEditedPhotos = saveEditedPhotos; + } + return self; +} + +- (void)loadView +{ + [super loadView]; + object_setClass(self.view, [TGFullscreenContainerView class]); + + if (iosMajorVersion() >= 11) + self.view.accessibilityIgnoresInvertColors = true; + + self.view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + self.view.backgroundColor = [UIColor clearColor]; + + _transitionParentView = [[UIView alloc] initWithFrame:self.view.bounds]; + _transitionParentView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + [self.view addSubview:_transitionParentView]; + + CGRect containerFrame = self.view.bounds; + CGSize fittedSize = TGScaleToSize(_image.size, containerFrame.size); + + _scrollView = [[TGModernGalleryZoomableScrollView alloc] initWithFrame:self.view.bounds]; + _scrollView.clipsToBounds = false; + _scrollView.delegate = self; + _scrollView.showsHorizontalScrollIndicator = false; + _scrollView.showsVerticalScrollIndicator = false; + [self.view addSubview:_scrollView]; + + _imageView = [[TGImageView alloc] initWithFrame:CGRectMake(0, 0, fittedSize.width, fittedSize.height)]; + [self.view addSubview:_imageView]; + + __weak TGCameraPhotoPreviewController *weakSelf = self; + void (^fadeOutRepView)(void) = ^ + { + __strong TGCameraPhotoPreviewController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + if (strongSelf->_temporaryRepView == nil) + return; + + UIView *repView = strongSelf->_temporaryRepView; + strongSelf->_temporaryRepView = nil; + [UIView animateWithDuration:0.2f animations:^ + { + repView.alpha = 0.0f; + } completion:^(__unused BOOL finished) + { + [repView removeFromSuperview]; + }]; + }; + + TGMediaEditingContext *editingContext = _editingContext; + + SSignal *assetSignal = [SSignal single:_image]; + SSignal *imageSignal = assetSignal; + if (editingContext != nil) + { + imageSignal = [[[editingContext imageSignalForItem:_image] deliverOn:[SQueue mainQueue]] mapToSignal:^SSignal *(id result) + { + __strong TGCameraPhotoPreviewController *strongSelf = weakSelf; + if (strongSelf == nil) + return [SSignal complete]; + + if (result == nil) + { + return [[assetSignal deliverOn:[SQueue mainQueue]] afterNext:^(__unused id next) + { + fadeOutRepView(); + }]; + } + else if ([result isKindOfClass:[UIView class]]) + { + [strongSelf _setTemporaryRepView:result]; + return [[SSignal single:nil] deliverOn:[SQueue mainQueue]]; + } + else + { + return [[[SSignal single:result] deliverOn:[SQueue mainQueue]] afterNext:^(__unused id next) + { + fadeOutRepView(); + }]; + } + }]; + } + + [_imageView setSignal:[[imageSignal deliverOn:[SQueue mainQueue]] afterNext:^(id next) + { + __strong TGCameraPhotoPreviewController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + if ([next isKindOfClass:[UIImage class]]) + strongSelf->_imageSize = ((UIImage *)next).size; + + [strongSelf reset]; + }]]; + + _wrapperView = [[TGCameraPhotoPreviewWrapperView alloc] initWithFrame:CGRectZero]; + [self.view addSubview:_wrapperView]; + + void (^cancelPressed)(void) = ^ + { + __strong TGCameraPhotoPreviewController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + if (strongSelf.retakePressed != nil) + strongSelf.retakePressed(); + + [strongSelf transitionOutWithCompletion:^ + { + [strongSelf dismiss]; + }]; + }; + + void (^donePressed)(void) = ^ + { + __strong TGCameraPhotoPreviewController *strongSelf = weakSelf; + if (strongSelf == nil || strongSelf->_dismissing) + return; + + [strongSelf.view.window endEditing:true]; + + strongSelf->_dismissing = true; + strongSelf.view.userInteractionEnabled = false; + + if (strongSelf.shouldStoreAssets && [strongSelf->_editingContext timerForItem:strongSelf->_image] == nil) + { + if (strongSelf->_saveCapturedMedia) + [[[TGMediaAssetsLibrary sharedLibrary] saveAssetWithImage:strongSelf->_image] startWithNext:nil]; + + if (strongSelf->_saveEditedPhotos) + { + [[[[[[editingContext fullSizeImageUrlForItem:strongSelf->_image] filter:^bool(id result) + { + return [result isKindOfClass:[NSURL class]]; + }] startOn:[SQueue concurrentDefaultQueue]] deliverOn:[SQueue mainQueue]] mapToSignal:^SSignal *(NSURL *url) + { + return [[[TGMediaAssetsLibrary sharedLibrary] saveAssetWithImageAtUrl:url] onCompletion:^ + { + __strong TGMediaEditingContext *strongEditingContext = editingContext; + [strongEditingContext description]; + }]; + }] startWithNext:nil]; + } + } + + SSignal *originalSignal = [[[SSignal single:strongSelf->_image] map:^id(UIImage *image) + { + return TGPhotoEditorCrop(image, nil, UIImageOrientationUp, 0, CGRectMake(0, 0, image.size.width, image.size.height), false, CGSizeMake(1280, 1280), image.size, true); + }] startOn:[SQueue concurrentDefaultQueue]]; + + SSignal *imageSignal = originalSignal; + if (editingContext != nil) + { + imageSignal = [[[[editingContext imageSignalForItem:strongSelf->_image withUpdates:true] filter:^bool(id result) + { + return result == nil || ([result isKindOfClass:[UIImage class]] && !((UIImage *)result).degraded); + }] take:1] mapToSignal:^SSignal *(id result) + { + if (result == nil) + { + return originalSignal; + } + else if ([result isKindOfClass:[UIImage class]]) + { + UIImage *image = (UIImage *)result; + image.edited = true; + return [SSignal single:image]; + } + + return [SSignal complete]; + }]; + } + + NSString *caption = [editingContext captionForItem:strongSelf->_image]; + NSArray *entities = [editingContext entitiesForItem:strongSelf->_image]; + NSArray *stickers = [editingContext adjustmentsForItem:strongSelf->_image].paintingData.stickers; + NSNumber *timer = [editingContext timerForItem:strongSelf->_image]; + [[imageSignal deliverOn:[SQueue mainQueue]] startWithNext:^(UIImage *result) + { + strongSelf.sendPressed(self, result, caption, entities, stickers, timer); + strongSelf.view.userInteractionEnabled = true; + strongSelf->_dismissing = false; + }]; + }; + + void (^tabPressed)(TGPhotoEditorTab) = ^(TGPhotoEditorTab tab) + { + __strong TGCameraPhotoPreviewController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + if (tab == TGPhotoEditorTimerTab) + [strongSelf openTimerSetup]; + else + [strongSelf presentPhotoEditorWithTab:tab]; + }; + + TGPhotoEditorTab tabs = TGPhotoEditorCropTab; + if (iosMajorVersion() >= 7) + { + tabs |= TGPhotoEditorPaintTab; + tabs |= TGPhotoEditorToolsTab; + } + + if (self.hasTimer) + tabs |= TGPhotoEditorTimerTab; + + _captionMixin = [[TGPhotoCaptionInputMixin alloc] initWithKeyCommandController:[_context keyCommandController]]; + _captionMixin.panelParentView = ^UIView * + { + __strong TGCameraPhotoPreviewController *strongSelf = weakSelf; + return strongSelf->_wrapperView; + }; + + _captionMixin.panelFocused = ^ + { + __strong TGCameraPhotoPreviewController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [strongSelf setInterfaceHidden:true animated:true]; + }; + + _captionMixin.finishedWithCaption = ^(NSString *caption, NSArray *entities) + { + __strong TGCameraPhotoPreviewController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [strongSelf->_editingContext setCaption:caption entities:entities forItem:strongSelf->_image]; + + PGPhotoEditorValues *values = (PGPhotoEditorValues *)[strongSelf->_editingContext adjustmentsForItem:strongSelf->_image]; + [strongSelf updateEditorButtonsForEditorValues:values]; + + [strongSelf setInterfaceHidden:false animated:true]; + }; + + _captionMixin.keyboardHeightChanged = ^(CGFloat keyboardHeight, NSTimeInterval duration, NSInteger animationCurve) + { + __strong TGCameraPhotoPreviewController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + CGFloat offset = 0.0f; + if (keyboardHeight > 0) + offset = -keyboardHeight / 2.0f; + + [UIView animateWithDuration:duration delay:0.0f options:animationCurve animations:^ + { + [strongSelf setScrollViewVerticalOffset:offset]; + } completion:nil]; + }; + _captionMixin.suggestionContext = self.suggestionContext; + [_captionMixin createInputPanelIfNeeded]; + + _portraitToolbarView = [[TGPhotoToolbarView alloc] initWithBackButton:TGPhotoEditorBackButtonBack doneButton:TGPhotoEditorDoneButtonSend solidBackground:false]; + [_portraitToolbarView setToolbarTabs:tabs animated:false]; + _portraitToolbarView.cancelPressed = cancelPressed; + _portraitToolbarView.donePressed = donePressed; + _portraitToolbarView.tabPressed = tabPressed; + [_wrapperView addSubview:_portraitToolbarView]; + + _landscapeToolbarView = [[TGPhotoToolbarView alloc] initWithBackButton:TGPhotoEditorBackButtonBack doneButton:TGPhotoEditorDoneButtonSend solidBackground:false]; + [_landscapeToolbarView setToolbarTabs:tabs animated:false]; + _landscapeToolbarView.cancelPressed = cancelPressed; + _landscapeToolbarView.donePressed = donePressed; + _landscapeToolbarView.tabPressed = tabPressed; + [_wrapperView addSubview:_landscapeToolbarView]; + + if (_recipientName.length > 0) + { + _arrowView = [[UIImageView alloc] initWithImage:TGComponentsImageNamed(@"PhotoPickerArrow")]; + _arrowView.alpha = 0.45f; + [_wrapperView addSubview:_arrowView]; + + _recipientLabel = [[UILabel alloc] init]; + _recipientLabel.backgroundColor = [UIColor clearColor]; + _recipientLabel.font = TGBoldSystemFontOfSize(13.0f); + _recipientLabel.textColor = UIColorRGBA(0xffffff, 0.45f); + _recipientLabel.text = _recipientName; + _recipientLabel.userInteractionEnabled = false; + [_recipientLabel sizeToFit]; + [_wrapperView addSubview:_recipientLabel]; + } +} + +- (void)openTimerSetup +{ + id editableMediaItem = _image; + + NSString *description = TGLocalized(@"SecretTimer.ImageDescription"); + + NSString *lastValueKey = @"mediaPickerLastTimerValue_v0"; + NSNumber *value = [_editingContext timerForItem:editableMediaItem]; + if (value == nil) + value = [[NSUserDefaults standardUserDefaults] objectForKey:lastValueKey]; + + __strong TGCameraPhotoPreviewController *weakSelf = self; + [TGSecretTimerMenu presentInParentController:self context:_context dark:true description:description values:[TGSecretTimerMenu secretMediaTimerValues] value:value completed:^(NSNumber *value) + { + __strong TGCameraPhotoPreviewController *strongSelf = weakSelf; + if (strongSelf != nil) + { + if (value == nil) + [[NSUserDefaults standardUserDefaults] removeObjectForKey:lastValueKey]; + else + [[NSUserDefaults standardUserDefaults] setObject:value forKey:lastValueKey]; + + [strongSelf->_editingContext setTimer:value forItem:editableMediaItem]; + + PGPhotoEditorValues *values = (PGPhotoEditorValues *)[strongSelf->_editingContext adjustmentsForItem:strongSelf->_image]; + [strongSelf updateEditorButtonsForEditorValues:values]; + } + } dismissed:^ + { + __strong TGCameraPhotoPreviewController *strongSelf = weakSelf; + if (strongSelf != nil) + [strongSelf setAllInterfaceHidden:false animated:true]; + } sourceView:self.view sourceRect:^CGRect + { + __strong TGCameraPhotoPreviewController *strongSelf = weakSelf; + if (strongSelf == nil) + return CGRectZero; + + return [[strongSelf timerButton] convertRect:[strongSelf timerButton].bounds toView:strongSelf.view]; + }]; + + if (!TGIsPad()) + [self setAllInterfaceHidden:true animated:true]; +} + +- (UIView *)timerButton +{ + if (UIInterfaceOrientationIsPortrait(self.interfaceOrientation)) + return [_portraitToolbarView buttonForTab:TGPhotoEditorTimerTab]; + else + return [_landscapeToolbarView buttonForTab:TGPhotoEditorTimerTab]; +} + +- (void)_setTemporaryRepView:(UIView *)view +{ + [_temporaryRepView removeFromSuperview]; + _temporaryRepView = view; + + _imageSize = TGScaleToSize(view.frame.size, self.view.frame.size); + + view.hidden = _imageView.hidden; + view.frame = CGRectMake((self.view.frame.size.width - _imageSize.width) / 2.0f, (self.view.frame.size.height - _imageSize.height) / 2.0f, _imageSize.width, _imageSize.height); + + [self.view insertSubview:view belowSubview:_wrapperView]; +} + +- (void)setInterfaceHidden:(bool)hidden animated:(bool)animated +{ + CGFloat alpha = (hidden ? 0.0f : 1.0f); + if (animated) + { + [UIView animateWithDuration:0.2 delay:0.0 options:UIViewAnimationOptionCurveLinear | UIViewAnimationOptionBeginFromCurrentState animations:^ + { + _arrowView.alpha = alpha * 0.45f; + _recipientLabel.alpha = alpha; + } completion:nil]; + } + else + { + _arrowView.alpha = alpha * 0.45f; + _recipientLabel.alpha = alpha; + } +} + +- (void)setAllInterfaceHidden:(bool)hidden animated:(bool)animated +{ + CGFloat alpha = (hidden ? 0.0f : 1.0f); + if (animated) + { + [UIView animateWithDuration:0.2 delay:0.0 options:UIViewAnimationOptionCurveLinear | UIViewAnimationOptionBeginFromCurrentState animations:^ + { + _arrowView.alpha = alpha * 0.45f; + _recipientLabel.alpha = alpha; + _portraitToolbarView.alpha = alpha; + _landscapeToolbarView.alpha = alpha; + _captionMixin.inputPanel.alpha = alpha; + } completion:^(BOOL finished) + { + if (finished) + { + _portraitToolbarView.userInteractionEnabled = !hidden; + _landscapeToolbarView.userInteractionEnabled = !hidden; + _captionMixin.inputPanel.userInteractionEnabled = !hidden; + } + }]; + } + else + { + _arrowView.alpha = alpha * 0.45f; + _recipientLabel.alpha = alpha; + + _portraitToolbarView.alpha = alpha; + _portraitToolbarView.userInteractionEnabled = !hidden; + + _landscapeToolbarView.alpha = alpha; + _landscapeToolbarView.userInteractionEnabled = !hidden; + + _captionMixin.inputPanel.alpha = alpha; + _captionMixin.inputPanel.userInteractionEnabled = !hidden; + } +} + +- (void)dismiss +{ + if (self.navigationController != nil) + { + TGOverlayController *parentController = (TGOverlayController *)self.navigationController.parentViewController; + [parentController dismiss]; + } + else if (self.overlayWindow != nil) + { + [super dismiss]; + } + else + { + [self.view removeFromSuperview]; + [self removeFromParentViewController]; + } +} + +- (UIRectEdge)preferredScreenEdgesDeferringSystemGestures +{ + if (self.childViewControllers.count > 0) + return [self.childViewControllers.lastObject preferredScreenEdgesDeferringSystemGestures]; + + return [super preferredScreenEdgesDeferringSystemGestures]; +} + +- (BOOL)prefersStatusBarHidden +{ + if (self.childViewControllers.count > 0) + return [self.childViewControllers.lastObject prefersStatusBarHidden]; + + return [super prefersStatusBarHidden]; +} + +- (UIBarStyle)requiredNavigationBarStyle +{ + return UIBarStyleDefault; +} + +- (bool)navigationBarShouldBeHidden +{ + return true; +} + +- (void)viewWillAppear:(BOOL)animated +{ + [super viewWillAppear:animated]; + + [self transitionIn]; +} + +#pragma mark - Transition + +- (void)transitionIn +{ + [_context setApplicationStatusBarAlpha:0.0f]; + + if (_appeared) + return; + + _appeared = true; + _transitionInProgress = true; + + _captionMixin.inputPanel.alpha = 0.0f; + _portraitToolbarView.alpha = 0.0f; + _landscapeToolbarView.alpha = 0.0f; + _arrowView.alpha = 0.0f; + _recipientLabel.alpha = 0.0f; + + [UIView animateWithDuration:0.3f delay:0.1f options:UIViewAnimationOptionCurveLinear animations:^ + { + _captionMixin.inputPanel.alpha = 1.0f; + _portraitToolbarView.alpha = 1.0f; + _landscapeToolbarView.alpha = 1.0f; + _arrowView.alpha = 0.45f; + _recipientLabel.alpha = 1.0f; + } completion:nil]; + + CGSize referenceSize = [self referenceViewSizeForOrientation:self.interfaceOrientation]; + CGRect referenceFrame = CGRectZero; + if (self.beginTransitionIn != nil) + referenceFrame = self.beginTransitionIn(); + + 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); + } + + CGRect containerFrame = CGRectMake(0, 0, referenceSize.width, referenceSize.height); + CGSize fittedSize = TGScaleToSize(_imageView.image.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); + + CGFloat referenceAspectRatio = referenceFrame.size.width / referenceFrame.size.height; + CGFloat targetAspectRatio = targetFrame.size.width / targetFrame.size.height; + + if (ABS(targetAspectRatio - referenceAspectRatio) > 0.03f) + { + CGSize newSize = CGSizeZero; + if (referenceFrame.size.width > referenceFrame.size.height) + newSize = CGSizeMake(referenceFrame.size.width, _imageView.image.size.height * referenceFrame.size.width / _imageView.image.size.width); + else + newSize = CGSizeMake(_imageView.image.size.width * referenceFrame.size.height / _imageView.image.size.height, referenceFrame.size.height); + + referenceFrame = CGRectMake(CGRectGetMidX(referenceFrame) - newSize.width / 2, + CGRectGetMidY(referenceFrame) - newSize.height / 2, + newSize.width, newSize.height); + } + + _imageView.frame = referenceFrame; + + POPSpringAnimation *animation = [TGPhotoEditorAnimation prepareTransitionAnimationForPropertyNamed:kPOPViewFrame]; + animation.fromValue = [NSValue valueWithCGRect:referenceFrame]; + animation.toValue = [NSValue valueWithCGRect:targetFrame]; + animation.completionBlock = ^(__unused POPAnimation *animation, __unused BOOL finished) + { + _transitionInProgress = false; + [_scrollView addSubview:_imageView]; + _imageView.frame = CGRectMake(0, 0, _scrollView.frame.size.width, _scrollView.frame.size.height); + self.view.backgroundColor = [UIColor blackColor]; + + [self reset]; + + if (self.finishedTransitionIn != nil) + self.finishedTransitionIn(); + }; + + [_imageView pop_addAnimation:animation forKey:@"frame"]; +} + +- (void)transitionOutWithCompletion:(void (^)(void))completion +{ + _transitionInProgress = true; + + self.view.backgroundColor = [UIColor clearColor]; + + CGRect frame = [self.view convertRect:_imageView.frame fromView:_scrollView]; + [self.view addSubview:_imageView]; + _imageView.frame = frame; + + CGSize referenceSize = [self referenceViewSizeForOrientation:self.interfaceOrientation]; + CGRect referenceFrame = _imageView.frame; + + if (self.interfaceOrientation == UIInterfaceOrientationLandscapeLeft) + { + referenceFrame = CGRectMake(referenceSize.height - 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.width - referenceFrame.size.width - referenceFrame.origin.x, + referenceFrame.size.height, referenceFrame.size.width); + } + + CGRect targetFrame = CGRectZero; + if (self.beginTransitionOut != nil) + targetFrame = self.beginTransitionOut(referenceFrame); + + if (self.interfaceOrientation == UIInterfaceOrientationLandscapeLeft) + { + targetFrame = CGRectMake(referenceSize.width - targetFrame.size.height - targetFrame.origin.y, + targetFrame.origin.x, + targetFrame.size.height, targetFrame.size.width); + } + else if (self.interfaceOrientation == UIInterfaceOrientationLandscapeRight) + { + targetFrame = CGRectMake(targetFrame.origin.y, + referenceSize.height - targetFrame.size.width - targetFrame.origin.x, + targetFrame.size.height, targetFrame.size.width); + } + + CGFloat referenceAspectRatio = referenceFrame.size.width / referenceFrame.size.height; + CGFloat targetAspectRatio = targetFrame.size.width / targetFrame.size.height; + + if (ABS(targetAspectRatio - referenceAspectRatio) > 0.03f) + { + CGSize newSize = CGSizeZero; + if (targetFrame.size.width > targetFrame.size.height) + newSize = CGSizeMake(targetFrame.size.width, _imageView.image.size.height * targetFrame.size.width / _imageView.image.size.width); + else + newSize = CGSizeMake(_imageView.image.size.width * targetFrame.size.height / _imageView.image.size.height, targetFrame.size.height); + + targetFrame = CGRectMake(CGRectGetMidX(targetFrame) - newSize.width / 2, + CGRectGetMidY(targetFrame) - newSize.height / 2, + newSize.width, newSize.height); + } + + POPSpringAnimation *animation = [TGPhotoEditorAnimation prepareTransitionAnimationForPropertyNamed:kPOPViewFrame]; + animation.fromValue = [NSValue valueWithCGRect:_imageView.frame]; + animation.toValue = [NSValue valueWithCGRect:targetFrame]; + [_imageView pop_addAnimation:animation forKey:@"frame"]; + + [UIView animateWithDuration:0.3f animations:^ + { + _imageView.alpha = 0.0f; + _portraitToolbarView.alpha = 0.0f; + _landscapeToolbarView.alpha = 0.0f; + _captionMixin.inputPanel.alpha = 0.0f; + _arrowView.alpha = 0.0f; + _recipientLabel.alpha = 0.0f; + } completion:^(__unused BOOL finished) + { + if (completion != nil) + completion(); + }]; +} + +#pragma mark - Scroll View + +- (void)scrollViewDidZoom:(UIScrollView *)__unused scrollView +{ + [self adjustZoom]; +} + +- (void)scrollViewDidEndZooming:(UIScrollView *)__unused scrollView withView:(UIView *)__unused view atScale:(CGFloat)__unused scale +{ + [self adjustZoom]; + + if (_scrollView.zoomScale < _scrollView.normalZoomScale - FLT_EPSILON) + { + [TGHacks setAnimationDurationFactor:0.5f]; + [_scrollView setZoomScale:_scrollView.normalZoomScale animated:true]; + [TGHacks setAnimationDurationFactor:1.0f]; + } +} + +- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView +{ + if (_imageView.superview == scrollView) + return _imageView; + + return nil; +} + +- (CGSize)contentSize +{ + return _imageSize; +} + +- (void)reset +{ + CGSize contentSize = [self contentSize]; + + _scrollView.minimumZoomScale = 1.0f; + _scrollView.maximumZoomScale = 1.0f; + _scrollView.normalZoomScale = 1.0f; + _scrollView.zoomScale = 1.0f; + _scrollView.contentSize = contentSize; + _imageView.frame = CGRectMake(0.0f, 0.0f, contentSize.width, contentSize.height); + + [self adjustZoom]; + _scrollView.zoomScale = _scrollView.normalZoomScale; +} + +- (void)adjustZoom +{ + CGSize contentSize = [self contentSize]; + CGSize boundsSize = _scrollView.frame.size; + if (contentSize.width < FLT_EPSILON || contentSize.height < FLT_EPSILON || boundsSize.width < FLT_EPSILON || boundsSize.height < FLT_EPSILON) + return; + + CGFloat scaleWidth = boundsSize.width / contentSize.width; + CGFloat scaleHeight = boundsSize.height / contentSize.height; + CGFloat minScale = MIN(scaleWidth, scaleHeight); + CGFloat maxScale = MAX(scaleWidth, scaleHeight); + maxScale = MAX(maxScale, minScale * 3.0f); + + if (ABS(maxScale - minScale) < 0.01f) + maxScale = minScale; + + if (_scrollView.minimumZoomScale != 0.05f) + _scrollView.minimumZoomScale = 0.05f; + if (_scrollView.normalZoomScale != minScale) + _scrollView.normalZoomScale = minScale; + if (_scrollView.maximumZoomScale != maxScale) + _scrollView.maximumZoomScale = maxScale; + + CGRect contentFrame = _imageView.frame; + + if (boundsSize.width > contentFrame.size.width) + contentFrame.origin.x = (boundsSize.width - contentFrame.size.width) / 2.0f; + else + contentFrame.origin.x = 0; + + if (boundsSize.height > contentFrame.size.height) + contentFrame.origin.y = (boundsSize.height - contentFrame.size.height) / 2.0f; + else + contentFrame.origin.y = 0; + + _imageView.frame = contentFrame; +} + +#pragma mark - + +- (void)updateEditorButtonsForEditorValues:(PGPhotoEditorValues *)editorValues +{ + TGPhotoEditorTab highlightedButtons = [TGPhotoEditorTabController highlightedButtonsForEditorValues:editorValues forAvatar:false]; + + TGPhotoEditorButton *timerButton = [_portraitToolbarView buttonForTab:TGPhotoEditorTimerTab]; + if (timerButton != nil) + { + NSInteger value = [[_editingContext timerForItem:_image] integerValue]; + + UIImage *defaultIcon = [TGPhotoEditorInterfaceAssets timerIconForValue:0]; + UIImage *icon = [TGPhotoEditorInterfaceAssets timerIconForValue:value]; + [timerButton setIconImage:defaultIcon activeIconImage:icon]; + + timerButton = [_landscapeToolbarView buttonForTab:TGPhotoEditorTimerTab]; + [timerButton setIconImage:defaultIcon activeIconImage:icon]; + + if (value > 0) + highlightedButtons |= TGPhotoEditorTimerTab; + } + + [_portraitToolbarView setEditButtonsHighlighted:highlightedButtons]; + [_landscapeToolbarView setEditButtonsHighlighted:highlightedButtons]; +} + +- (UIView *)transitionContentView +{ + if (_temporaryRepView != nil) + return _temporaryRepView; + + return _imageView; +} + +- (CGRect)transitionViewContentRect +{ + UIView *contentView = [self transitionContentView]; + return [self.view convertRect:contentView.frame fromView:contentView.superview]; +} + +- (void)presentPhotoEditorWithTab:(TGPhotoEditorTab)tab +{ + __weak TGCameraPhotoPreviewController *weakSelf = self; + + id editableMediaItem = _image; + + UIView *referenceView = [self transitionContentView]; + CGRect refFrame = [self transitionViewContentRect]; + UIImage *screenImage = nil; + if ([referenceView isKindOfClass:[UIImageView class]]) + screenImage = [(UIImageView *)referenceView image]; + + PGPhotoEditorValues *editorValues = (PGPhotoEditorValues *)[_editingContext adjustmentsForItem:_image]; + NSString *caption = [_editingContext captionForItem:_image]; + + TGPhotoEditorController *controller = [[TGPhotoEditorController alloc] initWithContext:_context item:editableMediaItem intent:TGPhotoEditorControllerFromCameraIntent adjustments:editorValues caption:caption screenImage:screenImage availableTabs:_portraitToolbarView.currentTabs selectedTab:tab]; + controller.editingContext = _editingContext; + self.editorController = controller; + controller.metadata = _metadata; + controller.suggestionContext = self.suggestionContext; + controller.didFinishRenderingFullSizeImage = ^(UIImage *image) + { + __strong TGCameraPhotoPreviewController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [strongSelf->_editingContext setFullSizeImage:image forItem:strongSelf->_image]; + }; + controller.willFinishEditing = ^(PGPhotoEditorValues *editorValues, id temporaryRep, bool hasChanges) + { + __strong TGCameraPhotoPreviewController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + if (hasChanges) + { + [strongSelf->_editingContext setAdjustments:editorValues forItem:strongSelf->_image]; + [strongSelf->_editingContext setTemporaryRep:temporaryRep forItem:strongSelf->_image]; + } + }; + controller.didFinishEditing = ^(PGPhotoEditorValues *editorValues, UIImage *resultImage, __unused UIImage *thumbnailImage, bool hasChanges) + { +#ifdef DEBUG + if (editorValues != nil && hasChanges) + NSAssert(resultImage != nil, @"resultImage should not be nil"); +#endif + + __strong TGCameraPhotoPreviewController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + if (hasChanges) + [strongSelf->_editingContext setImage:resultImage thumbnailImage:nil forItem:strongSelf->_image synchronous:false]; + + PGPhotoEditorValues *values = !hasChanges ? (PGPhotoEditorValues *)[strongSelf->_editingContext adjustmentsForItem:strongSelf->_image] : editorValues; + [strongSelf updateEditorButtonsForEditorValues:values]; + + [strongSelf reset]; + }; + + controller.captionSet = ^(NSString *caption, NSArray *entities) + { + __strong TGCameraPhotoPreviewController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [strongSelf reset]; + + [strongSelf->_editingContext setCaption:caption entities:entities forItem:strongSelf->_image]; + }; + + controller.requestToolbarsHidden = ^(bool hidden, bool animated) + { + __strong TGCameraPhotoPreviewController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [strongSelf setToolbarsHidden:hidden animated:animated]; + }; + + controller.beginTransitionIn = ^UIView *(CGRect *referenceFrame, UIView **parentView) + { + __strong TGCameraPhotoPreviewController *strongSelf = weakSelf; + if (strongSelf == nil) + return nil; + + [strongSelf editorTransitionIn]; + + if (strongSelf.photoEditorShown != nil) + strongSelf.photoEditorShown(); + + strongSelf->_imageView.hidden = true; + strongSelf->_temporaryRepView.hidden = true; + + *parentView = strongSelf->_transitionParentView; + *referenceFrame = refFrame; + + [strongSelf reset]; + + if (iosMajorVersion() >= 7) + [strongSelf setNeedsStatusBarAppearanceUpdate]; + else { + [_context setStatusBarHidden:true withAnimation:UIStatusBarAnimationNone]; + } + + return referenceView; + }; + + controller.beginTransitionOut = ^UIView *(CGRect *referenceFrame, UIView **parentView) + { + __strong TGCameraPhotoPreviewController *strongSelf = weakSelf; + if (strongSelf == nil) + return nil; + + [strongSelf editorTransitionOut]; + + *parentView = strongSelf->_transitionParentView; + *referenceFrame = [strongSelf transitionViewContentRect]; + + return [strongSelf transitionContentView]; + }; + + controller.finishedTransitionOut = ^(__unused bool saved) + { + __strong TGCameraPhotoPreviewController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + if (strongSelf.photoEditorHidden != nil) + strongSelf.photoEditorHidden(); + + strongSelf->_imageView.hidden = false; + strongSelf->_temporaryRepView.hidden = false; + + if (iosMajorVersion() >= 7) + [strongSelf setNeedsStatusBarAppearanceUpdate]; + else { + [_context setStatusBarHidden:false withAnimation:UIStatusBarAnimationNone]; + } + }; + + controller.requestThumbnailImage = ^(id editableItem) + { + return [editableItem thumbnailImageSignal]; + }; + + controller.requestOriginalScreenSizeImage = ^(id editableItem, NSTimeInterval position) + { + return [editableItem screenImageSignal:position]; + }; + + controller.requestOriginalFullSizeImage = ^(id editableItem, NSTimeInterval position) + { + return [editableItem originalImageSignal:position]; + }; + + [self addChildViewController:controller]; + [self.view addSubview:controller.view]; + controller.view.clipsToBounds = true; +} + +- (void)setToolbarsHidden:(bool)hidden animated:(bool)animated +{ + if (hidden) + { + [_portraitToolbarView transitionOutAnimated:animated transparent:true hideOnCompletion:false]; + [_landscapeToolbarView transitionOutAnimated:animated transparent:true hideOnCompletion:false]; + } + else + { + [_portraitToolbarView transitionInAnimated:animated transparent:true]; + [_landscapeToolbarView transitionInAnimated:animated transparent:true]; + } +} + +- (void)editorTransitionIn +{ + [UIView animateWithDuration:0.2 animations:^ + { + _arrowView.alpha = 0.0f; + _recipientLabel.alpha = 0.0f; + _captionMixin.inputPanel.alpha = 0.0f; + }]; +} + +- (void)editorTransitionOut +{ + [UIView animateWithDuration:0.3 animations:^ + { + _arrowView.alpha = 0.45f; + _recipientLabel.alpha = 1.0f; + _captionMixin.inputPanel.alpha = 1.0f; + }]; +} + +- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration +{ + [super willRotateToInterfaceOrientation:toInterfaceOrientation duration:duration]; + + [_imageView pop_removeAllAnimations]; +} + +- (void)viewWillLayoutSubviews +{ + [super viewWillLayoutSubviews]; + + [self updateLayout:[[LegacyComponentsGlobals provider] applicationStatusBarOrientation]]; +} + +- (void)setScrollViewVerticalOffset:(CGFloat)offset +{ + _scrollViewVerticalOffset = offset; + + CGRect scrollViewFrame = _scrollView.frame; + scrollViewFrame.origin.y = offset; + _scrollView.frame = scrollViewFrame; +} + +- (void)_layoutRecipientLabelForOrientation:(UIInterfaceOrientation)orientation screenEdges:(UIEdgeInsets)screenEdges +{ + CGFloat screenWidth = MIN(self.view.frame.size.width, self.view.frame.size.height); + CGFloat recipientWidth = MIN(_recipientLabel.frame.size.width, screenWidth - 100.0f); + + if (self.controllerSafeAreaInset.top > 20.0f + FLT_EPSILON) + screenEdges.top += self.controllerSafeAreaInset.top; + screenEdges.left += self.controllerSafeAreaInset.left; + screenEdges.right -= self.controllerSafeAreaInset.right; + + CGRect frame = CGRectZero; + switch (orientation) + { + case UIInterfaceOrientationLandscapeLeft: + frame = CGRectMake(screenEdges.right - recipientWidth - 28.0f, screenEdges.bottom - 24, _arrowView.frame.size.width, _arrowView.frame.size.height); + break; + + case UIInterfaceOrientationLandscapeRight: + frame = CGRectMake(screenEdges.left + 14, screenEdges.bottom - 24, _arrowView.frame.size.width, _arrowView.frame.size.height); + break; + + default: + frame = CGRectMake(screenEdges.left + 14, screenEdges.top + 16, _arrowView.frame.size.width, _arrowView.frame.size.height); + break; + } + + _arrowView.frame = frame; + _recipientLabel.frame = CGRectMake(CGRectGetMaxX(_arrowView.frame) + 6.0f, _arrowView.frame.origin.y - 2.0f, recipientWidth, _recipientLabel.frame.size.height); +} + +- (void)updateLayout:(UIInterfaceOrientation)orientation +{ + UIInterfaceOrientation originalOrientation = orientation; + if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad) + { + _landscapeToolbarView.hidden = true; + orientation = UIInterfaceOrientationPortrait; + } + + bool hasOnScreenNavigation = false; + if (iosMajorVersion() >= 11) + hasOnScreenNavigation = (self.viewLoaded && self.view.safeAreaInsets.bottom > FLT_EPSILON) || _context.safeAreaInset.bottom > FLT_EPSILON; + + CGSize referenceSize = [self referenceViewSizeForOrientation:originalOrientation]; + UIEdgeInsets safeAreaInset = [TGViewController safeAreaInsetForOrientation:orientation hasOnScreenNavigation:hasOnScreenNavigation]; + + [_captionMixin setContentAreaHeight:self.view.frame.size.height]; + + CGFloat screenSide = MAX(referenceSize.width, referenceSize.height); + _wrapperView.frame = CGRectMake((referenceSize.width - screenSide) / 2, (referenceSize.height - screenSide) / 2, screenSide, screenSide); + + UIEdgeInsets screenEdges = UIEdgeInsetsMake((screenSide - referenceSize.height) / 2, (screenSide - referenceSize.width) / 2, (screenSide + referenceSize.height) / 2, (screenSide + referenceSize.width) / 2); + + _landscapeToolbarView.interfaceOrientation = orientation; + + CGFloat portraitToolbarViewBottomEdge = screenSide; + if (TGIsPad()) + portraitToolbarViewBottomEdge = screenEdges.bottom; + _portraitToolbarView.frame = CGRectMake(screenEdges.left, portraitToolbarViewBottomEdge - TGPhotoEditorToolbarSize - safeAreaInset.bottom, referenceSize.width, TGPhotoEditorToolbarSize + safeAreaInset.bottom); + + UIEdgeInsets captionEdgeInsets = screenEdges; + captionEdgeInsets.bottom = _portraitToolbarView.frame.size.height; + [_captionMixin updateLayoutWithFrame:self.view.bounds edgeInsets:captionEdgeInsets]; + + switch (orientation) + { + case UIInterfaceOrientationLandscapeLeft: + { + [UIView performWithoutAnimation:^ + { + _landscapeToolbarView.frame = CGRectMake(screenEdges.left, screenEdges.top, TGPhotoEditorToolbarSize + safeAreaInset.left, referenceSize.height); + }]; + } + break; + + case UIInterfaceOrientationLandscapeRight: + { + [UIView performWithoutAnimation:^ + { + _landscapeToolbarView.frame = CGRectMake(screenEdges.right - TGPhotoEditorToolbarSize - safeAreaInset.right, screenEdges.top, TGPhotoEditorToolbarSize + safeAreaInset.right, referenceSize.height); + }]; + } + break; + + default: + { + _landscapeToolbarView.frame = CGRectMake(_landscapeToolbarView.frame.origin.x, screenEdges.top, TGPhotoEditorToolbarSize, referenceSize.height); + } + break; + } + + [self _layoutRecipientLabelForOrientation:orientation screenEdges:screenEdges]; + + if (_transitionInProgress) + return; + + if (!CGRectEqualToRect(_scrollView.frame, self.view.bounds)) + { + _scrollView.frame = CGRectMake(0.0f, _scrollViewVerticalOffset, self.view.bounds.size.width, self.view.bounds.size.height); + [self reset]; + } +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGCameraPreviewView.h b/submodules/LegacyComponents/LegacyComponents/TGCameraPreviewView.h new file mode 100644 index 0000000000..66e3d8ab82 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGCameraPreviewView.h @@ -0,0 +1,27 @@ +#import +#import + +@class PGCamera; + +@interface TGCameraPreviewView : UIView + +@property (nonatomic, readonly) PGCamera *camera; +@property (nonatomic, readonly) AVCaptureConnection *captureConnection; + +- (void)setupWithCamera:(PGCamera *)camera; +- (void)invalidate; + +- (void)beginTransitionWithSnapshotImage:(UIImage *)image animated:(bool)animated; +- (void)endTransitionAnimated:(bool)animated; + +- (void)beginResetTransitionAnimated:(bool)animated; +- (void)endResetTransitionAnimated:(bool)animated; + +- (void)fadeInAnimated:(bool)animated; +- (void)fadeOutAnimated:(bool)animated; + +- (void)blink; + +- (CGPoint)devicePointOfInterestForPoint:(CGPoint)point; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGCameraPreviewView.m b/submodules/LegacyComponents/LegacyComponents/TGCameraPreviewView.m new file mode 100644 index 0000000000..19d27fc065 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGCameraPreviewView.m @@ -0,0 +1,407 @@ +#import "TGCameraPreviewView.h" + +#import "LegacyComponentsInternal.h" +#import "TGImageUtils.h" + +#import + +#import +#import + +@protocol TGCameraPreviewLayerView + +@property (nonatomic, strong) NSString *videoGravity; +@property (nonatomic, readonly) AVCaptureConnection *connection; +- (CGPoint)captureDevicePointOfInterestForPoint:(CGPoint)point; + +@optional +- (AVSampleBufferDisplayLayer *)displayLayer; +- (AVCaptureVideoPreviewLayer *)previewLayer; + +@end + + +@interface TGCameraPreviewLayerWrapperView : UIView +{ + __weak AVCaptureConnection *_connection; +} + +@property (nonatomic, readonly) AVSampleBufferDisplayLayer *displayLayer; + +- (void)enqueueSampleBuffer:(CMSampleBufferRef)buffer connection:(AVCaptureConnection *)connection; + +@end + + +@interface TGCameraLegacyPreviewLayerWrapperView : UIView + +@property (nonatomic, readonly) AVCaptureVideoPreviewLayer *previewLayer; + +@end + + +@interface TGCameraPreviewView () +{ + UIView *_wrapperView; + UIView *_fadeView; + UIView *_snapshotView; + + PGCamera *_camera; +} +@end + +@implementation TGCameraPreviewView + +- (instancetype)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + if (self != nil) + { + self.backgroundColor = [UIColor blackColor]; + self.clipsToBounds = true; + + if (false && iosMajorVersion() >= 8) + _wrapperView = [[TGCameraPreviewLayerWrapperView alloc] init]; + else + _wrapperView = [[TGCameraLegacyPreviewLayerWrapperView alloc] init]; + [self addSubview:_wrapperView]; + + _wrapperView.videoGravity = AVLayerVideoGravityResizeAspectFill; + + _fadeView = [[UIView alloc] initWithFrame:self.bounds]; + _fadeView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + _fadeView.backgroundColor = [UIColor blackColor]; + _fadeView.userInteractionEnabled = false; + [self addSubview:_fadeView]; + + if (iosMajorVersion() >= 11) + _fadeView.accessibilityIgnoresInvertColors = true; + +#if TARGET_IPHONE_SIMULATOR + _fadeView.backgroundColor = [UIColor redColor]; +#endif + } + return self; +} + +- (AVCaptureConnection *)captureConnection +{ + return _wrapperView.connection; +} + +- (AVSampleBufferDisplayLayer *)displayLayer +{ + return _wrapperView.displayLayer; +} + +- (AVCaptureVideoPreviewLayer *)legacyPreviewLayer +{ + return _wrapperView.previewLayer; +} + +- (void)setupWithCamera:(PGCamera *)camera +{ + _camera = camera; + + __weak TGCameraPreviewView *weakSelf = self; + if ([_wrapperView isKindOfClass:[TGCameraPreviewLayerWrapperView class]]) + { + [self.displayLayer flushAndRemoveImage]; + camera.captureSession.outputSampleBuffer = ^(CMSampleBufferRef buffer, AVCaptureConnection *connection) + { + __strong TGCameraPreviewView *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [(TGCameraPreviewLayerWrapperView *)strongSelf->_wrapperView enqueueSampleBuffer:buffer connection:connection]; + }; + } + else + { +#if !TARGET_IPHONE_SIMULATOR + [self.legacyPreviewLayer setSession:camera.captureSession]; +#endif + } + + camera.captureStarted = ^(bool resume) + { + __strong TGCameraPreviewView *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + if (resume) + [strongSelf endResetTransitionAnimated:true]; + else + [strongSelf fadeInAnimated:true]; + }; + + camera.captureStopped = ^(bool pause) + { + __strong TGCameraPreviewView *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + if (pause) + [strongSelf beginResetTransitionAnimated:true]; + else + [strongSelf fadeOutAnimated:true]; + }; +} + +- (void)invalidate +{ + if ([_wrapperView isKindOfClass:[TGCameraPreviewLayerWrapperView class]]) + { + [self.displayLayer flushAndRemoveImage]; + _camera.captureSession.outputSampleBuffer = nil; + } + else + { + dispatch_async(dispatch_get_main_queue(), ^{ + [self.legacyPreviewLayer setSession:nil]; + }); + } + _wrapperView = nil; +} + +- (PGCamera *)camera +{ + return _camera; +} + +- (void)fadeInAnimated:(bool)animated +{ + if (animated) + { + [UIView animateWithDuration:0.3f delay:0.05f options:UIViewAnimationOptionCurveLinear animations:^ + { + _fadeView.alpha = 0.0f; + } completion:nil]; + } + else + { + _fadeView.alpha = 0.0f; + } +} + +- (void)fadeOutAnimated:(bool)animated +{ + if (animated) + { + [UIView animateWithDuration:0.3f animations:^ + { + _fadeView.alpha = 1.0f; + }]; + } + else + { + _fadeView.alpha = 1.0f; + } +} + +- (void)blink +{ + [UIView animateWithDuration:0.07f delay:0.0f options:UIViewAnimationOptionCurveLinear animations:^ + { + _fadeView.alpha = 1.0f; + } completion:^(BOOL finished) + { + [UIView animateWithDuration:0.07f delay:0.0f options:UIViewAnimationOptionCurveLinear animations:^ + { + _fadeView.alpha = 0.0f; + } completion:^(BOOL finished) + { + + }]; + }]; +} + +- (void)beginTransitionWithSnapshotImage:(UIImage *)image animated:(bool)animated +{ + [_snapshotView removeFromSuperview]; + + UIImageView *snapshotView = [[UIImageView alloc] initWithFrame:_wrapperView.frame]; + snapshotView.contentMode = UIViewContentModeScaleAspectFill; + snapshotView.image = image; + [self insertSubview:snapshotView aboveSubview:_wrapperView]; + + if (iosMajorVersion() >= 11) + snapshotView.accessibilityIgnoresInvertColors = true; + + _snapshotView = snapshotView; + + if (animated) + { + _snapshotView.alpha = 0.0f; + [UIView animateWithDuration:0.3f delay:0.0f options:UIViewAnimationOptionCurveEaseInOut animations:^ + { + _snapshotView.alpha = 1.0f; + } completion:nil]; + } +} + +- (void)endTransitionAnimated:(bool)animated +{ + if (animated) + { + UIView *snapshotView = _snapshotView; + _snapshotView = nil; + + [UIView animateWithDuration:0.4f delay:0.0f options:UIViewAnimationOptionCurveEaseInOut | UIViewAnimationOptionBeginFromCurrentState animations:^ + { + snapshotView.alpha = 0.0f; + } completion:^(__unused BOOL finished) + { + [snapshotView removeFromSuperview]; + }]; + } + else + { + [_snapshotView removeFromSuperview]; + _snapshotView = nil; + } +} + +- (void)beginResetTransitionAnimated:(bool)animated +{ + if (iosMajorVersion() < 7) + return; + + [_snapshotView removeFromSuperview]; + + _snapshotView = [_wrapperView snapshotViewAfterScreenUpdates:false]; + _snapshotView.frame = _wrapperView.frame; + [self insertSubview:_snapshotView aboveSubview:_wrapperView]; + + if (animated) + { + _snapshotView.alpha = 0.0f; + [UIView animateWithDuration:0.3f delay:0.0f options:UIViewAnimationOptionCurveEaseInOut animations:^ + { + _snapshotView.alpha = 1.0f; + } completion:nil]; + } +} + +- (void)endResetTransitionAnimated:(bool)animated +{ + if (iosMajorVersion() < 7) + return; + + if (animated) + { + UIView *snapshotView = _snapshotView; + _snapshotView = nil; + + [UIView animateWithDuration:0.4f delay:0.05f options:UIViewAnimationOptionCurveEaseInOut | UIViewAnimationOptionBeginFromCurrentState animations:^ + { + snapshotView.alpha = 0.0f; + } completion:^(__unused BOOL finished) + { + [snapshotView removeFromSuperview]; + }]; + } + else + { + [_snapshotView removeFromSuperview]; + _snapshotView = nil; + } +} + +- (CGPoint)devicePointOfInterestForPoint:(CGPoint)point +{ + return [_wrapperView captureDevicePointOfInterestForPoint:point]; +} + +- (void)layoutSubviews +{ + _wrapperView.frame = self.bounds; + + if (_snapshotView != nil) + { + CGSize size = TGScaleToFill(_snapshotView.frame.size, _wrapperView.frame.size); + _snapshotView.frame = CGRectMake(floor((self.frame.size.width - size.width) / 2.0f), floor((self.frame.size.height - size.height) / 2.0f), size.width, size.height); + } +} + +@end + + +@implementation TGCameraPreviewLayerWrapperView + +- (NSString *)videoGravity +{ + return [self displayLayer].videoGravity; +} + +- (void)setVideoGravity:(NSString *)videoGravity +{ + self.displayLayer.videoGravity = videoGravity; +} + +- (AVCaptureConnection *)connection +{ + return _connection; +} + +- (CGPoint)captureDevicePointOfInterestForPoint:(CGPoint)point +{ + return CGPointZero; +} + +- (void)enqueueSampleBuffer:(CMSampleBufferRef)buffer connection:(AVCaptureConnection *)connection +{ + _connection = connection; + + //self.orientation = connection.videoOrientation; + //self.mirrored = connection.videoMirrored; + + [self.displayLayer enqueueSampleBuffer:buffer]; +} + +- (AVSampleBufferDisplayLayer *)displayLayer +{ + return (AVSampleBufferDisplayLayer *)self.layer; +} + ++ (Class)layerClass +{ + return [AVSampleBufferDisplayLayer class]; +} + +@end + + +@implementation TGCameraLegacyPreviewLayerWrapperView + +- (NSString *)videoGravity +{ + return self.previewLayer.videoGravity; +} + +- (void)setVideoGravity:(NSString *)videoGravity +{ + self.previewLayer.videoGravity = videoGravity; +} + +- (AVCaptureConnection *)connection +{ + return self.previewLayer.connection; +} + +- (CGPoint)captureDevicePointOfInterestForPoint:(CGPoint)point +{ + return [self.previewLayer captureDevicePointOfInterestForPoint:point]; +} + +- (AVCaptureVideoPreviewLayer *)previewLayer +{ + return (AVCaptureVideoPreviewLayer *)self.layer; +} + ++ (Class)layerClass +{ + return [AVCaptureVideoPreviewLayer class]; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGCameraSegmentsView.h b/submodules/LegacyComponents/LegacyComponents/TGCameraSegmentsView.h new file mode 100644 index 0000000000..b2f0c2ba73 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGCameraSegmentsView.h @@ -0,0 +1,20 @@ +#import + +@interface TGCameraSegmentsView : UIView + +@property (nonatomic, copy) void (^deletePressed)(void); + +- (void)setSegments:(NSArray *)segments; + +- (void)startCurrentSegment; +- (void)setCurrentSegment:(CGFloat)length; +- (void)commitCurrentSegmentWithCompletion:(void (^)(void))completion; + +- (void)highlightLastSegment; +- (void)removeLastSegment; + +- (void)setHidden:(bool)hidden animated:(bool)animated delay:(NSTimeInterval)delay; + +- (void)setDeleteButtonHidden:(bool)hidden animated:(bool)animated; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGCameraSegmentsView.m b/submodules/LegacyComponents/LegacyComponents/TGCameraSegmentsView.m new file mode 100644 index 0000000000..6f5ed063cf --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGCameraSegmentsView.m @@ -0,0 +1,260 @@ +#import "TGCameraSegmentsView.h" + +#import "LegacyComponentsInternal.h" +#import "TGImageUtils.h" + +#import "TGCameraInterfaceAssets.h" + +#import + +const CGFloat TGCameraSegmentsBackgroundInset = 21.0f; +const CGFloat TGCameraSegmentsBackgroundHeight = 10.0f; +const CGFloat TGCameraSegmentsSpacing = 1.5f; +const CGFloat TGCameraSegmentsMinimumWidth = 4.0f; + +@interface TGCameraSegmentView : UIImageView + +- (void)setBlinking; +- (void)setRecording; +- (void)setCommittingWithCompletion:(void (^)(void))completion; + +@end + +@interface TGCameraSegmentsView () +{ + UIImageView *_backgroundView; + UIView *_segmentWrapper; + NSArray *_segmentViews; + + TGCameraSegmentView *_currentSegmentView; + CGFloat _currentSegment; + + TGModernButton *_deleteButton; +} +@end + +@implementation TGCameraSegmentsView + +- (instancetype)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + if (self != nil) + { + static dispatch_once_t onceToken; + static UIImage *segmentImage = nil; + dispatch_once(&onceToken, ^ + { + UIGraphicsBeginImageContextWithOptions(CGSizeMake(4, 4), false, 0.0f); + CGContextRef context = UIGraphicsGetCurrentContext(); + CGContextSetFillColorWithColor(context, [UIColor whiteColor].CGColor); + [[UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, 4, 4) cornerRadius:0.5f] fill]; + segmentImage = [UIGraphicsGetImageFromCurrentImageContext() resizableImageWithCapInsets:UIEdgeInsetsMake(4, 4, 4, 4)]; + UIGraphicsEndImageContext(); + }); + + _backgroundView = [[UIImageView alloc] initWithImage:[TGComponentsImageNamed(@"CameraSegmentsBack") resizableImageWithCapInsets:UIEdgeInsetsMake(4, 4, 4, 4)]]; + [self addSubview:_backgroundView]; + + _segmentWrapper = [[UIView alloc] init]; + [_backgroundView addSubview:_segmentWrapper]; + + _currentSegmentView = [[TGCameraSegmentView alloc] initWithImage:[TGTintedImage(segmentImage, [TGCameraInterfaceAssets accentColor]) resizableImageWithCapInsets:UIEdgeInsetsMake(4, 4, 4, 4)]]; + [_segmentWrapper addSubview:_currentSegmentView]; + + _deleteButton = [[TGModernButton alloc] initWithFrame:CGRectMake(0, 0, 32, 32)]; + _deleteButton.exclusiveTouch = true; + [_deleteButton setImage:TGComponentsImageNamed(@"CameraDeleteIcon") forState:UIControlStateNormal]; + [_deleteButton addTarget:self action:@selector(deleteButtonPressed) forControlEvents:UIControlEventTouchUpInside]; + [self addSubview:_deleteButton]; + + [self setDeleteButtonHidden:true animated:false]; + } + return self; +} + +- (void)deleteButtonPressed +{ + if (self.deletePressed != nil) + self.deletePressed(); +} + +- (void)setSegments:(NSArray *)__unused segments +{ + +} + +- (void)startCurrentSegment +{ + [_currentSegmentView setRecording]; +} + +- (void)setCurrentSegment:(CGFloat)length +{ + _currentSegment = length; + [self _layoutSegmentViews]; +} + +- (void)commitCurrentSegmentWithCompletion:(void (^)(void))completion +{ + __weak TGCameraSegmentView *weakSegmentView = _currentSegmentView; + [_currentSegmentView setCommittingWithCompletion:^ + { + __strong TGCameraSegmentView *strongSegmentView = weakSegmentView; + if (strongSegmentView == nil) + return; + + _currentSegment = 0; + + if (completion != nil) + completion(); + + [strongSegmentView setBlinking]; + }]; +} + +- (void)highlightLastSegment +{ + +} + +- (void)removeLastSegment +{ + +} + +- (void)setHidden:(BOOL)hidden +{ + self.alpha = hidden ? 0.0f : 1.0f; + super.hidden = hidden; + + if (!hidden) + [_currentSegmentView setBlinking]; +} + +- (void)setHidden:(bool)hidden animated:(bool)animated delay:(NSTimeInterval)delay +{ + if (animated) + { + super.hidden = false; + + [UIView animateWithDuration:0.25f delay:delay options:UIViewAnimationOptionCurveLinear animations:^ + { + self.alpha = hidden ? 0.0f : 1.0f; + } completion:^(BOOL finished) + { + if (finished) + self.hidden = hidden; + + if (!hidden) + [_currentSegmentView setBlinking]; + }]; + } + else + { + [self setHidden:hidden]; + } +} + +- (void)setDeleteButtonHidden:(bool)hidden animated:(bool)animated +{ + if (animated) + { + _deleteButton.hidden = false; + + [UIView animateWithDuration:0.25f animations:^ + { + _deleteButton.alpha = hidden ? 0.0f : 1.0f; + } completion:^(BOOL finished) + { + if (finished) + _deleteButton.hidden = hidden; + }]; + } + else + { + _deleteButton.hidden = hidden; + _deleteButton.alpha = hidden ? 0.0f : 1.0f; + } +} + +- (void)_layoutBackgroundView +{ + CGFloat backgroundRightPadding = 0.0f; + CGFloat deleteButtonMargin = _deleteButton.frame.size.width + 9.0f; + if (!_deleteButton.hidden) + backgroundRightPadding = deleteButtonMargin; + + _backgroundView.frame = CGRectMake(TGCameraSegmentsBackgroundInset, (self.frame.size.height - TGCameraSegmentsBackgroundHeight) / 2, self.frame.size.width - TGCameraSegmentsBackgroundInset * 2 - backgroundRightPadding, TGCameraSegmentsBackgroundHeight); + _segmentWrapper.frame = CGRectMake(3, 3, self.frame.size.width - TGCameraSegmentsBackgroundInset * 2 - deleteButtonMargin, TGCameraSegmentsBackgroundHeight - 3 * 2); +} + +- (void)_layoutDeleteButton +{ + _deleteButton.frame = CGRectMake(CGRectGetMaxX(_backgroundView.frame) + 14, (self.frame.size.height - _deleteButton.frame.size.height) / 2, _deleteButton.frame.size.width, _deleteButton.frame.size.height); +} + +- (void)_layoutSegmentViews +{ + +} + +- (void)layoutSubviews +{ + [self _layoutBackgroundView]; + [self _layoutDeleteButton]; +} + +@end + +@interface TGCameraSegmentView () +{ + +} +@end + +@implementation TGCameraSegmentView + +- (instancetype)initWithImage:(UIImage *)image +{ + self = [super initWithImage:image]; + if (self != nil) + { + + } + return self; +} + +- (void)setBlinking +{ + [self _playBlinkAnimation]; +} + +- (void)setRecording +{ + [self _stopBlinkAnimation]; +} + +- (void)setCommittingWithCompletion:(void (^)(void))__unused completion +{ + +} + +- (void)_playBlinkAnimation +{ + CAKeyframeAnimation *blinkAnim = [CAKeyframeAnimation animationWithKeyPath:@"opacity"]; + blinkAnim.duration = 1.2f; + blinkAnim.autoreverses = false; + blinkAnim.fillMode = kCAFillModeForwards; + blinkAnim.repeatCount = HUGE_VALF; + blinkAnim.keyTimes = @[ @0.0f, @0.4f, @0.5f, @0.9f, @1.0f ]; + blinkAnim.values = @[ @1.0f, @1.0f, @0.0f, @0.0f, @1.0f ]; + + [self.layer addAnimation:blinkAnim forKey:@"opacity"]; +} + +- (void)_stopBlinkAnimation +{ + [self.layer removeAllAnimations]; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGCameraShutterButton.h b/submodules/LegacyComponents/LegacyComponents/TGCameraShutterButton.h new file mode 100644 index 0000000000..c69ded5aad --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGCameraShutterButton.h @@ -0,0 +1,17 @@ +#import + +typedef enum +{ + TGCameraShutterButtonNormalMode, + TGCameraShutterButtonVideoMode, + TGCameraShutterButtonRecordingMode +} TGCameraShutterButtonMode; + +@interface TGCameraShutterButton : UIControl + +- (void)setButtonMode:(TGCameraShutterButtonMode)mode animated:(bool)animated; +- (void)setEnabled:(bool)enabled animated:(bool)animated; + +- (void)setHighlighted:(bool)highlighted animated:(bool)animated; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGCameraShutterButton.m b/submodules/LegacyComponents/LegacyComponents/TGCameraShutterButton.m new file mode 100644 index 0000000000..158bbc6718 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGCameraShutterButton.m @@ -0,0 +1,238 @@ +#import "TGCameraShutterButton.h" + +#import + +#import "TGCameraInterfaceAssets.h" +#import + +@interface TGCameraShutterButton () +{ + UIImageView *_ringView; + TGModernButton *_buttonView; +} +@end + +@implementation TGCameraShutterButton + +- (instancetype)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + if (self != nil) + { + CGFloat padding = [self innerPadding]; + + NSString *key = [NSString stringWithFormat:@"%f", frame.size.width]; + + static NSMutableDictionary *ringImages = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^ + { + ringImages = [[NSMutableDictionary alloc] init]; + }); + + UIImage *ringImage = ringImages[key]; + if (ringImage == nil) + { + UIGraphicsBeginImageContextWithOptions(CGSizeMake(frame.size.width, frame.size.height), false, 0.0f); + CGContextRef context = UIGraphicsGetCurrentContext(); + + CGFloat thickness = (padding < 8.0f) ? 5.0f : 6.0f; + + CGContextSetStrokeColorWithColor(context, [TGCameraInterfaceAssets normalColor].CGColor); + CGContextSetLineWidth(context, thickness); + CGContextStrokeEllipseInRect(context, CGRectMake(thickness / 2.0f, thickness / 2.0f, frame.size.width - thickness, frame.size.height - thickness)); + + ringImage = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + + ringImages[key] = ringImage; + } + + self.exclusiveTouch = true; + + _ringView = [[UIImageView alloc] initWithFrame:self.bounds]; + _ringView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + _ringView.image = ringImage; + [self addSubview:_ringView]; + + _buttonView = [[TGModernButton alloc] initWithFrame:CGRectMake(padding, padding, self.frame.size.width - padding * 2, self.frame.size.height - padding * 2)]; + _buttonView.backgroundColor = [TGCameraInterfaceAssets normalColor]; + _buttonView.layer.cornerRadius = _buttonView.frame.size.width / 2; + [_buttonView addTarget:self action:@selector(buttonReleased) forControlEvents:UIControlEventTouchUpInside]; + [_buttonView addTarget:self action:@selector(buttonPressed) forControlEvents:UIControlEventTouchDown]; + [self addSubview:_buttonView]; + + [self setButtonMode:TGCameraShutterButtonNormalMode animated:false]; + } + return self; +} + +- (CGFloat)innerPadding +{ + if (self.frame.size.width == 50.0f) + return 7.0f; + + return 8.0f; +} + +- (CGFloat)squarePadding +{ + if (self.frame.size.width == 50.0f) + return 15.0f; + + return 19.0f; +} + +- (void)setButtonMode:(TGCameraShutterButtonMode)mode animated:(bool)animated +{ + CGFloat padding = [self innerPadding]; + CGFloat squarePadding = [self squarePadding]; + CGFloat radius = (self.frame.size.width - padding * 2) / 2.0f; + + if (animated) + { + switch (mode) + { + case TGCameraShutterButtonNormalMode: + { + [UIView animateWithDuration:0.25f animations:^ + { + _buttonView.backgroundColor = [TGCameraInterfaceAssets normalColor]; + }]; + } + break; + + case TGCameraShutterButtonVideoMode: + { + [UIView animateWithDuration:0.25f animations:^ + { + _buttonView.backgroundColor = [TGCameraInterfaceAssets redColor]; + }]; + + if (_buttonView.layer.cornerRadius < 5) + { + JNWSpringAnimation *cornersAnimation = [JNWSpringAnimation animationWithKeyPath:@"cornerRadius"]; + cornersAnimation.fromValue = @(_buttonView.layer.cornerRadius); + cornersAnimation.toValue = @(radius); + cornersAnimation.mass = 5; + cornersAnimation.damping = 100; + cornersAnimation.stiffness = 300; + [_buttonView.layer addAnimation:cornersAnimation forKey:@"cornerRadius"]; + _buttonView.layer.cornerRadius = radius; + + JNWSpringAnimation *boundsAnimation = [JNWSpringAnimation animationWithKeyPath:@"bounds"]; + boundsAnimation.fromValue = [NSValue valueWithCGRect:_buttonView.layer.bounds]; + boundsAnimation.toValue = [NSValue valueWithCGRect:CGRectMake(0, 0, self.frame.size.width - padding * 2, self.frame.size.height - padding * 2)]; + boundsAnimation.mass = 5; + boundsAnimation.damping = 100; + boundsAnimation.stiffness = 300; + [_buttonView.layer addAnimation:boundsAnimation forKey:@"bounds"]; + _buttonView.layer.bounds = CGRectMake(0, 0, self.frame.size.width - padding * 2, self.frame.size.height - padding * 2); + } + } + break; + + case TGCameraShutterButtonRecordingMode: + { + [UIView animateWithDuration:0.25f animations:^ + { + _buttonView.backgroundColor = [TGCameraInterfaceAssets redColor]; + }]; + + JNWSpringAnimation *cornersAnimation = [JNWSpringAnimation animationWithKeyPath:@"cornerRadius"]; + cornersAnimation.fromValue = @(_buttonView.layer.cornerRadius); + cornersAnimation.toValue = @(4); + cornersAnimation.mass = 5; + cornersAnimation.damping = 100; + cornersAnimation.stiffness = 300; + [_buttonView.layer addAnimation:cornersAnimation forKey:@"cornerRadius"]; + _buttonView.layer.cornerRadius = 4; + + JNWSpringAnimation *boundsAnimation = [JNWSpringAnimation animationWithKeyPath:@"bounds"]; + boundsAnimation.fromValue = [NSValue valueWithCGRect:_buttonView.layer.bounds]; + boundsAnimation.toValue = [NSValue valueWithCGRect:CGRectMake(0, 0, self.frame.size.width - squarePadding * 2, self.frame.size.height - squarePadding * 2)]; + boundsAnimation.mass = 5; + boundsAnimation.damping = 100; + boundsAnimation.stiffness = 300; + [_buttonView.layer addAnimation:boundsAnimation forKey:@"bounds"]; + _buttonView.layer.bounds = CGRectMake(0, 0, self.frame.size.width - squarePadding * 2, self.frame.size.height - squarePadding * 2); + } + break; + + default: + break; + } + } + else + { + switch (mode) + { + case TGCameraShutterButtonNormalMode: + { + _buttonView.backgroundColor = [TGCameraInterfaceAssets normalColor]; + _buttonView.frame = CGRectMake(padding, padding, self.frame.size.width - padding * 2, self.frame.size.height - padding * 2); + _buttonView.layer.cornerRadius = _buttonView.frame.size.width / 2; + } + break; + + case TGCameraShutterButtonVideoMode: + { + [_buttonView.layer removeAllAnimations]; + _buttonView.backgroundColor = [TGCameraInterfaceAssets redColor]; + _buttonView.frame = CGRectMake(padding, padding, self.frame.size.width - padding * 2, self.frame.size.height - padding * 2); + _buttonView.layer.cornerRadius = _buttonView.frame.size.width / 2; + } + break; + + case TGCameraShutterButtonRecordingMode: + { + _buttonView.backgroundColor = [TGCameraInterfaceAssets redColor]; + _buttonView.frame = CGRectMake(squarePadding, squarePadding, self.frame.size.width - squarePadding * 2, self.frame.size.height - squarePadding * 2); + _buttonView.layer.cornerRadius = 4; + } + break; + + default: + break; + } + } +} + +- (void)setEnabled:(bool)__unused enabled animated:(bool)__unused animated +{ + +} + +- (void)buttonReleased +{ + [self sendActionsForControlEvents:UIControlEventTouchUpInside]; +} + +- (void)buttonPressed +{ + [self sendActionsForControlEvents:UIControlEventTouchDown]; +} + +- (void)setHighlighted:(BOOL)highlighted +{ + [super setHighlighted:highlighted]; + + [_buttonView setHighlighted:highlighted]; +} + +- (void)setHighlighted:(bool)highlighted animated:(bool)animated +{ + if (animated) + { + [UIView animateWithDuration:0.25 animations:^ + { + [self setHighlighted:highlighted]; + }]; + } + else + { + [self setHighlighted:highlighted]; + } +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGCameraTimeCodeView.h b/submodules/LegacyComponents/LegacyComponents/TGCameraTimeCodeView.h new file mode 100644 index 0000000000..b5854a29ae --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGCameraTimeCodeView.h @@ -0,0 +1,13 @@ +#import + +@interface TGCameraTimeCodeView : UIView + +@property (nonatomic, copy) NSTimeInterval(^requestedRecordingDuration)(void); + +- (void)startRecording; +- (void)stopRecording; +- (void)reset; + +- (void)setHidden:(bool)hidden animated:(bool)animated; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGCameraTimeCodeView.m b/submodules/LegacyComponents/LegacyComponents/TGCameraTimeCodeView.m new file mode 100644 index 0000000000..8a6f5ad331 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGCameraTimeCodeView.m @@ -0,0 +1,162 @@ +#import "TGCameraTimeCodeView.h" + +#import "LegacyComponentsInternal.h" + +#import +#import + +@interface TGCameraTimeCodeView () +{ + UIImageView *_dotView; + UILabel *_timeLabel; + + NSUInteger _recordingDurationSeconds; + NSTimer *_recordingTimer; +} +@end + +@implementation TGCameraTimeCodeView + +- (instancetype)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + if (self != nil) + { + static UIImage *dotImage = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^ + { + UIGraphicsBeginImageContextWithOptions(CGSizeMake(6, 6), false, 0.0f); + CGContextRef context = UIGraphicsGetCurrentContext(); + + CGContextSetFillColorWithColor(context, [TGCameraInterfaceAssets redColor].CGColor); + CGContextFillEllipseInRect(context, CGRectMake(0, 0, 6, 6)); + + dotImage = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + }); + + _dotView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 7, 6, 6)]; + _dotView.layer.opacity = 0.0f; + _dotView.image = dotImage; + [self addSubview:_dotView]; + + _timeLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, self.frame.size.width, self.frame.size.height)]; + _timeLabel.backgroundColor = [UIColor clearColor]; + _timeLabel.font = [TGCameraInterfaceAssets normalFontOfSize:21]; + _timeLabel.text = @"00:00:00"; + _timeLabel.textAlignment = NSTextAlignmentCenter; + _timeLabel.textColor = [TGCameraInterfaceAssets normalColor]; + [self addSubview:_timeLabel]; + } + return self; +} + +- (void)dealloc +{ + [self stopRecording]; +} + +- (void)_updateRecordingTime +{ + _timeLabel.text = [NSString stringWithFormat:@"%02d:%02d:%02d", (int)(_recordingDurationSeconds / 3600), (int)(_recordingDurationSeconds / 60) % 60, (int)(_recordingDurationSeconds % 60)]; +} + +- (void)startRecording +{ + [self reset]; + + _recordingTimer = [TGTimerTarget scheduledMainThreadTimerWithTarget:self action:@selector(recordingTimerEvent) interval:1.0 repeat:false]; + + [self playBlinkAnimation]; +} + +- (void)stopRecording +{ + [_recordingTimer invalidate]; + _recordingTimer = nil; + + [self stopBlinkAnimation]; +} + +- (void)reset +{ + _timeLabel.text = @"00:00:00"; + _recordingDurationSeconds = 0; +} + +- (void)recordingTimerEvent +{ + [_recordingTimer invalidate]; + _recordingTimer = nil; + + NSTimeInterval recordingDuration = (self.requestedRecordingDuration != nil) ? self.requestedRecordingDuration() : 0.0f; + if (recordingDuration < _recordingDurationSeconds) + return; + + CFAbsoluteTime currentTime = CACurrentMediaTime(); + NSUInteger currentDurationSeconds = (NSUInteger)recordingDuration; + if (currentDurationSeconds == _recordingDurationSeconds) + { + _recordingTimer = [TGTimerTarget scheduledMainThreadTimerWithTarget:self action:@selector(recordingTimerEvent) interval:MAX(0.01, _recordingDurationSeconds + 1.0 - currentTime) repeat:false]; + } + else + { + _recordingDurationSeconds = currentDurationSeconds; + [self _updateRecordingTime]; + _recordingTimer = [TGTimerTarget scheduledMainThreadTimerWithTarget:self action:@selector(recordingTimerEvent) interval:1.0 repeat:false]; + } +} + +- (void)playBlinkAnimation +{ + CAKeyframeAnimation *blinkAnim = [CAKeyframeAnimation animationWithKeyPath:@"opacity"]; + blinkAnim.duration = 0.75f; + blinkAnim.autoreverses = false; + blinkAnim.fillMode = kCAFillModeForwards; + blinkAnim.repeatCount = HUGE_VALF; + blinkAnim.keyTimes = @[ @0.0f, @0.4f, @0.5f, @0.9f, @1.0f ]; + blinkAnim.values = @[ @1.0f, @1.0f, @0.0f, @0.0f, @1.0f ]; + + [_dotView.layer addAnimation:blinkAnim forKey:@"opacity"]; +} + +- (void)stopBlinkAnimation +{ + [_dotView.layer removeAllAnimations]; +} + +- (void)setHidden:(BOOL)hidden +{ + self.alpha = hidden ? 0.0f : 1.0f; + super.hidden = hidden; +} + +- (void)setHidden:(bool)hidden animated:(bool)animated +{ + if (animated) + { + super.hidden = false; + + [UIView animateWithDuration:0.25f animations:^ + { + self.alpha = hidden ? 0.0f : 1.0f; + } completion:^(BOOL finished) + { + if (finished) + self.hidden = hidden; + }]; + } + else + { + self.alpha = hidden ? 0.0f : 1.0f; + super.hidden = hidden; + } +} + +- (void)layoutSubviews +{ + _dotView.frame = CGRectMake(CGFloor(self.frame.size.width / 2 - 48), 7, 6, 6); +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGCameraZoomView.h b/submodules/LegacyComponents/LegacyComponents/TGCameraZoomView.h new file mode 100644 index 0000000000..086b07fa96 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGCameraZoomView.h @@ -0,0 +1,16 @@ +#import + +@interface TGCameraZoomView : UIView + +@property (copy, nonatomic) void(^activityChanged)(bool active); + +@property (nonatomic, assign) CGFloat zoomLevel; +- (void)setZoomLevel:(CGFloat)zoomLevel displayNeeded:(bool)displayNeeded; + +- (void)interactionEnded; + +- (bool)isActive; + +- (void)hideAnimated:(bool)animated; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGCameraZoomView.m b/submodules/LegacyComponents/LegacyComponents/TGCameraZoomView.m new file mode 100644 index 0000000000..ed4febd19d --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGCameraZoomView.m @@ -0,0 +1,176 @@ +#import "TGCameraZoomView.h" +#import "TGCameraInterfaceAssets.h" + +#import "LegacyComponentsInternal.h" + +@interface TGCameraZoomView () +{ + UIView *_clipView; + UIView *_wrapperView; + + UIView *_minusIconView; + UIView *_plusIconView; + + UIView *_leftLine; + UIView *_rightLine; + UIImageView *_knobView; +} +@end + +@implementation TGCameraZoomView + +- (instancetype)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + if (self != nil) + { + self.userInteractionEnabled = false; + + _clipView = [[UIView alloc] init]; + _clipView.clipsToBounds = true; + [self addSubview:_clipView]; + + _wrapperView = [[UIView alloc] initWithFrame:self.bounds]; + [_clipView addSubview:_wrapperView]; + + _leftLine = [[UIView alloc] initWithFrame:CGRectMake(-1000, (12.5f - 1.5f) / 2, 1000, 1.5f)]; + _leftLine.backgroundColor = [TGCameraInterfaceAssets normalColor]; + [_wrapperView addSubview:_leftLine]; + + _rightLine = [[UIView alloc] initWithFrame:CGRectMake(12.5f, (12.5 - 1.5f) / 2, 1000, 1.5f)]; + _rightLine.backgroundColor = [TGCameraInterfaceAssets normalColor]; + [_wrapperView addSubview:_rightLine]; + + static UIImage *knobImage = nil; + + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^ + { + UIGraphicsBeginImageContextWithOptions(CGSizeMake(12.5f, 12.5f), false, 0.0f); + CGContextRef context = UIGraphicsGetCurrentContext(); + + CGContextSetStrokeColorWithColor(context, [TGCameraInterfaceAssets accentColor].CGColor); + CGContextSetLineWidth(context, 1.5f); + CGContextStrokeEllipseInRect(context, CGRectMake(0.75f, 0.75f, 12.5f - 1.5f, 12.5f - 1.5f)); + + knobImage = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + }); + + _knobView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 12.5f, 12.5f)]; + _knobView.image = knobImage; + [_wrapperView addSubview:_knobView]; + + _minusIconView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 9.5f, 1.5f)]; + _minusIconView.backgroundColor = [TGCameraInterfaceAssets normalColor]; + _minusIconView.layer.cornerRadius = 1; + [self addSubview:_minusIconView]; + + _plusIconView = [[UIView alloc] initWithFrame:CGRectMake(frame.size.width - 9.5f, 0, 9.5f, 1.5f)]; + _plusIconView.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin; + _plusIconView.backgroundColor = [TGCameraInterfaceAssets normalColor]; + _plusIconView.layer.cornerRadius = 1; + [self addSubview:_plusIconView]; + + CALayer *plusVertLayer = [[CALayer alloc] init]; + plusVertLayer.backgroundColor = [TGCameraInterfaceAssets normalColor].CGColor; + plusVertLayer.cornerRadius = 1; + plusVertLayer.frame = CGRectMake((9.5f - 1.5f) / 2, -(9.5f - 1.5f) / 2, 1.5f, 9.5f); + [_plusIconView.layer addSublayer:plusVertLayer]; + + [self hideAnimated:false]; + } + return self; +} + +- (void)setZoomLevel:(CGFloat)zoomLevel +{ + [self setZoomLevel:zoomLevel displayNeeded:true]; +} + +- (void)setZoomLevel:(CGFloat)zoomLevel displayNeeded:(bool)displayNeeded +{ + _zoomLevel = zoomLevel; + [self setNeedsLayout]; + + if (displayNeeded) + { + [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(hideAnimated) object:nil]; + + if (self.alpha < FLT_EPSILON) + [self showAnimated:true]; + } +} + +- (bool)isActive +{ + return (self.alpha > FLT_EPSILON); +} + +- (void)showAnimated:(bool)animated +{ + if (self.activityChanged != nil) + self.activityChanged(true); + + if (animated) + { + [UIView animateWithDuration:0.3f animations:^ + { + self.alpha = 1.0f; + }]; + } + else + { + self.alpha = 1.0f; + } +} + +- (void)hideAnimated:(bool)animated +{ + [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(hideAnimated) object:nil]; + + if (animated) + { + [UIView animateWithDuration:0.3f animations:^ + { + self.alpha = 0.0f; + } completion:^(__unused BOOL finished) + { + if (finished) + { + if (self.activityChanged != nil) + self.activityChanged(false); + } + }]; + } + else + { + self.alpha = 0.0f; + + if (self.activityChanged != nil) + self.activityChanged(false); + } +} + +- (void)hideAnimated +{ + [self hideAnimated:true]; +} + +- (void)interactionEnded +{ + [self performSelector:@selector(hideAnimated) withObject:nil afterDelay:4.0f]; +} + +- (void)layoutSubviews +{ + _clipView.frame = CGRectMake(22, (self.frame.size.height - 12.5f) / 2, self.frame.size.width - 44, 12.5f); + + CGFloat position = (_clipView.frame.size.width - _knobView.frame.size.width) * self.zoomLevel; + if (self.zoomLevel < 1.0f - FLT_EPSILON) + position = CGFloor(position); + + _wrapperView.frame = CGRectMake(position, 0, _wrapperView.frame.size.width, _wrapperView.frame.size.height); +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGChannelAdminRights.h b/submodules/LegacyComponents/LegacyComponents/TGChannelAdminRights.h new file mode 100644 index 0000000000..6acdf02185 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGChannelAdminRights.h @@ -0,0 +1,27 @@ +#import + +#import + +@class TLChannelAdminRights; + +@interface TGChannelAdminRights : NSObject + +@property (nonatomic, readonly) bool canChangeInfo; +@property (nonatomic, readonly) bool canPostMessages; +@property (nonatomic, readonly) bool canEditMessages; +@property (nonatomic, readonly) bool canDeleteMessages; +@property (nonatomic, readonly) bool canBanUsers; +@property (nonatomic, readonly) bool canInviteUsers; +@property (nonatomic, readonly) bool canChangeInviteLink; +@property (nonatomic, readonly) bool canPinMessages; +@property (nonatomic, readonly) bool canAddAdmins; + +- (instancetype)initWithCanChangeInfo:(bool)canChangeInfo canPostMessages:(bool)canPostMessages canEditMessages:(bool)canEditMessages canDeleteMessages:(bool)canDeleteMessages canBanUsers:(bool)canBanUsers canInviteUsers:(bool)canInviteUsers canChangeInviteLink:(bool)canChangeInviteLink canPinMessages:(bool)canPinMessages canAddAdmins:(bool)canAddAdmins; + +- (instancetype)initWithFlags:(int32_t)flags; + +- (int32_t)tlFlags; +- (bool)hasAnyRights; +- (int32_t)numberOfRights; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGChannelAdminRights.m b/submodules/LegacyComponents/LegacyComponents/TGChannelAdminRights.m new file mode 100644 index 0000000000..a547f8a812 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGChannelAdminRights.m @@ -0,0 +1,95 @@ +#import "TGChannelAdminRights.h" + +#import "PSKeyValueCoder.h" + +@implementation TGChannelAdminRights + +- (instancetype)initWithCanChangeInfo:(bool)canChangeInfo canPostMessages:(bool)canPostMessages canEditMessages:(bool)canEditMessages canDeleteMessages:(bool)canDeleteMessages canBanUsers:(bool)canBanUsers canInviteUsers:(bool)canInviteUsers canChangeInviteLink:(bool)canChangeInviteLink canPinMessages:(bool)canPinMessages canAddAdmins:(bool)canAddAdmins { + self = [super init]; + if (self != nil) { + _canChangeInfo = canChangeInfo; + _canPostMessages = canPostMessages; + _canEditMessages = canEditMessages; + _canDeleteMessages = canDeleteMessages; + _canBanUsers = canBanUsers; + _canInviteUsers = canInviteUsers; + _canChangeInviteLink = canChangeInviteLink; + _canPinMessages = canPinMessages; + _canAddAdmins = canAddAdmins; + } + return self; +} + +- (instancetype)initWithFlags:(int32_t)flags { + return [self initWithCanChangeInfo:flags & (1 << 0) canPostMessages:flags & (1 << 1) canEditMessages:flags & (1 << 2) canDeleteMessages:flags & (1 << 3) canBanUsers:flags & (1 << 4) canInviteUsers:flags & (1 << 5) canChangeInviteLink:flags & (1 << 6) canPinMessages:flags & (1 << 7) canAddAdmins:flags & (1 << 9)]; +} + +- (int32_t)tlFlags { + int32_t flags = 0; + if (_canChangeInfo) { + flags |= (1 << 0); + } + if (_canPostMessages) { + flags |= (1 << 1); + } + if (_canEditMessages) { + flags |= (1 << 2); + } + if (_canDeleteMessages) { + flags |= (1 << 3); + } + if (_canBanUsers) { + flags |= (1 << 4); + } + if (_canInviteUsers) { + flags |= (1 << 5); + } + if (_canChangeInviteLink) { + flags |= (1 << 6); + } + if (_canPinMessages) { + flags |= (1 << 7); + } + if (_canAddAdmins) { + flags |= (1 << 9); + } + return flags; +} + +- (bool)hasAnyRights { + return [self tlFlags] != 0; +} + +- (int32_t)numberOfRights { + int32_t flags = [self tlFlags]; + int32_t count = 0; + for (int i = 0; i < 31; i++) { + if (flags == 0) { + break; + } + if ((flags & 1) != 0) { + count++; + } + flags = flags >> 1; + } + return count; +} + +- (instancetype)initWithKeyValueCoder:(PSKeyValueCoder *)coder { + int32_t flags = [coder decodeInt32ForCKey:"f"]; + return [self initWithFlags:flags]; +} + +- (void)encodeWithKeyValueCoder:(PSKeyValueCoder *)coder { + [coder encodeInt32:[self tlFlags] forCKey:"f"]; +} + +- (BOOL)isEqual:(id)object { + if (![object isKindOfClass:[TGChannelAdminRights class]]) { + return false; + } + TGChannelAdminRights *other = object; + return [self tlFlags] == [other tlFlags]; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGChannelBannedRights.h b/submodules/LegacyComponents/LegacyComponents/TGChannelBannedRights.h new file mode 100644 index 0000000000..6553084643 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGChannelBannedRights.h @@ -0,0 +1,25 @@ +#import + +#import + +@interface TGChannelBannedRights : NSObject + +@property (nonatomic, readonly) bool banReadMessages; +@property (nonatomic, readonly) bool banSendMessages; +@property (nonatomic, readonly) bool banSendMedia; +@property (nonatomic, readonly) bool banSendStickers; +@property (nonatomic, readonly) bool banSendGifs; +@property (nonatomic, readonly) bool banSendGames; +@property (nonatomic, readonly) bool banSendInline; +@property (nonatomic, readonly) bool banEmbedLinks; +@property (nonatomic, readonly) int32_t timeout; + +- (instancetype)initWithBanReadMessages:(bool)banReadMessages banSendMessages:(bool)banSendMessages banSendMedia:(bool)banSendMedia banSendStickers:(bool)banSendStickers banSendGifs:(bool)banSendGifs banSendGames:(bool)banSendGames banSendInline:(bool)banSendInline banEmbedLinks:(bool)banEmbedLinks timeout:(int32_t)timeout; + +- (instancetype)initWithFlags:(int32_t)flags timeout:(int32_t)timeout; + +- (int32_t)tlFlags; + +- (int32_t)numberOfRestrictions; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGChannelBannedRights.m b/submodules/LegacyComponents/LegacyComponents/TGChannelBannedRights.m new file mode 100644 index 0000000000..038019ed64 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGChannelBannedRights.m @@ -0,0 +1,90 @@ +#import "TGChannelBannedRights.h" + +#import "PSKeyValueCoder.h" + +@implementation TGChannelBannedRights + +- (instancetype)initWithBanReadMessages:(bool)banReadMessages banSendMessages:(bool)banSendMessages banSendMedia:(bool)banSendMedia banSendStickers:(bool)banSendStickers banSendGifs:(bool)banSendGifs banSendGames:(bool)banSendGames banSendInline:(bool)banSendInline banEmbedLinks:(bool)banEmbedLinks timeout:(int32_t)timeout { + self = [super init]; + if (self != nil) { + _banReadMessages = banReadMessages; + _banSendMessages = banSendMessages; + _banSendMedia = banSendMedia; + _banSendStickers = banSendStickers; + _banSendGifs = banSendGifs; + _banSendGames = banSendGames; + _banSendInline = banSendInline; + _banEmbedLinks = banEmbedLinks; + _timeout = timeout; + } + return self; +} + +- (int32_t)tlFlags { + int32_t flags = 0; + if (_banReadMessages) { + flags |= (1 << 0); + } + if (_banSendMessages) { + flags |= (1 << 1); + } + if (_banSendMedia) { + flags |= (1 << 2); + } + if (_banSendStickers) { + flags |= (1 << 3); + } + if (_banSendGifs) { + flags |= (1 << 4); + } + if (_banSendGames) { + flags |= (1 << 5); + } + if (_banSendInline) { + flags |= (1 << 6); + } + if (_banEmbedLinks) { + flags |= (1 << 7); + } + return flags; +} + +- (instancetype)initWithKeyValueCoder:(PSKeyValueCoder *)coder { + int32_t flags = [coder decodeInt32ForCKey:"f"]; + int32_t until_date = [coder decodeInt32ForCKey:"t"]; + return [self initWithFlags:flags timeout:until_date]; +} + +- (instancetype)initWithFlags:(int32_t)flags timeout:(int32_t)timeout { + return [self initWithBanReadMessages:flags & (1 << 0) banSendMessages:flags & (1 << 1) banSendMedia:flags & (1 << 2) banSendStickers:flags & (1 << 3) banSendGifs:flags & (1 << 4) banSendGames:flags & (1 << 5) banSendInline:flags & (1 << 6) banEmbedLinks:flags & (1 << 7) timeout:timeout]; +} + +- (void)encodeWithKeyValueCoder:(PSKeyValueCoder *)coder { + [coder encodeInt32:[self tlFlags] forCKey:"f"]; + [coder encodeInt32:_timeout forCKey:"t"]; +} + +- (BOOL)isEqual:(id)object { + if ([object isKindOfClass:[TGChannelBannedRights class]] && [((TGChannelBannedRights *)object) tlFlags] == [self tlFlags] && ((TGChannelBannedRights *)object)->_timeout == _timeout) { + return true; + } else { + return false; + } +} + +- (int32_t)numberOfRestrictions { + int32_t flags = [self tlFlags]; + int32_t count = 0; + for (int i = 0; i < 31; i++) { + if (flags == 0) { + break; + } + if ((flags & 1) != 0) { + count++; + } + flags = flags >> 1; + } + return count; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGCheckButtonView.h b/submodules/LegacyComponents/LegacyComponents/TGCheckButtonView.h new file mode 100644 index 0000000000..5ec223eeb0 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGCheckButtonView.h @@ -0,0 +1,42 @@ +#import + +typedef enum +{ + TGCheckButtonStyleDefault, + TGCheckButtonStyleDefaultBlue, + TGCheckButtonStyleBar, + TGCheckButtonStyleMedia, + TGCheckButtonStyleGallery, + TGCheckButtonStyleShare, + TGCheckButtonStyleChat +} TGCheckButtonStyle; + +@interface TGCheckButtonPallete : NSObject + +@property (nonatomic, readonly) UIColor *defaultBackgroundColor; +@property (nonatomic, readonly) UIColor *accentBackgroundColor; +@property (nonatomic, readonly) UIColor *defaultBorderColor; +@property (nonatomic, readonly) UIColor *mediaBorderColor; +@property (nonatomic, readonly) UIColor *chatBorderColor; +@property (nonatomic, readonly) UIColor *checkColor; +@property (nonatomic, readonly) UIColor *blueColor; +@property (nonatomic, readonly) UIColor *barBackgroundColor; + ++ (instancetype)palleteWithDefaultBackgroundColor:(UIColor *)defaultBackgroundColor accentBackgroundColor:(UIColor *)accentBackgroundColor defaultBorderColor:(UIColor *)defaultBorderColor mediaBorderColor:(UIColor *)mediaBorderColor chatBorderColor:(UIColor *)chatBorderColor checkColor:(UIColor *)checkColor blueColor:(UIColor *)blueColor barBackgroundColor:(UIColor *)barBackgroundColor; + +@end + +@interface TGCheckButtonView : UIButton + +- (instancetype)initWithStyle:(TGCheckButtonStyle)style; +- (instancetype)initWithStyle:(TGCheckButtonStyle)style pallete:(TGCheckButtonPallete *)pallete; + +- (void)setSelected:(bool)selected animated:(bool)animated; +- (void)setSelected:(bool)selected animated:(bool)animated bump:(bool)bump; +- (void)setSelected:(bool)selected animated:(bool)animated bump:(bool)bump completion:(void (^)())completion; + +- (void)setNumber:(NSUInteger)number; + ++ (void)resetCache; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGCheckButtonView.m b/submodules/LegacyComponents/LegacyComponents/TGCheckButtonView.m new file mode 100644 index 0000000000..8bf2270aa4 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGCheckButtonView.m @@ -0,0 +1,563 @@ +#import "TGCheckButtonView.h" + +#import "TGFont.h" +#import "TGImageUtils.h" + +#import "LegacyComponentsGlobals.h" + +@interface TGCheckButtonView () +{ + UIView *_wrapperView; + + CALayer *_checkBackground; + + TGCheckButtonStyle _style; + CGSize _size; + UIImage *_fillImage; + + UIView *_checkView; + UIImageView *_checkFillView; + UIView *_checkShortFragment; + UIView *_checkLongFragment; + + UILabel *_numberLabel; + NSInteger _number; + + UIColor *_checkColor; +} +@end + +@implementation TGCheckButtonView + +static NSMutableDictionary *backgroundImages; +static NSMutableDictionary *fillImages; +static CGAffineTransform TGCheckButtonDefaultTransform; + ++ (void)resetCache +{ + [backgroundImages removeAllObjects]; + [fillImages removeAllObjects]; +} + +- (instancetype)initWithStyle:(TGCheckButtonStyle)style { + TGCheckButtonPallete *pallete = nil; + if ([[LegacyComponentsGlobals provider] respondsToSelector:@selector(checkButtonPallete)]) + pallete = [[LegacyComponentsGlobals provider] checkButtonPallete]; + return [self initWithStyle:style pallete:pallete]; +} + +- (instancetype)initWithStyle:(TGCheckButtonStyle)style pallete:(TGCheckButtonPallete *)pallete +{ + CGSize size = CGSizeMake(32.0f, 32.0f); + if (style == TGCheckButtonStyleGallery) + size = CGSizeMake(39.0f, 39.0f); + + self = [super initWithFrame:CGRectMake(0, 0, size.width, size.height)]; + if (self != nil) + { + static dispatch_once_t onceToken; + static CGFloat screenScale = 2.0f; + dispatch_once(&onceToken, ^ + { + TGCheckButtonDefaultTransform = CGAffineTransformMakeRotation(-M_PI_4); + backgroundImages = [[NSMutableDictionary alloc] init]; + fillImages = [[NSMutableDictionary alloc] init]; + screenScale = [UIScreen mainScreen].scale; + }); + + int32_t hex = 0x29c519; + UIColor *greenColor = [[UIColor alloc] initWithRed:(((hex >> 16) & 0xff) / 255.0f) green:(((hex >> 8) & 0xff) / 255.0f) blue:(((hex) & 0xff) / 255.0f) alpha:1.0f]; + + hex = 0x007ee5; + UIColor *blueColor = [[UIColor alloc] initWithRed:(((hex >> 16) & 0xff) / 255.0f) green:(((hex >> 8) & 0xff) / 255.0f) blue:(((hex) & 0xff) / 255.0f) alpha:1.0f]; + + hex = 0xcacacf; + UIColor *borderColor = [[UIColor alloc] initWithRed:(((hex >> 16) & 0xff) / 255.0f) green:(((hex >> 8) & 0xff) / 255.0f) blue:(((hex) & 0xff) / 255.0f) alpha:1.0f]; + + UIColor *checkColor = [UIColor whiteColor]; + + UIColor *backgroundColor = [UIColor whiteColor]; + UIColor *chatBorderColor = [UIColor whiteColor]; + + if (pallete != nil) + { + greenColor = pallete.defaultBackgroundColor; + blueColor = pallete.accentBackgroundColor; + borderColor = pallete.defaultBorderColor; + chatBorderColor = pallete.chatBorderColor; + checkColor = pallete.checkColor; + backgroundColor = pallete.barBackgroundColor; + } + + _checkColor = checkColor; + + CGFloat insideInset = 0.0f; + switch (style) + { + case TGCheckButtonStyleBar: + { + insideInset = 2.0f; + } + break; + + case TGCheckButtonStyleGallery: + { + insideInset = 3.5f; + } + break; + + case TGCheckButtonStyleShare: + case TGCheckButtonStyleChat: + case TGCheckButtonStyleMedia: + { + insideInset = 4.0f; + } + break; + + default: + { + insideInset = 5.0f; + } + break; + } + + UIImage *backgroundImage = backgroundImages[@(style)]; + if (backgroundImage == nil) + { + switch (style) + { + case TGCheckButtonStyleGallery: + { + CGRect rect = CGRectMake(0, 0, size.width, size.height); + UIGraphicsBeginImageContextWithOptions(rect.size, false, 0); + CGContextRef context = UIGraphicsGetCurrentContext(); + CGContextSetShadowWithColor(context, CGSizeZero, 2.5f, [UIColor colorWithWhite:0.0f alpha:0.22f].CGColor); + + CGFloat lineWidth = 1.5f; + if (screenScale == 3.0f) + lineWidth = 5.0f / 3.0f; + CGContextSetLineWidth(context, lineWidth); + + CGContextSetStrokeColorWithColor(context, [UIColor whiteColor].CGColor); + CGContextStrokeEllipseInRect(context, CGRectInset(rect, insideInset + 0.5f, insideInset + 0.5f)); + + backgroundImage = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + } + break; + + case TGCheckButtonStyleMedia: + case TGCheckButtonStyleChat: + { + CGRect rect = CGRectMake(0, 0, size.width, size.height); + UIGraphicsBeginImageContextWithOptions(rect.size, false, 0); + CGContextRef context = UIGraphicsGetCurrentContext(); + + if (style == TGCheckButtonStyleMedia || [chatBorderColor isEqual:[UIColor whiteColor]]) + CGContextSetShadowWithColor(context, CGSizeZero, 2.5f, [UIColor colorWithWhite:0.0f alpha:0.22f].CGColor); + CGContextSetLineWidth(context, 1.5f); + CGContextSetStrokeColorWithColor(context, (style == TGCheckButtonStyleChat ? chatBorderColor : [UIColor whiteColor]).CGColor); + CGContextStrokeEllipseInRect(context, CGRectInset(rect, insideInset + 0.5f, insideInset + 0.5f)); + + backgroundImage = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + } + break; + + case TGCheckButtonStyleBar: + { + CGRect rect = CGRectMake(0, 0, size.width, size.height); + UIGraphicsBeginImageContextWithOptions(rect.size, false, 0); + CGContextRef context = UIGraphicsGetCurrentContext(); + CGContextSetLineWidth(context, 1.0f); + + CGContextSetStrokeColorWithColor(context, borderColor.CGColor); + CGContextStrokeEllipseInRect(context, CGRectInset(rect, insideInset + 0.5f, insideInset + 0.5f)); + + backgroundImage = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + } + break; + + case TGCheckButtonStyleShare: + { + CGRect rect = CGRectMake(0, 0, size.width, size.height); + UIGraphicsBeginImageContextWithOptions(rect.size, false, 0); + backgroundImage = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + } + break; + + default: + { + CGRect rect = CGRectMake(0, 0, size.width, size.height); + UIGraphicsBeginImageContextWithOptions(rect.size, false, 0); + CGContextRef context = UIGraphicsGetCurrentContext(); + CGContextSetLineWidth(context, 1.0f); + + CGContextSetStrokeColorWithColor(context, borderColor.CGColor); + CGContextStrokeEllipseInRect(context, CGRectInset(rect, insideInset + 0.5f, insideInset + 0.5f)); + + backgroundImage = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + } + break; + } + + backgroundImages[@(style)] = backgroundImage; + } + + UIImage *fillImage = fillImages[@(style)]; + if (fillImage == nil) + { + switch (style) + { + case TGCheckButtonStyleShare: + case TGCheckButtonStyleChat: + { + CGRect rect = CGRectMake(0, 0, size.width, size.height); + UIGraphicsBeginImageContextWithOptions(rect.size, false, 0); + CGContextRef context = UIGraphicsGetCurrentContext(); + + CGContextSetFillColorWithColor(context, blueColor.CGColor); + CGFloat inset = (style == TGCheckButtonStyleChat && ![chatBorderColor isEqual:[UIColor whiteColor]]) ? 0.0f : 1.2f; + CGContextFillEllipseInRect(context, CGRectInset(rect, insideInset + inset, insideInset + inset)); + CGContextSetLineWidth(context, 2.0f); + if (style == TGCheckButtonStyleShare) + { + CGContextSetStrokeColorWithColor(context, backgroundColor.CGColor); + CGContextStrokeEllipseInRect(context, CGRectInset(rect, insideInset + 1.0f, insideInset + 1.0f)); + } + + fillImage = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + } + break; + + default: + { + CGRect rect = CGRectMake(0, 0, size.width, size.height); + UIGraphicsBeginImageContextWithOptions(rect.size, false, 0); + CGContextRef context = UIGraphicsGetCurrentContext(); + + UIColor *color = style == TGCheckButtonStyleDefaultBlue ? blueColor : greenColor; + CGContextSetFillColorWithColor(context, color.CGColor); + CGFloat inset = (style == TGCheckButtonStyleDefault || style == TGCheckButtonStyleDefaultBlue) ? 0.0f : 1.2f; + CGContextFillEllipseInRect(context, CGRectInset(rect, insideInset + inset, insideInset + inset)); + + fillImage = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + } + break; + } + + fillImages[@(style)] = fillImage; + } + + _style = style; + _size = size; + _fillImage = fillImage; + + _wrapperView = [[UIView alloc] initWithFrame:self.bounds]; + _wrapperView.userInteractionEnabled = false; + [self addSubview:_wrapperView]; + + _checkBackground = [[CALayer alloc] init]; + _checkBackground.contents = (__bridge id)(backgroundImage.CGImage); + _checkBackground.frame = CGRectMake(0.0f, 0.0f, size.width, size.height); + + [_wrapperView.layer addSublayer:_checkBackground]; + } + return self; +} + +- (void)_createCheckButtonDetailsIfNeeded +{ + if (_checkFillView != nil) + return; + + _checkFillView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, _size.width, _size.height)]; + _checkFillView.alpha = 0.0f; + _checkFillView.image = _fillImage; + _checkFillView.transform = CGAffineTransformMakeScale(0.01f, 0.01f); + [_wrapperView addSubview:_checkFillView]; + + _checkView = [[UIView alloc] initWithFrame:CGRectInset(self.bounds, 4, 4)]; + _checkView.alpha = 0.0f; + _checkView.userInteractionEnabled = false; + _checkView.transform = TGCheckButtonDefaultTransform; + [_wrapperView addSubview:_checkView]; + + CGRect shortFragmentFrame = CGRectMake(6.5f, 8.5f, 1.5f, 6.0f); + CGRect longFragmentFrame = CGRectMake(7.5f, 13.0f, 11.0f, 1.5f); + + if (_style == TGCheckButtonStyleGallery) + { + shortFragmentFrame = CGRectMake(9.0f, 10.5f, 2.0f, 8.0f); + longFragmentFrame = CGRectMake(9.5f, 16.5f, 14.5f, 2.0f); + } + + _checkShortFragment = [[UIView alloc] init]; + _checkShortFragment.backgroundColor = _checkColor; + _checkShortFragment.layer.anchorPoint = CGPointMake(0.5f, 0); + _checkShortFragment.frame = shortFragmentFrame; + _checkShortFragment.transform = CGAffineTransformMakeScale(1.0f, 0.0f); + [_checkView addSubview:_checkShortFragment]; + + _checkLongFragment = [[UIView alloc] init]; + _checkLongFragment.backgroundColor = _checkColor; + _checkLongFragment.layer.anchorPoint = CGPointMake(0, 0.5f); + _checkLongFragment.frame = longFragmentFrame; + _checkLongFragment.transform = CGAffineTransformMakeScale(0.0f, 1.0f); + [_checkView addSubview:_checkLongFragment]; + + if (_numberLabel != nil) + { + if (_numberLabel.superview == nil) + [_checkFillView addSubview:_numberLabel]; + else + [_numberLabel.superview bringSubviewToFront:_numberLabel]; + } +} + +- (void)setSelected:(bool)selected animated:(bool)animated +{ + [self setSelected:selected animated:animated bump:false]; +} + +- (void)setSelected:(bool)selected animated:(bool)animated bump:(bool)bump +{ + [self setSelected:selected animated:animated bump:bump completion:nil]; +} + +- (void)setSelected:(bool)selected animated:(bool)animated bump:(bool)bump completion:(void (^)())completion +{ + if (selected) + [self _createCheckButtonDetailsIfNeeded]; + + if (animated && bump) + [self setWrapperScale:selected ? 0.77f : 0.87f animated:false]; + + static dispatch_once_t onceToken; + static bool inhibitAnimation = false; + dispatch_once(&onceToken, ^ + { + inhibitAnimation = ([[[UIDevice currentDevice] systemVersion] compare:@"7" options:NSNumericSearch] == NSOrderedAscending); + }); + + if (inhibitAnimation) + animated = false; + + if (animated) + { + if (self.selected == selected) { + if (completion) { + completion(); + } + return; + } + + if (bump) + { + if (selected) + _checkFillView.transform = CGAffineTransformIdentity; + } + else + { + if (selected) + _checkFillView.transform = CGAffineTransformMakeScale(0.01f, 0.01f); + else + _checkFillView.transform = CGAffineTransformIdentity; + } + + [UIView animateWithDuration:0.19f animations:^ + { + _checkFillView.alpha = selected ? 1.0f : 0.0f; + if (!bump || !selected) + _checkFillView.transform = selected ? CGAffineTransformIdentity : CGAffineTransformMakeScale(0.01f, 0.01f); + }]; + + if (selected) + { + CGFloat duration = 0.4f; + CGFloat damping = 0.35f; + CGFloat initialVelocity = 0.8f; + if (bump) + { + duration = 0.5f; + damping = 0.4f; + initialVelocity = 0.6f; + } + + [UIView animateWithDuration:duration delay:0.0f usingSpringWithDamping:damping initialSpringVelocity:initialVelocity options:UIViewAnimationOptionBeginFromCurrentState animations:^ + { + _wrapperView.transform = CGAffineTransformIdentity; + } completion:^(__unused BOOL finished) { + if (completion) { + completion(); + } + }]; + + if (_number == 0) + { + _checkView.alpha = 1.0f; + _checkShortFragment.transform = CGAffineTransformMakeScale(1.0f, 0.0f); + _checkLongFragment.transform = CGAffineTransformMakeScale(0.0f, 1.0f); + + [UIView animateKeyframesWithDuration:0.21f delay:0.0f options:kNilOptions animations:^ + { + [UIView addKeyframeWithRelativeStartTime:0.0f relativeDuration:0.333f animations:^ + { + _checkShortFragment.transform = CGAffineTransformIdentity; + }]; + + [UIView addKeyframeWithRelativeStartTime:0.333f relativeDuration:0.666f animations:^ + { + _checkLongFragment.transform = CGAffineTransformIdentity; + }]; + } completion:^(__unused BOOL finished) { + }]; + } + } + else + { + CGFloat duration = 0.17f; + if (bump) + duration = 0.15f; + + [UIView animateWithDuration:duration animations:^ + { + if (_number > 0) + { + + } + else + { + _checkView.transform = CGAffineTransformScale(_checkView.transform, 0.01f, 0.01f); + _checkView.alpha = 0.0f; + } + + if (bump) + _wrapperView.transform = CGAffineTransformIdentity; + } completion:^(__unused BOOL finished) + { + _checkView.transform = TGCheckButtonDefaultTransform; + if (completion) { + completion(); + } + }]; + } + } + else + { + _checkFillView.alpha = selected ? 1.0f : 0.0f; + _checkFillView.transform = selected ? CGAffineTransformIdentity : CGAffineTransformMakeScale(0.1f, 0.1f); + + if (_number > 0) + { + _checkView.alpha = 0.0f; + } + else + { + _checkView.alpha = selected ? 1.0f : 0.0f; + _checkView.transform = TGCheckButtonDefaultTransform; + _checkShortFragment.transform = CGAffineTransformIdentity; + _checkLongFragment.transform = CGAffineTransformIdentity; + } + + if (completion) { + completion(); + } + } + + super.selected = selected; +} + +- (void)setNumber:(NSUInteger)number +{ + if (number == NSNotFound) + number = 0; + + _number = number; + if (_numberLabel == nil && number != 0) + { + _numberLabel = [[UILabel alloc] init]; + _numberLabel.backgroundColor = [UIColor clearColor]; + _numberLabel.frame = CGRectMake(0.0f, -TGScreenPixel, _wrapperView.frame.size.width, _wrapperView.frame.size.height); + _numberLabel.textColor = _checkColor; + _numberLabel.textAlignment = NSTextAlignmentCenter; + _numberLabel.userInteractionEnabled = false; + _numberLabel.font = [TGFont roundedFontOfSize:_style == TGCheckButtonStyleGallery ? 18.0f : 16.0f]; + [_checkFillView addSubview:_numberLabel]; + } + + if (number > 0) + { + _checkView.alpha = 0.0f; + _numberLabel.alpha = 1.0f; + _numberLabel.text = [NSString stringWithFormat:@"%ld", number]; + if (_style != TGCheckButtonStyleGallery) + _numberLabel.font = [TGFont roundedFontOfSize:_number < 10 ? 16.0f : 15.0f]; + } + else + { + [UIView animateWithDuration:0.2 animations:^ + { + _numberLabel.alpha = 0.0f; + }]; + } +} + +#pragma mark - + +- (void)setWrapperScale:(CGFloat)scale animated:(bool)animated +{ + void (^change)(void) = ^ + { + _wrapperView.transform = CGAffineTransformMakeScale(scale, scale); + }; + + if (animated) + [UIView animateWithDuration:0.12 delay:0 options:UIViewAnimationOptionAllowAnimatedContent animations:change completion:nil]; + else + change(); +} + +- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event +{ + [super touchesBegan:touches withEvent:event]; + [self setWrapperScale:0.85f animated:true]; +} + +- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event +{ + [super touchesEnded:touches withEvent:event]; + [self setWrapperScale:1.0f animated:true]; +} + +- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event +{ + [super touchesCancelled:touches withEvent:event]; + [self setWrapperScale:1.0f animated:true]; +} + +@end + + +@implementation TGCheckButtonPallete + ++ (instancetype)palleteWithDefaultBackgroundColor:(UIColor *)defaultBackgroundColor accentBackgroundColor:(UIColor *)accentBackgroundColor defaultBorderColor:(UIColor *)defaultBorderColor mediaBorderColor:(UIColor *)mediaBorderColor chatBorderColor:(UIColor *)chatBorderColor checkColor:(UIColor *)checkColor blueColor:(UIColor *)blueColor barBackgroundColor:(UIColor *)barBackgroundColor +{ + TGCheckButtonPallete *pallete = [[TGCheckButtonPallete alloc] init]; + pallete->_defaultBackgroundColor = defaultBackgroundColor; + pallete->_accentBackgroundColor = accentBackgroundColor; + pallete->_defaultBorderColor = defaultBorderColor; + pallete->_mediaBorderColor = mediaBorderColor; + pallete->_chatBorderColor = chatBorderColor; + pallete->_checkColor = checkColor; + pallete->_blueColor = blueColor; + pallete->_barBackgroundColor = barBackgroundColor; + return pallete; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGClipboardGalleryMixin.h b/submodules/LegacyComponents/LegacyComponents/TGClipboardGalleryMixin.h new file mode 100644 index 0000000000..91029a936f --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGClipboardGalleryMixin.h @@ -0,0 +1,26 @@ +#import +#import + +#import + +@class TGClipboardGalleryPhotoItem; + +@interface TGClipboardGalleryMixin : NSObject + +@property (nonatomic, copy) void (^itemFocused)(TGClipboardGalleryPhotoItem *); + +@property (nonatomic, copy) void (^willTransitionIn)(); +@property (nonatomic, copy) void (^willTransitionOut)(); +@property (nonatomic, copy) void (^didTransitionOut)(); +@property (nonatomic, copy) UIView *(^referenceViewForItem)(TGClipboardGalleryPhotoItem *); + +@property (nonatomic, copy) void (^completeWithItem)(TGClipboardGalleryPhotoItem *item); + +@property (nonatomic, copy) void (^editorOpened)(void); +@property (nonatomic, copy) void (^editorClosed)(void); + +- (instancetype)initWithContext:(id)context image:(UIImage *)image images:(NSArray *)images parentController:(TGViewController *)parentController thumbnailImage:(UIImage *)thumbnailImage selectionContext:(TGMediaSelectionContext *)selectionContext editingContext:(TGMediaEditingContext *)editingContext suggestionContext:(TGSuggestionContext *)suggestionContext hasCaptions:(bool)hasCaptions hasTimer:(bool)hasTimer recipientName:(NSString *)recipientName; + +- (void)present; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGClipboardGalleryMixin.m b/submodules/LegacyComponents/LegacyComponents/TGClipboardGalleryMixin.m new file mode 100644 index 0000000000..0e7688207a --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGClipboardGalleryMixin.m @@ -0,0 +1,190 @@ +#import "TGClipboardGalleryMixin.h" + +#import + +#import +#import "TGClipboardGalleryPhotoItem.h" +#import "TGClipboardGalleryModel.h" + +#import +#import +#import + +#import +#import +#import +#import + +@interface TGClipboardGalleryMixin () +{ + TGMediaEditingContext *_editingContext; + bool _asFile; + + __weak TGViewController *_parentController; + __weak TGModernGalleryController *_galleryController; + TGModernGalleryController *_strongGalleryController; + + NSUInteger _itemsLimit; + + id _context; +} + +@property (nonatomic, weak, readonly) TGClipboardGalleryModel *galleryModel; + +@end + +@implementation TGClipboardGalleryMixin + +- (instancetype)initWithContext:(id)context image:(UIImage *)image images:(NSArray *)images parentController:(TGViewController *)parentController thumbnailImage:(UIImage *)thumbnailImage selectionContext:(TGMediaSelectionContext *)selectionContext editingContext:(TGMediaEditingContext *)editingContext suggestionContext:(TGSuggestionContext *)suggestionContext hasCaptions:(bool)hasCaptions hasTimer:(bool)hasTimer recipientName:(NSString *)recipientName +{ + self = [super init]; + if (self != nil) + { + _context = context; + _parentController = parentController; + _editingContext = editingContext; + + __weak TGClipboardGalleryMixin *weakSelf = self; + + TGModernGalleryController *modernGallery = [[TGModernGalleryController alloc] initWithContext:_context]; + _galleryController = modernGallery; + _strongGalleryController = modernGallery; + modernGallery.isImportant = true; + + __block NSUInteger focusIndex = 0; + [images enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL * _Nonnull stop) + { + if (obj == image) + { + focusIndex = idx; + *stop = true; + } + }]; + + TGClipboardGalleryModel *model = [[TGClipboardGalleryModel alloc] initWithContext:_context images:images focusIndex:focusIndex selectionContext:selectionContext editingContext:editingContext hasCaptions:hasCaptions hasTimer:hasTimer hasSelectionPanel:false recipientName:recipientName]; + _galleryModel = model; + model.controller = modernGallery; + model.suggestionContext = suggestionContext; + model.willFinishEditingItem = ^(id editableItem, id adjustments, id representation, bool hasChanges) + { + __strong TGClipboardGalleryMixin *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + if (hasChanges) + { + [editingContext setAdjustments:adjustments forItem:editableItem]; + [editingContext setTemporaryRep:representation forItem:editableItem]; + } + + if (selectionContext != nil && adjustments != nil && [editableItem conformsToProtocol:@protocol(TGMediaSelectableItem)]) + [selectionContext setItem:(id)editableItem selected:true]; + }; + + model.didFinishEditingItem = ^(id editableItem, __unused id adjustments, UIImage *resultImage, UIImage *thumbnailImage) + { + [editingContext setImage:resultImage thumbnailImage:thumbnailImage forItem:editableItem synchronous:false]; + }; + + model.didFinishRenderingFullSizeImage = ^(id editableItem, UIImage *resultImage) + { + [editingContext setFullSizeImage:resultImage forItem:editableItem]; + }; + + model.saveItemCaption = ^(id editableItem, NSString *caption, NSArray *entities) + { + [editingContext setCaption:caption entities:entities forItem:editableItem]; + + if (selectionContext != nil && caption.length > 0 && [editableItem conformsToProtocol:@protocol(TGMediaSelectableItem)]) + [selectionContext setItem:(id)editableItem selected:true]; + }; + + [model.interfaceView updateSelectionInterface:selectionContext.count counterVisible:(selectionContext.count > 0) animated:false]; + model.interfaceView.donePressed = ^(TGClipboardGalleryPhotoItem *item) + { + __strong TGClipboardGalleryMixin *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + strongSelf->_galleryModel.dismiss(true, false); + + if (strongSelf.completeWithItem != nil) + strongSelf.completeWithItem(item); + }; + + modernGallery.model = model; + modernGallery.itemFocused = ^(TGClipboardGalleryPhotoItem *item) + { + __strong TGClipboardGalleryMixin *strongSelf = weakSelf; + if (strongSelf != nil && strongSelf.itemFocused != nil) + strongSelf.itemFocused(item); + }; + + modernGallery.beginTransitionIn = ^UIView *(TGClipboardGalleryPhotoItem *item, TGModernGalleryItemView *itemView) + { + __strong TGClipboardGalleryMixin *strongSelf = weakSelf; + if (strongSelf == nil) + return nil; + + if (strongSelf.willTransitionIn != nil) + strongSelf.willTransitionIn(); + + if (strongSelf.referenceViewForItem != nil) + return strongSelf.referenceViewForItem(item); + + return nil; + }; + + modernGallery.finishedTransitionIn = ^(__unused TGClipboardGalleryPhotoItem *item, __unused TGModernGalleryItemView *itemView) + { + __strong TGClipboardGalleryMixin *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [strongSelf->_galleryModel.interfaceView setSelectedItemsModel:strongSelf->_galleryModel.selectedItemsModel]; + }; + + modernGallery.beginTransitionOut = ^UIView *(TGClipboardGalleryPhotoItem *item, TGModernGalleryItemView *itemView) + { + __strong TGClipboardGalleryMixin *strongSelf = weakSelf; + if (strongSelf != nil) + { + if (strongSelf.willTransitionOut != nil) + strongSelf.willTransitionOut(); + + if (strongSelf.referenceViewForItem != nil) + return strongSelf.referenceViewForItem(item); + } + return nil; + }; + + modernGallery.completedTransitionOut = ^ + { + __strong TGClipboardGalleryMixin *strongSelf = weakSelf; + if (strongSelf != nil && strongSelf.didTransitionOut != nil) + strongSelf.didTransitionOut(); + }; + } + return self; +} + +- (void)present +{ + _galleryModel.editorOpened = self.editorOpened; + _galleryModel.editorClosed = self.editorClosed; + + [_galleryController setPreviewMode:false]; + + TGOverlayControllerWindow *controllerWindow = [[TGOverlayControllerWindow alloc] initWithManager:[_context makeOverlayWindowManager] parentController:_parentController contentController:_galleryController]; + controllerWindow.hidden = false; + _galleryController.view.clipsToBounds = true; + + _strongGalleryController = nil; +} + +- (UIViewController *)galleryController +{ + return _galleryController; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGClipboardGalleryModel.h b/submodules/LegacyComponents/LegacyComponents/TGClipboardGalleryModel.h new file mode 100644 index 0000000000..f2a5356d91 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGClipboardGalleryModel.h @@ -0,0 +1,31 @@ +#import + +#import +#import + +#import + +#import + +@interface TGClipboardGalleryModel : TGModernGalleryModel + +@property (nonatomic, copy) void (^willFinishEditingItem)(id item, id adjustments, id temporaryRep, bool hasChanges); +@property (nonatomic, copy) void (^didFinishEditingItem)(iditem, id adjustments, UIImage *resultImage, UIImage *thumbnailImage); +@property (nonatomic, copy) void (^didFinishRenderingFullSizeImage)(id item, UIImage *fullSizeImage); + +@property (nonatomic, copy) void (^saveItemCaption)(id item, NSString *caption, NSArray *entities); + +@property (nonatomic, copy) void (^editorOpened)(void); +@property (nonatomic, copy) void (^editorClosed)(void); + +@property (nonatomic, weak) TGModernGalleryController *controller; + +@property (nonatomic, readonly, strong) TGMediaPickerGalleryInterfaceView *interfaceView; +@property (nonatomic, readonly, strong) TGMediaPickerGallerySelectedItemsModel *selectedItemsModel; + +@property (nonatomic, readonly) TGMediaSelectionContext *selectionContext; +@property (nonatomic, strong) TGSuggestionContext *suggestionContext; + +- (instancetype)initWithContext:(id)context images:(NSArray *)images focusIndex:(NSUInteger)focusIndex selectionContext:(TGMediaSelectionContext *)selectionContext editingContext:(TGMediaEditingContext *)editingContext hasCaptions:(bool)hasCaptions hasTimer:(bool)hasTimer hasSelectionPanel:(bool)hasSelectionPanel recipientName:(NSString *)recipientName; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGClipboardGalleryModel.m b/submodules/LegacyComponents/LegacyComponents/TGClipboardGalleryModel.m new file mode 100644 index 0000000000..83f9266763 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGClipboardGalleryModel.m @@ -0,0 +1,503 @@ +#import "TGClipboardGalleryModel.h" + +#import "TGMediaPickerGallerySelectedItemsModel.h" + +#import "LegacyComponentsInternal.h" + +#import +#import +#import "TGModernGallerySelectableItem.h" +#import "TGModernGalleryEditableItem.h" +#import "TGModernGalleryEditableItemView.h" +#import + +#import "TGClipboardGalleryPhotoItem.h" + +#import "TGModernMediaListItem.h" +#import "TGModernMediaListSelectableItem.h" + +#import + +#import + +@interface TGClipboardGalleryModel () +{ + id _itemBeingEdited; + TGMediaEditingContext *_editingContext; + + id _context; +} + +@property (nonatomic, weak) TGPhotoEditorController *editorController; + +@end + +@implementation TGClipboardGalleryModel + +- (instancetype)initWithContext:(id)context images:(NSArray *)images focusIndex:(NSUInteger)focusIndex selectionContext:(TGMediaSelectionContext *)selectionContext editingContext:(TGMediaEditingContext *)editingContext hasCaptions:(bool)hasCaptions hasTimer:(bool)hasTimer hasSelectionPanel:(bool)hasSelectionPanel recipientName:(NSString *)recipientName +{ + self = [super init]; + if (self != nil) + { + _context = context; + + NSMutableArray *items = [[NSMutableArray alloc] init]; + TGClipboardGalleryPhotoItem *focusItem = nil; + NSUInteger i = 0; + for (UIImage *image in images) + { + TGClipboardGalleryPhotoItem *item = [[TGClipboardGalleryPhotoItem alloc] initWithImage:image]; + item.selectionContext = selectionContext; + item.editingContext = editingContext; + [items addObject:item]; + + if (i == focusIndex) + focusItem = item; + + i++; + } + + [self _replaceItems:items focusingOnItem:focusItem]; + + _editingContext = editingContext; + _selectionContext = selectionContext; + + __weak TGClipboardGalleryModel *weakSelf = self; + if (selectionContext != nil) + { + _selectedItemsModel = [[TGMediaPickerGallerySelectedItemsModel alloc] initWithSelectionContext:selectionContext]; + _selectedItemsModel.selectionUpdated = ^(bool reload, bool incremental, bool add, NSInteger index) + { + __strong TGClipboardGalleryModel *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [strongSelf.interfaceView updateSelectionInterface:[strongSelf selectionCount] counterVisible:([strongSelf selectionCount] > 0) animated:incremental]; + [strongSelf.interfaceView updateSelectedPhotosView:reload incremental:incremental add:add index:index]; + }; + } + + _interfaceView = [[TGMediaPickerGalleryInterfaceView alloc] initWithContext:_context focusItem:focusItem selectionContext:selectionContext editingContext:editingContext hasSelectionPanel:hasSelectionPanel hasCameraButton:false recipientName:recipientName]; + _interfaceView.hasCaptions = hasCaptions; + _interfaceView.hasTimer = hasTimer; + [_interfaceView setEditorTabPressed:^(TGPhotoEditorTab tab) + { + __strong TGClipboardGalleryModel *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + __strong TGModernGalleryController *controller = strongSelf.controller; + if ([controller.currentItem conformsToProtocol:@protocol(TGModernGalleryEditableItem)]) + [strongSelf presentPhotoEditorForItem:(id)controller.currentItem tab:tab]; + }]; + _interfaceView.photoStripItemSelected = ^(NSInteger index) + { + __strong TGClipboardGalleryModel *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [strongSelf setCurrentItemWithIndex:index]; + }; + _interfaceView.captionSet = ^(id item, NSString *caption, NSArray *entities) + { + __strong TGClipboardGalleryModel *strongSelf = weakSelf; + if (strongSelf == nil || strongSelf.saveItemCaption == nil) + return; + + __strong TGModernGalleryController *controller = strongSelf.controller; + if ([controller.currentItem conformsToProtocol:@protocol(TGModernGalleryEditableItem)]) + strongSelf.saveItemCaption(((id)item).editableMediaItem, caption, entities); + }; + _interfaceView.timerRequested = ^ + { + __strong TGClipboardGalleryModel *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + __strong TGModernGalleryController *controller = strongSelf.controller; + id editableMediaItem = ((id)controller.currentItem).editableMediaItem; + + NSString *description = editableMediaItem.isVideo ? TGLocalized(@"SecretTimer.VideoDescription") : TGLocalized(@"SecretTimer.ImageDescription"); + + NSString *lastValueKey = @"mediaPickerLastTimerValue_v0"; + NSNumber *value = [strongSelf->_editingContext timerForItem:editableMediaItem]; + if (value == nil) + value = [[NSUserDefaults standardUserDefaults] objectForKey:lastValueKey]; + + [TGSecretTimerMenu presentInParentController:controller context:strongSelf->_context dark:true description:description values:[TGSecretTimerMenu secretMediaTimerValues] value:value completed:^(NSNumber *value) + { + __strong TGClipboardGalleryModel *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + if (value == nil) + [[NSUserDefaults standardUserDefaults] removeObjectForKey:lastValueKey]; + else + [[NSUserDefaults standardUserDefaults] setObject:value forKey:lastValueKey]; + + [strongSelf->_editingContext setTimer:value forItem:editableMediaItem]; + + if (value.integerValue != 0) + { + __strong TGModernGalleryController *controller = strongSelf.controller; + id selectableItem = nil; + if ([controller.currentItem conformsToProtocol:@protocol(TGModernGallerySelectableItem)]) + { + selectableItem = ((id)controller.currentItem).selectableMediaItem; + + if (selectableItem != nil) + [strongSelf->_selectionContext setItem:selectableItem selected:true animated:false sender:nil]; + } + } + } dismissed:^ + { + __strong TGClipboardGalleryModel *strongSelf = weakSelf; + if (strongSelf != nil) + [strongSelf->_interfaceView setAllInterfaceHidden:false delay:0.0f animated:true]; + } sourceView:controller.view sourceRect:^CGRect + { + __strong TGClipboardGalleryModel *strongSelf = weakSelf; + if (strongSelf == nil) + return CGRectZero; + + __strong TGModernGalleryController *controller = strongSelf.controller; + return [strongSelf->_interfaceView.timerButton convertRect:strongSelf->_interfaceView.timerButton.bounds toView:controller.view]; + }]; + }; + } + return self; +} + +- (void)setSuggestionContext:(TGSuggestionContext *)suggestionContext +{ + _suggestionContext = suggestionContext; + [_interfaceView setSuggestionContext:suggestionContext]; +} + +- (NSInteger)selectionCount +{ + return _selectedItemsModel.selectedCount; +} + +- (void)setCurrentItem:(id)item direction:(TGModernGalleryScrollAnimationDirection)direction +{ + if (![(id)item conformsToProtocol:@protocol(TGMediaSelectableItem)]) + return; + + id targetSelectableItem = (id)item; + + __block NSUInteger newIndex = NSNotFound; + [self.items enumerateObjectsUsingBlock:^(id galleryItem, NSUInteger idx, BOOL *stop) + { + if ([galleryItem conformsToProtocol:@protocol(TGModernGallerySelectableItem)]) + { + id selectableItem = ((id)galleryItem).selectableMediaItem; + + if ([selectableItem.uniqueIdentifier isEqual:targetSelectableItem.uniqueIdentifier]) + { + newIndex = idx; + *stop = true; + } + } + }]; + + TGModernGalleryController *galleryController = self.controller; + [galleryController setCurrentItemIndex:newIndex direction:direction animated:true]; +} + +- (void)setCurrentItemWithIndex:(NSUInteger)index +{ + if (_selectedItemsModel == nil) + return; + + TGModernGalleryController *galleryController = self.controller; + + if (![galleryController.currentItem conformsToProtocol:@protocol(TGModernGallerySelectableItem)]) + return; + + id currentGalleryItem = (id)galleryController.currentItem; + + __block NSUInteger currentSelectedItemIndex = NSNotFound; + [_selectedItemsModel.items enumerateObjectsUsingBlock:^(id item, NSUInteger index, BOOL *stop) + { + if ([item.uniqueIdentifier isEqualToString:currentGalleryItem.selectableMediaItem.uniqueIdentifier]) + { + currentSelectedItemIndex = index; + *stop = true; + } + }]; + + id item = _selectedItemsModel.items[index]; + + TGModernGalleryScrollAnimationDirection direction = TGModernGalleryScrollAnimationDirectionLeft; + if (currentSelectedItemIndex < index) + direction = TGModernGalleryScrollAnimationDirectionRight; + + [self setCurrentItem:item direction:direction]; +} + +- (UIView *)createInterfaceView +{ + return _interfaceView; +} + +- (UIView *)referenceViewForItem:(id)item frame:(CGRect *)frame +{ + TGModernGalleryController *galleryController = self.controller; + TGModernGalleryItemView *galleryItemView = [galleryController itemViewForItem:item]; + + if ([galleryItemView isKindOfClass:[TGModernGalleryZoomableItemView class]]) + { + TGModernGalleryZoomableItemView *zoomableItemView = (TGModernGalleryZoomableItemView *)galleryItemView; + + if (zoomableItemView.contentView != nil) + { + if (frame != NULL) + *frame = [zoomableItemView transitionViewContentRect]; + + return (UIImageView *)zoomableItemView.transitionContentView; + } + } + + return nil; +} + +- (void)updateHiddenItem +{ + TGModernGalleryController *galleryController = self.controller; + + for (TGModernGalleryItemView *itemView in galleryController.visibleItemViews) + { + if ([itemView conformsToProtocol:@protocol(TGModernGalleryEditableItemView)]) + [(TGModernGalleryItemView *)itemView setHiddenAsBeingEdited:[itemView.item isEqual:_itemBeingEdited]]; + } +} + +- (void)updateEditedItemView +{ + TGModernGalleryController *galleryController = self.controller; + + for (TGModernGalleryItemView *itemView in galleryController.visibleItemViews) + { + if ([itemView conformsToProtocol:@protocol(TGModernGalleryEditableItemView)]) + { + if ([itemView.item isEqual:_itemBeingEdited]) + { + [(TGModernGalleryItemView *)itemView setItem:_itemBeingEdited synchronously:true]; + if (self.itemsUpdated != nil) + self.itemsUpdated(_itemBeingEdited); + } + } + } +} + +- (void)presentPhotoEditorForItem:(id)item tab:(TGPhotoEditorTab)tab +{ + __weak TGClipboardGalleryModel *weakSelf = self; + + if (_itemBeingEdited != nil) + return; + + _itemBeingEdited = item; + + PGPhotoEditorValues *editorValues = (PGPhotoEditorValues *)[item.editingContext adjustmentsForItem:item.editableMediaItem]; + + NSString *caption = [item.editingContext captionForItem:item.editableMediaItem]; + + CGRect refFrame = CGRectZero; + UIView *editorReferenceView = [self referenceViewForItem:item frame:&refFrame]; + UIView *referenceView = nil; + UIImage *screenImage = nil; + UIView *referenceParentView = nil; + UIImage *image = nil; + + bool isVideo = false; + if ([editorReferenceView isKindOfClass:[UIImageView class]]) + { + screenImage = [(UIImageView *)editorReferenceView image]; + referenceView = editorReferenceView; + } + + TGPhotoEditorControllerIntent intent = isVideo ? TGPhotoEditorControllerVideoIntent : TGPhotoEditorControllerGenericIntent; + TGPhotoEditorController *controller = [[TGPhotoEditorController alloc] initWithContext:_context item:item.editableMediaItem intent:intent adjustments:editorValues caption:caption screenImage:screenImage availableTabs:_interfaceView.currentTabs selectedTab:tab]; + controller.editingContext = _editingContext; + self.editorController = controller; + controller.suggestionContext = self.suggestionContext; + controller.willFinishEditing = ^(id adjustments, id temporaryRep, bool hasChanges) + { + __strong TGClipboardGalleryModel *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + strongSelf->_itemBeingEdited = nil; + + if (strongSelf.willFinishEditingItem != nil) + strongSelf.willFinishEditingItem(item.editableMediaItem, adjustments, temporaryRep, hasChanges); + }; + + void (^didFinishEditingItem)(iditem, id adjustments, UIImage *resultImage, UIImage *thumbnailImage) = self.didFinishEditingItem; + controller.didFinishEditing = ^(id adjustments, UIImage *resultImage, UIImage *thumbnailImage, bool hasChanges) + { + __strong TGClipboardGalleryModel *strongSelf = weakSelf; + if (strongSelf == nil) { + TGLegacyLog(@"controller.didFinishEditing strongSelf == nil"); + } + +#ifdef DEBUG + if (adjustments != nil && hasChanges && !isVideo) + NSAssert(resultImage != nil, @"resultImage should not be nil"); +#endif + + if (hasChanges) + { + if (didFinishEditingItem != nil) { + didFinishEditingItem(item.editableMediaItem, adjustments, resultImage, thumbnailImage); + } + } + }; + + controller.didFinishRenderingFullSizeImage = ^(UIImage *image) + { + __strong TGClipboardGalleryModel *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + if (strongSelf.didFinishRenderingFullSizeImage != nil) + strongSelf.didFinishRenderingFullSizeImage(item.editableMediaItem, image); + }; + + controller.captionSet = ^(NSString *caption, NSArray *entities) + { + __strong TGClipboardGalleryModel *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + if (strongSelf.saveItemCaption != nil) + strongSelf.saveItemCaption(item.editableMediaItem, caption, entities); + }; + + controller.requestToolbarsHidden = ^(bool hidden, bool animated) + { + __strong TGClipboardGalleryModel *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [strongSelf.interfaceView setToolbarsHidden:hidden animated:animated]; + }; + + controller.beginTransitionIn = ^UIView *(CGRect *referenceFrame, __unused UIView **parentView) + { + __strong TGClipboardGalleryModel *strongSelf = weakSelf; + if (strongSelf == nil) + return nil; + + if (strongSelf.editorOpened != nil) + strongSelf.editorOpened(); + + [strongSelf updateHiddenItem]; + [strongSelf.interfaceView editorTransitionIn]; + + *referenceFrame = refFrame; + + if (referenceView.superview == nil) + *parentView = referenceParentView; + + if (iosMajorVersion() >= 7) + [strongSelf.controller setNeedsStatusBarAppearanceUpdate]; + else + [_context setStatusBarHidden:true withAnimation:UIStatusBarAnimationNone]; + + return referenceView; + }; + + controller.finishedTransitionIn = ^ + { + __strong TGClipboardGalleryModel *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + TGModernGalleryController *galleryController = strongSelf.controller; + TGModernGalleryItemView *galleryItemView = [galleryController itemViewForItem:strongSelf->_itemBeingEdited]; + if (![galleryItemView isKindOfClass:[TGModernGalleryZoomableItemView class]]) + return; + + TGModernGalleryZoomableItemView *zoomableItemView = (TGModernGalleryZoomableItemView *)galleryItemView; + [zoomableItemView reset]; + }; + + controller.beginTransitionOut = ^UIView *(CGRect *referenceFrame, __unused UIView **parentView) + { + __strong TGClipboardGalleryModel *strongSelf = weakSelf; + if (strongSelf == nil) + return nil; + + [strongSelf.interfaceView editorTransitionOut]; + + CGRect refFrame; + UIView *referenceView = [strongSelf referenceViewForItem:item frame:&refFrame]; + + *referenceFrame = refFrame; + + return referenceView; + }; + + controller.finishedTransitionOut = ^(__unused bool saved) + { + __strong TGClipboardGalleryModel *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + if (strongSelf.editorClosed != nil) + strongSelf.editorClosed(); + + [strongSelf updateHiddenItem]; + + if (iosMajorVersion() >= 7) + [strongSelf.controller setNeedsStatusBarAppearanceUpdate]; + else { + [_context setStatusBarHidden:false withAnimation:UIStatusBarAnimationNone]; + } + }; + + controller.requestThumbnailImage = ^SSignal *(id editableItem) + { + return [editableItem thumbnailImageSignal]; + }; + + controller.requestOriginalScreenSizeImage = ^SSignal *(id editableItem, NSTimeInterval position) + { + return [editableItem screenImageSignal:position]; + }; + + controller.requestOriginalFullSizeImage = ^SSignal *(id editableItem, NSTimeInterval position) + { + return [editableItem originalImageSignal:position]; + }; + + controller.requestImage = ^ + { + return image; + }; + + [self.controller addChildViewController:controller]; + [self.controller.view addSubview:controller.view]; +} + +- (void)_replaceItems:(NSArray *)items focusingOnItem:(id)item +{ + [super _replaceItems:items focusingOnItem:item]; + + TGModernGalleryController *controller = self.controller; + + NSArray *itemViews = [controller.visibleItemViews copy]; + for (TGModernGalleryItemView *itemView in itemViews) + [itemView setItem:itemView.item synchronously:false]; +} + +- (bool)_shouldAutorotate +{ + TGPhotoEditorController *editorController = self.editorController; + return (!editorController || [editorController shouldAutorotate]); +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGClipboardGalleryPhotoItem.h b/submodules/LegacyComponents/LegacyComponents/TGClipboardGalleryPhotoItem.h new file mode 100644 index 0000000000..97065641fd --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGClipboardGalleryPhotoItem.h @@ -0,0 +1,10 @@ +#import +#import + +@interface TGClipboardGalleryPhotoItem : NSObject + +@property (nonatomic, strong) UIImage *image; + +- (instancetype)initWithImage:(UIImage *)image; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGClipboardGalleryPhotoItem.m b/submodules/LegacyComponents/LegacyComponents/TGClipboardGalleryPhotoItem.m new file mode 100644 index 0000000000..4c93487802 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGClipboardGalleryPhotoItem.m @@ -0,0 +1,55 @@ +#import "TGClipboardGalleryPhotoItem.h" +#import "TGClipboardGalleryPhotoItemView.h" + +#import "LegacyComponentsInternal.h" + +#import "UIImage+TGMediaEditableItem.h" + +@implementation TGClipboardGalleryPhotoItem + +@synthesize selectionContext; +@synthesize editingContext; + +- (instancetype)initWithImage:(UIImage *)image +{ + self = [super init]; + if (self != nil) + { + _image = image; + } + return self; +} + +- (NSString *)uniqueId +{ + return self.image.uniqueIdentifier; +} + +- (id)selectableMediaItem +{ + return self.image; +} + +- (id)editableMediaItem +{ + return self.image; +} + +- (TGPhotoEditorTab)toolbarTabs +{ + return TGPhotoEditorCropTab | TGPhotoEditorToolsTab | TGPhotoEditorPaintTab | TGPhotoEditorTimerTab; +} + + +- (Class)viewClass +{ + return [TGClipboardGalleryPhotoItemView class]; +} + +- (BOOL)isEqual:(id)object +{ + return [object isKindOfClass:[TGClipboardGalleryPhotoItem class]] && TGObjectCompare(_image, ((TGClipboardGalleryPhotoItem *)object)->_image); +} + + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGClipboardGalleryPhotoItemView.h b/submodules/LegacyComponents/LegacyComponents/TGClipboardGalleryPhotoItemView.h new file mode 100644 index 0000000000..9646d8f683 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGClipboardGalleryPhotoItemView.h @@ -0,0 +1,11 @@ +#import +#import "TGModernGalleryEditableItemView.h" +#import "TGModernGalleryImageItemImageView.h" + +@interface TGClipboardGalleryPhotoItemView : TGModernGalleryZoomableItemView + +@property (nonatomic) CGSize imageSize; + +@property (nonatomic, strong) TGModernGalleryImageItemImageView *imageView; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGClipboardGalleryPhotoItemView.m b/submodules/LegacyComponents/LegacyComponents/TGClipboardGalleryPhotoItemView.m new file mode 100644 index 0000000000..30a8a7d724 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGClipboardGalleryPhotoItemView.m @@ -0,0 +1,208 @@ +#import "TGClipboardGalleryPhotoItemView.h" + +#import "LegacyComponentsInternal.h" +#import "TGFont.h" +#import "TGStringUtils.h" + +#import + +#import + +#import +#import +#import + +#import + +#import "TGClipboardGalleryPhotoItem.h" + +@interface TGClipboardGalleryPhotoItemView () +{ + UIView *_temporaryRepView; + + SMetaDisposable *_attributesDisposable; +} +@end + +@implementation TGClipboardGalleryPhotoItemView + +- (instancetype)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + if (self != nil) + { + _imageView = [[TGModernGalleryImageItemImageView alloc] init]; + [self.scrollView addSubview:_imageView]; + } + return self; +} + +- (void)dealloc +{ + [_attributesDisposable dispose]; +} + +- (void)setHiddenAsBeingEdited:(bool)hidden +{ + self.imageView.hidden = hidden; + _temporaryRepView.hidden = hidden; +} + +- (void)prepareForRecycle +{ + _imageView.hidden = false; + [_imageView reset]; +} + +- (void)setItem:(TGClipboardGalleryPhotoItem *)item synchronously:(bool)synchronously +{ + [super setItem:item synchronously:synchronously]; + + _imageSize = item.image.size; + [self reset]; + + if (item.image == nil) + { + [self.imageView reset]; + } + else + { + __weak TGClipboardGalleryPhotoItemView *weakSelf = self; + void (^fadeOutRepView)(void) = ^ + { + __strong TGClipboardGalleryPhotoItemView *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + if (strongSelf->_temporaryRepView == nil) + return; + + UIView *repView = strongSelf->_temporaryRepView; + strongSelf->_temporaryRepView = nil; + [UIView animateWithDuration:0.2f animations:^ + { + repView.alpha = 0.0f; + } completion:^(__unused BOOL finished) + { + [repView removeFromSuperview]; + }]; + }; + + SSignal *assetSignal = [SSignal single:item.image]; + + SSignal *imageSignal = assetSignal; + if (item.editingContext != nil) + { + imageSignal = [[[item.editingContext imageSignalForItem:item.editableMediaItem] deliverOn:[SQueue mainQueue]] mapToSignal:^SSignal *(id result) + { + __strong TGClipboardGalleryPhotoItemView *strongSelf = weakSelf; + if (strongSelf == nil) + return [SSignal complete]; + + if (result == nil) + { + return [[assetSignal deliverOn:[SQueue mainQueue]] afterNext:^(__unused id next) + { + fadeOutRepView(); + }]; + } + else if ([result isKindOfClass:[UIView class]]) + { + [strongSelf _setTemporaryRepView:result]; + return [[SSignal single:nil] deliverOn:[SQueue mainQueue]]; + } + else + { + return [[[SSignal single:result] deliverOn:[SQueue mainQueue]] afterNext:^(__unused id next) + { + fadeOutRepView(); + }]; + } + }]; + } + + + [self.imageView setSignal:[[imageSignal deliverOn:[SQueue mainQueue]] afterNext:^(id next) + { + __strong TGClipboardGalleryPhotoItemView *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + if ([next isKindOfClass:[UIImage class]]) + strongSelf->_imageSize = ((UIImage *)next).size; + + [strongSelf reset]; + }]]; + } +} + +- (void)_setTemporaryRepView:(UIView *)view +{ + [_temporaryRepView removeFromSuperview]; + _temporaryRepView = view; + + _imageSize = TGScaleToSize(view.frame.size, self.containerView.frame.size); + + view.hidden = self.imageView.hidden; + view.frame = CGRectMake((self.containerView.frame.size.width - _imageSize.width) / 2.0f, (self.containerView.frame.size.height - _imageSize.height) / 2.0f, _imageSize.width, _imageSize.height); + + [self.containerView addSubview:view]; +} + +- (void)singleTap +{ + if ([self.item conformsToProtocol:@protocol(TGModernGallerySelectableItem)]) + { + TGMediaSelectionContext *selectionContext = ((id)self.item).selectionContext; + id item = ((id)self.item).selectableMediaItem; + + [selectionContext toggleItemSelection:item animated:true sender:nil]; + } + else + { + id delegate = self.delegate; + if ([delegate respondsToSelector:@selector(itemViewDidRequestInterfaceShowHide:)]) + [delegate itemViewDidRequestInterfaceShowHide:self]; + } +} + +- (UIView *)footerView +{ + return nil; +} + +- (SSignal *)contentAvailabilityStateSignal +{ + return [SSignal single:@true]; +} + +- (CGSize)contentSize +{ + return _imageSize; +} + +- (UIView *)contentView +{ + return _imageView; +} + +- (UIView *)transitionContentView +{ + if (_temporaryRepView != nil) + return _temporaryRepView; + + return [self contentView]; +} + +- (UIView *)transitionView +{ + return self.containerView; +} + +- (CGRect)transitionViewContentRect +{ + UIView *contentView = [self transitionContentView]; + return [contentView convertRect:contentView.bounds toView:[self transitionView]]; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGClipboardMenu.h b/submodules/LegacyComponents/LegacyComponents/TGClipboardMenu.h new file mode 100644 index 0000000000..3fa226683c --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGClipboardMenu.h @@ -0,0 +1,19 @@ +#import + +#import + +@class TGViewController; +@class TGMenuSheetController; + +@class TGMediaSelectionContext; +@class TGMediaEditingContext; + +@protocol TGMediaSelectableItem; + +@interface TGClipboardMenu : NSObject + ++ (TGMenuSheetController *)presentInParentController:(TGViewController *)parentController context:(id)context images:(NSArray *)images hasCaption:(bool)hasCaption hasTimer:(bool)hasTimer recipientName:(NSString *)recipientName completed:(void (^)(TGMediaSelectionContext *selectionContext, TGMediaEditingContext *editingContext, id currentItem))completed dismissed:(void (^)(void))dismissed sourceView:(UIView *)sourceView sourceRect:(CGRect (^)(void))sourceRect; + ++ (NSArray *)resultSignalsForSelectionContext:(TGMediaSelectionContext *)selectionContext editingContext:(TGMediaEditingContext *)editingContext currentItem:(id)currentItem descriptionGenerator:(id (^)(id, NSString *, NSArray *, NSString *))descriptionGenerator; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGClipboardMenu.m b/submodules/LegacyComponents/LegacyComponents/TGClipboardMenu.m new file mode 100644 index 0000000000..e61ce11057 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGClipboardMenu.m @@ -0,0 +1,183 @@ +#import "TGClipboardMenu.h" + +#import "LegacyComponentsInternal.h" + +#import +#import + +#import "TGClipboardPreviewItemView.h" + +@implementation TGClipboardMenu + ++ (TGMenuSheetController *)presentInParentController:(TGViewController *)parentController context:(id)context images:(NSArray *)images hasCaption:(bool)hasCaption hasTimer:(bool)hasTimer recipientName:(NSString *)recipientName completed:(void (^)(TGMediaSelectionContext *selectionContext, TGMediaEditingContext *editingContext, id currentItem))completed dismissed:(void (^)(void))dismissed sourceView:(UIView *)sourceView sourceRect:(CGRect (^)(void))sourceRect +{ + bool centered = false; + if (sourceRect == nil) + { + centered = true; + sourceRect = ^CGRect + { + return CGRectMake(CGRectGetMidX(sourceView.frame), CGRectGetMidY(sourceView.frame), 0, 0); + }; + } + + TGMenuSheetController *controller = [[TGMenuSheetController alloc] initWithContext:context dark:false]; + __weak TGMenuSheetController *weakController = controller; + controller.dismissesByOutsideTap = true; + controller.forceFullScreen = true; + controller.hasSwipeGesture = true; + controller.narrowInLandscape = true; + controller.sourceRect = sourceRect; + controller.permittedArrowDirections = centered ? 0 : (UIPopoverArrowDirectionUp | UIPopoverArrowDirectionDown); + controller.willDismiss = ^(__unused bool manual) + { + }; + controller.didDismiss = ^(__unused bool manual) { + if (dismissed != nil) + dismissed(); + }; + + NSMutableArray *itemViews = [[NSMutableArray alloc] init]; + + TGClipboardPreviewItemView *previewItem = [[TGClipboardPreviewItemView alloc] initWithContext:context images:images]; + __weak TGClipboardPreviewItemView *weakPreviewItem = previewItem; + + previewItem.parentController = parentController; + previewItem.allowCaptions = hasCaption; + previewItem.hasTimer = hasTimer; + previewItem.recipientName = recipientName; + previewItem.sendPressed = ^(UIImage *currentItem) + { + __strong TGClipboardPreviewItemView *strongPreviewItem = weakPreviewItem; + completed(strongPreviewItem.selectionContext, strongPreviewItem.editingContext, currentItem); + + __strong TGMenuSheetController *strongController = weakController; + [strongController dismissAnimated:true]; + }; + [itemViews addObject:previewItem]; + + NSString *sendTitle = TGLocalized(@"Clipboard.SendPhoto"); + NSUInteger photosCount = images.count; + if (photosCount > 1) + { + NSString *format = TGLocalized([TGStringUtils integerValueFormat:@"AttachmentMenu.SendPhoto_" value:photosCount]); + sendTitle = [NSString stringWithFormat:format, [NSString stringWithFormat:@"%ld", photosCount]]; + } + + TGMenuSheetButtonItemView *sendItem = [[TGMenuSheetButtonItemView alloc] initWithTitle:sendTitle type:TGMenuSheetButtonTypeSend action:^ + { + __strong TGClipboardPreviewItemView *strongPreviewItem = weakPreviewItem; + completed(strongPreviewItem.selectionContext, strongPreviewItem.editingContext, nil); + + __strong TGMenuSheetController *strongController = weakController; + [strongController dismissAnimated:true]; + }]; + [itemViews addObject:sendItem]; + + __weak TGMenuSheetButtonItemView *weakSendItem = sendItem; + previewItem.selectionChanged = ^(NSUInteger count) + { + __strong TGMenuSheetButtonItemView *strongSendItem = weakSendItem; + __strong TGClipboardPreviewItemView *strongPreviewItem = weakPreviewItem; + if (count > 0) + { + NSString *format = TGLocalized([TGStringUtils integerValueFormat:@"AttachmentMenu.SendPhoto_" value:count]); + NSString *sendTitle = [NSString stringWithFormat:format, [NSString stringWithFormat:@"%ld", count]]; + [strongSendItem setTitle:sendTitle]; + } + + strongSendItem.userInteractionEnabled = count > 0; + [strongPreviewItem setCollapsed:count == 0 animated:true]; + }; + + TGMenuSheetButtonItemView *cancelItem = [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Common.Cancel") type:TGMenuSheetButtonTypeCancel action:^ + { + __strong TGMenuSheetController *strongController = weakController; + [strongController dismissAnimated:true]; + }]; + [itemViews addObject:cancelItem]; + + [controller setItemViews:itemViews animated:false]; + + return controller; + +} + ++ (NSArray *)resultSignalsForSelectionContext:(TGMediaSelectionContext *)selectionContext editingContext:(TGMediaEditingContext *)editingContext currentItem:(id)currentItem descriptionGenerator:(id (^)(id, NSString *, NSArray *, NSString *))descriptionGenerator +{ + NSMutableArray *signals = [[NSMutableArray alloc] init]; + NSMutableArray *selectedItems = [selectionContext.selectedItems mutableCopy]; + if (selectedItems.count == 0 && currentItem != nil) + [selectedItems addObject:currentItem]; + + for (UIImage *asset in selectedItems) + { + NSString *caption = [editingContext captionForItem:asset]; + NSArray *entities = [editingContext entitiesForItem:asset]; + id adjustments = [editingContext adjustmentsForItem:asset]; + NSNumber *timer = [editingContext timerForItem:asset]; + + SSignal *inlineSignal = [[SSignal single:asset] map:^id(UIImage *image) + { + NSMutableDictionary *dict = [[NSMutableDictionary alloc] init]; + dict[@"type"] = @"editedPhoto"; + dict[@"image"] = image; + + if (timer != nil) + dict[@"timer"] = timer; + + id generatedItem = descriptionGenerator(dict, caption, entities, nil); + return generatedItem; + }]; + + SSignal *assetSignal = inlineSignal; + SSignal *imageSignal = assetSignal; + if (editingContext != nil) + { + imageSignal = [[[[[editingContext imageSignalForItem:asset withUpdates:true] filter:^bool(id result) + { + return result == nil || ([result isKindOfClass:[UIImage class]] && !((UIImage *)result).degraded); + }] take:1] mapToSignal:^SSignal *(id result) + { + if (result == nil) + { + return [SSignal fail:nil]; + } + else if ([result isKindOfClass:[UIImage class]]) + { + UIImage *image = (UIImage *)result; + image.edited = true; + return [SSignal single:image]; + } + + return [SSignal complete]; + }] onCompletion:^ + { + __strong TGMediaEditingContext *strongEditingContext = editingContext; + [strongEditingContext description]; + }]; + } + + [signals addObject:[[imageSignal map:^NSDictionary *(UIImage *image) + { + NSMutableDictionary *dict = [[NSMutableDictionary alloc] init]; + dict[@"type"] = @"editedPhoto"; + dict[@"image"] = image; + + if (adjustments.paintingData.stickers.count > 0) + dict[@"stickers"] = adjustments.paintingData.stickers; + + if (timer != nil) + dict[@"timer"] = timer; + + id generatedItem = descriptionGenerator(dict, caption, entities, nil); + return generatedItem; + }] catch:^SSignal *(__unused id error) + { + return inlineSignal; + }]]; + } + return signals; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGClipboardPreviewCell.h b/submodules/LegacyComponents/LegacyComponents/TGClipboardPreviewCell.h new file mode 100644 index 0000000000..d25f041b2b --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGClipboardPreviewCell.h @@ -0,0 +1,23 @@ +#import +#import +#import + +@class TGMediaSelectionContext; +@class TGMediaEditingContext; + +@interface TGClipboardPreviewCell : UICollectionViewCell + +@property (nonatomic, readonly) UIImage *image; +@property (nonatomic, readonly) TGImageView *imageView; + +@property (nonatomic, strong) TGMediaSelectionContext *selectionContext; +@property (nonatomic, strong) TGMediaEditingContext *editingContext; + +- (void)setCornersImage:(UIImage *)cornersImage; + +- (void)setImage:(UIImage *)image signal:(SSignal *)signal hasCheck:(bool)hasCheck; +- (void)setHidden:(bool)hidden animated:(bool)animated; + +@end + +extern NSString *const TGClipboardPreviewCellIdentifier; diff --git a/submodules/LegacyComponents/LegacyComponents/TGClipboardPreviewCell.m b/submodules/LegacyComponents/LegacyComponents/TGClipboardPreviewCell.m new file mode 100644 index 0000000000..11f21576ce --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGClipboardPreviewCell.m @@ -0,0 +1,228 @@ +#import "TGClipboardPreviewCell.h" + +#import +#import + +#import + +#import + +NSString *const TGClipboardPreviewCellIdentifier = @"TGClipboardPreviewCell"; +const CGFloat TGClipboardCellCornerRadius = 5.5f; + +@interface TGClipboardPreviewCell () +{ + TGCheckButtonView *_checkButton; + UIImageView *_cornersView; + + SMetaDisposable *_itemSelectedDisposable; +} +@end + +@implementation TGClipboardPreviewCell + +- (instancetype)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + if (self != nil) + { + self.backgroundColor = [UIColor whiteColor]; + self.clipsToBounds = true; + + _imageView = [[TGImageView alloc] initWithFrame:self.bounds]; + _imageView.contentMode = UIViewContentModeScaleAspectFill; + [self addSubview:_imageView]; + + _cornersView = [[UIImageView alloc] init]; + _cornersView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + _cornersView.frame = self.bounds; + [self addSubview:_cornersView]; + } + return self; +} + +- (void)setCornersImage:(UIImage *)cornersImage +{ + _cornersView.image = cornersImage; +} + +- (void)setImage:(UIImage *)image signal:(SSignal *)signal hasCheck:(bool)hasCheck +{ + _image = image; + + if (self.selectionContext != nil) + { + if (_checkButton == nil && hasCheck) + { + _checkButton = [[TGCheckButtonView alloc] initWithStyle:TGCheckButtonStyleMedia]; + [_checkButton addTarget:self action:@selector(checkButtonPressed) forControlEvents:UIControlEventTouchUpInside]; + [self addSubview:_checkButton]; + } + + if (_itemSelectedDisposable == nil) + _itemSelectedDisposable = [[SMetaDisposable alloc] init]; + + [self setChecked:[self.selectionContext isItemSelected:(id)_image] animated:false]; + __weak TGClipboardPreviewCell *weakSelf = self; + [_itemSelectedDisposable setDisposable:[[self.selectionContext itemInformativeSelectedSignal:(id)_image] startWithNext:^(TGMediaSelectionChange *next) + { + __strong TGClipboardPreviewCell *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + if (next.sender != strongSelf->_checkButton) + [strongSelf setChecked:next.selected animated:next.animated]; + }]]; + + _checkButton.hidden = !hasCheck; + } + + if (_image == nil) + { + [_imageView reset]; + return; + } + + [self setSignal:signal]; +} + + +- (void)setSignal:(SSignal *)signal +{ + if (signal != nil) + [_imageView setSignal:signal]; + else + [_imageView reset]; +} + +- (void)checkButtonPressed +{ + [_checkButton setSelected:!_checkButton.selected animated:true]; + + [self.selectionContext setItem:(id)_image selected:_checkButton.selected animated:false sender:_checkButton]; +} + +- (void)setChecked:(bool)checked animated:(bool)animated +{ + [_checkButton setSelected:checked animated:animated]; +} + +- (void)prepareForReuse +{ + [super prepareForReuse]; + [_imageView reset]; +} + +- (void)setHidden:(bool)hidden animated:(bool)animated +{ + if (hidden != _imageView.hidden) + { + _imageView.hidden = hidden; + + if (animated) + { + if (!hidden) + { + for (UIView *view in self.subviews) + { + if (view != _imageView && view != _cornersView) + view.alpha = 0.0f; + } + } + + [UIView animateWithDuration:0.2 animations:^ + { + if (!hidden) + { + for (UIView *view in self.subviews) + { + if (view != _imageView && view != _cornersView) + view.alpha = 1.0f; + } + } + }]; + } + else + { + for (UIView *view in self.subviews) + { + if (view != _imageView && view != _cornersView) + view.alpha = hidden ? 0.0f : 1.0f; + } + } + } +} + +- (void)layoutSubviews +{ + _imageView.frame = self.bounds; + + if (_checkButton != nil) + { + CGFloat offset = 0.0f; + if (self.superview != nil) + { + CGRect rect = [self.superview convertRect:self.frame toView:self.superview.superview]; + if (rect.origin.x < 0) + offset = rect.origin.x * -1; + else if (CGRectGetMaxX(rect) > self.superview.frame.size.width) + offset = self.superview.frame.size.width - CGRectGetMaxX(rect); + } + + CGFloat x = MAX(0, MIN(self.bounds.size.width - _checkButton.frame.size.width, self.bounds.size.width - _checkButton.frame.size.width + offset)); + _checkButton.frame = CGRectMake(x, 0, _checkButton.frame.size.width, _checkButton.frame.size.height); + } +} + +- (UIImage *)transitionImage +{ + CGFloat scale = 1.0f; + CGSize scaledBoundsSize = CGSizeZero; + CGSize scaledImageSize = CGSizeZero; + + if (self.imageView.image.size.width > self.imageView.image.size.height) + { + scale = self.frame.size.height / self.imageView.image.size.height; + scaledBoundsSize = CGSizeMake(self.frame.size.width / scale, self.imageView.image.size.height); + + scaledImageSize = CGSizeMake(self.imageView.image.size.width * scale, self.imageView.image.size.height * scale); + + if (scaledImageSize.width < self.frame.size.width) + { + scale = self.frame.size.width / self.imageView.image.size.width; + scaledBoundsSize = CGSizeMake(self.imageView.image.size.width, self.frame.size.height / scale); + } + } + else + { + scale = self.frame.size.width / self.imageView.image.size.width; + scaledBoundsSize = CGSizeMake(self.imageView.image.size.width, self.frame.size.height / scale); + + scaledImageSize = CGSizeMake(self.imageView.image.size.width * scale, self.imageView.image.size.height * scale); + + if (scaledImageSize.width < self.frame.size.width) + { + scale = self.frame.size.height / self.imageView.image.size.height; + scaledBoundsSize = CGSizeMake(self.frame.size.width / scale, self.imageView.image.size.height); + } + } + + CGRect rect = self.bounds; + UIGraphicsBeginImageContextWithOptions(rect.size, false, 0.0f); + + CGContextRef context = UIGraphicsGetCurrentContext(); + [[UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, self.frame.size.width, self.frame.size.height) cornerRadius:TGClipboardCellCornerRadius] addClip]; + + CGContextScaleCTM(context, scale, scale); + [self.imageView.image drawInRect:CGRectMake((scaledBoundsSize.width - self.imageView.image.size.width) / 2, + (scaledBoundsSize.height - self.imageView.image.size.height) / 2, + self.imageView.image.size.width, + self.imageView.image.size.height)]; + + UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + + return image; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGClipboardPreviewItemView.h b/submodules/LegacyComponents/LegacyComponents/TGClipboardPreviewItemView.h new file mode 100644 index 0000000000..1283089e83 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGClipboardPreviewItemView.h @@ -0,0 +1,26 @@ +#import + +@class TGMediaSelectionContext; +@class TGMediaEditingContext; +@class TGSuggestionContext; + +@interface TGClipboardPreviewItemView : TGMenuSheetItemView + +@property (nonatomic, weak) TGViewController *parentController; + +@property (nonatomic, assign) bool allowCaptions; +@property (nonatomic, assign) bool hasTimer; +@property (nonatomic, strong) NSString *recipientName; + +@property (nonatomic, readonly) TGMediaSelectionContext *selectionContext; +@property (nonatomic, readonly) TGMediaEditingContext *editingContext; +@property (nonatomic, strong) TGSuggestionContext *suggestionContext; + +@property (nonatomic, copy) void (^selectionChanged)(NSUInteger); +@property (nonatomic, copy) void (^sendPressed)(UIImage *currentItem); + +- (instancetype)initWithContext:(id)context images:(NSArray *)images; + +- (void)setCollapsed:(bool)collapsed animated:(bool)animated; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGClipboardPreviewItemView.m b/submodules/LegacyComponents/LegacyComponents/TGClipboardPreviewItemView.m new file mode 100644 index 0000000000..8ca193e6fd --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGClipboardPreviewItemView.m @@ -0,0 +1,296 @@ +#import "TGClipboardPreviewItemView.h" + +#import "LegacyComponentsInternal.h" + +#import +#import + +#import + +#import "TGClipboardPreviewCell.h" + +const CGFloat TGClipboardPreviewMaxWidth = 250.0f; +const CGFloat TGClipboardPreviewCellHeight = 198.0f; +const CGFloat TGClipboardPreviewEdgeInset = 8.0f; + +@interface TGClipboardPreviewItemView () +{ + id _context; + + NSArray *_images; + + UICollectionView *_collectionView; + UICollectionViewFlowLayout *_collectionLayout; + + TGClipboardGalleryMixin *_galleryMixin; + UIImage *_hiddenItem; + + SMetaDisposable *_selectionChangedDisposable; + + bool _collapsed; + + UIImage *_cornersImage; +} +@end + +@implementation TGClipboardPreviewItemView + +- (instancetype)initWithContext:(id)context images:(NSArray *)images +{ + self = [super initWithType:TGMenuSheetItemTypeDefault]; + if (self != nil) + { + _context = context; + _images = images; + + self.backgroundColor = [UIColor whiteColor]; + + _collectionLayout = [[UICollectionViewFlowLayout alloc] init]; + _collectionLayout.scrollDirection = UICollectionViewScrollDirectionHorizontal; + _collectionLayout.minimumLineSpacing = 8.0f; + + _collectionView = [[UICollectionView alloc] initWithFrame:CGRectMake(0, 0, self.bounds.size.width, TGClipboardPreviewCellHeight + TGClipboardPreviewEdgeInset * 2) collectionViewLayout:_collectionLayout]; + if (iosMajorVersion() >= 11) + _collectionView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; + _collectionView.backgroundColor = [UIColor whiteColor]; + _collectionView.dataSource = self; + _collectionView.delegate = self; + _collectionView.showsHorizontalScrollIndicator = false; + _collectionView.showsVerticalScrollIndicator = false; + [_collectionView registerClass:[TGClipboardPreviewCell class] forCellWithReuseIdentifier:TGClipboardPreviewCellIdentifier]; + [self addSubview:_collectionView]; + + _selectionContext = [[TGMediaSelectionContext alloc] init]; + + for (UIImage *image in _images) + { + [_selectionContext setItem:(id)image selected:true]; + } + + __weak TGClipboardPreviewItemView *weakSelf = self; + _selectionChangedDisposable = [[SMetaDisposable alloc] init]; + [_selectionChangedDisposable setDisposable:[[_selectionContext selectionChangedSignal] startWithNext:^(__unused TGMediaSelectionChange *change) + { + __strong TGClipboardPreviewItemView *strongSelf = weakSelf; + if (strongSelf != nil && strongSelf.selectionChanged != nil) + strongSelf.selectionChanged(strongSelf->_selectionContext.count); + }]]; + + _editingContext = [[TGMediaEditingContext alloc] init]; + } + return self; +} + +- (void)setPallete:(TGMenuSheetPallete *)pallete +{ + [super setPallete:pallete]; + + self.backgroundColor = pallete.backgroundColor; + _collectionView.backgroundColor = pallete.backgroundColor; + + CGFloat radius = 5.5f; + CGRect rect = CGRectMake(0, 0, 12.0f, 12.0f); + UIGraphicsBeginImageContextWithOptions(rect.size, false, 0); + CGContextRef context = UIGraphicsGetCurrentContext(); + + CGContextSetFillColorWithColor(context, pallete.backgroundColor.CGColor); + CGContextFillRect(context, rect); + + CGContextSetBlendMode(context, kCGBlendModeClear); + + CGContextSetFillColorWithColor(context, [UIColor clearColor].CGColor); + CGContextFillEllipseInRect(context, rect); + + _cornersImage = [UIGraphicsGetImageFromCurrentImageContext() resizableImageWithCapInsets:UIEdgeInsetsMake(radius, radius, radius, radius)]; + + UIGraphicsEndImageContext(); +} + +- (void)setCollapsed:(bool)collapsed animated:(bool)animated +{ + _collapsed = collapsed; + + [self _updateHeightAnimated:animated]; +} + +- (CGFloat)contentHeightCorrection +{ + return _collapsed ? -TGMenuSheetButtonItemViewHeight : 0.0f; +} + +- (SSignal *)_signalForImage:(UIImage *)image +{ + SSignal *originalSignal = [SSignal single:image]; + if (_editingContext == nil) + return originalSignal; + + SSignal *editedSignal = [_editingContext fastImageSignalForItem:image withUpdates:true]; + return [editedSignal mapToSignal:^SSignal *(id result) + { + if (result != nil) + return [SSignal single:result]; + else + return originalSignal; + }]; +} + +- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath +{ + TGClipboardPreviewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:TGClipboardPreviewCellIdentifier forIndexPath:indexPath]; + cell.selectionContext = _selectionContext; + cell.editingContext = _editingContext; + [cell setCornersImage:_cornersImage]; + + UIImage *image = _images[indexPath.row]; + [cell setImage:image signal:[self _signalForImage:image] hasCheck:_images.count > 1]; + + return cell; +} + +- (void)collectionView:(UICollectionView *)__unused collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath +{ + _galleryMixin = [self galleryMixinForIndexPath:indexPath]; + [_galleryMixin present]; +} + +- (NSInteger)collectionView:(UICollectionView *)__unused collectionView numberOfItemsInSection:(NSInteger)__unused section +{ + return _images.count; +} + +- (UIEdgeInsets)collectionView:(UICollectionView *)__unused collectionView layout:(UICollectionViewLayout *)__unused collectionViewLayout insetForSectionAtIndex:(NSInteger)__unused section +{ + return UIEdgeInsetsMake(TGClipboardPreviewEdgeInset, TGClipboardPreviewEdgeInset, TGClipboardPreviewEdgeInset, TGClipboardPreviewEdgeInset); +} + +- (CGSize)collectionView:(UICollectionView *)__unused collectionView layout:(UICollectionViewLayout *)__unused collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath +{ + CGSize dimensions = [(UIImage *)_images[indexPath.row] size]; + + CGSize maxPhotoSize = CGSizeMake(TGClipboardPreviewMaxWidth, TGClipboardPreviewCellHeight); + CGFloat width = MIN(maxPhotoSize.width, ceil(dimensions.width * maxPhotoSize.height / dimensions.height)); + return CGSizeMake(width, maxPhotoSize.height); +} + +- (void)scrollViewDidScroll:(UIScrollView *)__unused scrollView +{ + for (UICollectionViewCell *cell in _collectionView.visibleCells) + { + if ([cell isKindOfClass:[TGClipboardPreviewCell class]]) + [(TGClipboardPreviewCell *)cell setNeedsLayout]; + } +} + +- (bool)requiresDivider +{ + return false; +} + +- (CGFloat)preferredHeightForWidth:(CGFloat)__unused width screenHeight:(CGFloat)__unused screenHeight +{ + return TGClipboardPreviewCellHeight + TGClipboardPreviewEdgeInset * 2.0f; +} + +- (void)layoutSubviews +{ + [super layoutSubviews]; + _collectionView.frame = self.bounds; +} + +#pragma mark - + +- (void)_setupGalleryMixin:(TGClipboardGalleryMixin *)mixin +{ + __weak TGClipboardPreviewItemView *weakSelf = self; + mixin.referenceViewForItem = ^UIView *(TGClipboardGalleryPhotoItem *item) + { + __strong TGClipboardPreviewItemView *strongSelf = weakSelf; + if (strongSelf != nil) + return [strongSelf referenceViewForAsset:item.image]; + + return nil; + }; + + mixin.itemFocused = ^(TGClipboardGalleryPhotoItem *item) + { + __strong TGClipboardPreviewItemView *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + strongSelf->_hiddenItem = item.image; + [strongSelf updateHiddenCellAnimated:false]; + }; + + mixin.willTransitionIn = ^ + { + __strong TGClipboardPreviewItemView *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [strongSelf.superview bringSubviewToFront:strongSelf]; + }; + + mixin.willTransitionOut = ^ + { + __strong TGClipboardPreviewItemView *strongSelf = weakSelf; + if (strongSelf == nil) + return; + }; + + mixin.didTransitionOut = ^ + { + __strong TGClipboardPreviewItemView *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + strongSelf->_hiddenItem = nil; + [strongSelf updateHiddenCellAnimated:true]; + + strongSelf->_galleryMixin = nil; + }; + + mixin.completeWithItem = ^(TGClipboardGalleryPhotoItem *item) + { + __strong TGClipboardPreviewItemView *strongSelf = weakSelf; + if (strongSelf != nil && strongSelf.sendPressed != nil) + strongSelf.sendPressed(item.image); + }; + + //mixin.editorOpened = self.editorOpened; + //mixin.editorClosed = self.editorClosed; +} + +- (TGClipboardGalleryMixin *)galleryMixinForIndexPath:(NSIndexPath *)indexPath +{ + UIImage *image = _images[indexPath.row]; + UIImage *thumbnailImage = nil; + + TGClipboardPreviewCell *cell = (TGClipboardPreviewCell *)[_collectionView cellForItemAtIndexPath:indexPath]; + if ([cell isKindOfClass:[TGClipboardPreviewCell class]]) + thumbnailImage = cell.imageView.image; + + TGClipboardGalleryMixin *mixin = [[TGClipboardGalleryMixin alloc] initWithContext:_context image:image images:_images parentController:self.parentController thumbnailImage:thumbnailImage selectionContext:_selectionContext editingContext:_editingContext suggestionContext:self.suggestionContext hasCaptions:self.allowCaptions hasTimer:self.hasTimer recipientName:self.recipientName]; + + [self _setupGalleryMixin:mixin]; + + return mixin; +} + +- (UIView *)referenceViewForAsset:(UIImage *)image +{ + for (TGClipboardPreviewCell *cell in [_collectionView visibleCells]) + { + if ([cell.image isEqual:image]) + return cell; + } + + return nil; +} + +- (void)updateHiddenCellAnimated:(bool)animated +{ + for (TGClipboardPreviewCell *cell in [_collectionView visibleCells]) + [cell setHidden:([cell.image isEqual:_hiddenItem]) animated:animated]; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGColor.h b/submodules/LegacyComponents/LegacyComponents/TGColor.h new file mode 100644 index 0000000000..76585c9b38 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGColor.h @@ -0,0 +1,19 @@ +#import + +#import + +#ifdef __cplusplus +extern "C" { +#endif + +UIColor *TGAccentColor(); +UIColor *TGDestructiveAccentColor(); +UIColor *TGSelectionColor(); +UIColor *TGSeparatorColor(); + +UIColor *TGColorWithHex(int hex); +UIColor *TGColorWithHexAndAlpha(int hex, CGFloat alpha); + +#ifdef __cplusplus +} +#endif diff --git a/submodules/LegacyComponents/LegacyComponents/TGColor.m b/submodules/LegacyComponents/LegacyComponents/TGColor.m new file mode 100644 index 0000000000..503078b23c --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGColor.m @@ -0,0 +1,58 @@ +#import "TGColor.h" + +UIColor *TGColorWithHex(int hex) +{ + return [[UIColor alloc] initWithRed:(((hex >> 16) & 0xff) / 255.0f) green:(((hex >> 8) & 0xff) / 255.0f) blue:(((hex) & 0xff) / 255.0f) alpha:1.0f]; +} + +UIColor *TGColorWithHexAndAlpha(int hex, CGFloat alpha) +{ + return [[UIColor alloc] initWithRed:(((hex >> 16) & 0xff) / 255.0f) green:(((hex >> 8) & 0xff) / 255.0f) blue:(((hex) & 0xff) / 255.0f) alpha:alpha]; +} + +UIColor *TGAccentColor() +{ + static UIColor *color = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^ + { + color = TGColorWithHex(0x007ee5); + }); + return color; +} + +UIColor *TGDestructiveAccentColor() +{ + static UIColor *color = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^ + { + color = TGColorWithHex(0xff3b30); + }); + return color; +} + +UIColor *TGSelectionColor() +{ + static UIColor *color = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^ + { + if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) + color = TGColorWithHex(0xe4e4e4); + else + color = TGColorWithHex(0xd9d9d9); + }); + return color; +} + +UIColor *TGSeparatorColor() +{ + static UIColor *color = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^ + { + color = TGColorWithHex(0xc8c7cc); + }); + return color; +} diff --git a/submodules/LegacyComponents/LegacyComponents/TGColorWallpaperInfo.h b/submodules/LegacyComponents/LegacyComponents/TGColorWallpaperInfo.h new file mode 100644 index 0000000000..f973d84c18 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGColorWallpaperInfo.h @@ -0,0 +1,10 @@ +#import + +@interface TGColorWallpaperInfo : TGWallpaperInfo + +@property (nonatomic, readonly) uint32_t color; + +- (instancetype)initWithColor:(uint32_t)color; +- (instancetype)initWithColor:(uint32_t)color tintColor:(int)tintColor systemAlpha:(CGFloat)systemAlpha buttonsAlpha:(CGFloat)buttonsAlpha highlightedButtonAlpha:(CGFloat)highlightedButtonAlpha progressAlpha:(CGFloat)progressAlpha; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGColorWallpaperInfo.m b/submodules/LegacyComponents/LegacyComponents/TGColorWallpaperInfo.m new file mode 100644 index 0000000000..e1860519dc --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGColorWallpaperInfo.m @@ -0,0 +1,131 @@ +#import "TGColorWallpaperInfo.h" + +#import "LegacyComponentsInternal.h" + +@interface TGColorWallpaperInfo () +{ + int _tintColor; + + CGFloat _systemAlpha; + CGFloat _buttonsAlpha; + CGFloat _highlightedButtonAlpha; + CGFloat _progressAlpha; +} + +@end + +@implementation TGColorWallpaperInfo + +- (instancetype)initWithColor:(uint32_t)color +{ + return [self initWithColor:color tintColor:0x000000 systemAlpha:0.25f buttonsAlpha:0.35f highlightedButtonAlpha:0.50f progressAlpha:0.35f]; +} + +- (instancetype)initWithColor:(uint32_t)color tintColor:(int)tintColor systemAlpha:(CGFloat)systemAlpha buttonsAlpha:(CGFloat)buttonsAlpha highlightedButtonAlpha:(CGFloat)highlightedButtonAlpha progressAlpha:(CGFloat)progressAlpha +{ + self = [super init]; + if (self != nil) + { + _color = color; + + _tintColor = tintColor; + _systemAlpha = systemAlpha; + _buttonsAlpha = buttonsAlpha; + _highlightedButtonAlpha = highlightedButtonAlpha; + _progressAlpha = progressAlpha; + } + return self; +} + +- (NSString *)thumbnailUrl +{ + return [[NSString alloc] initWithFormat:@"color://?color=%d", (int)_color]; +} + +- (NSString *)fullscreenUrl +{ + return [self thumbnailUrl]; +} + +- (int)tintColor +{ + return _tintColor; +} + +- (CGFloat)systemAlpha +{ + return _systemAlpha; +} + +- (CGFloat)buttonsAlpha +{ + return _buttonsAlpha; +} + +- (CGFloat)highlightedButtonAlpha +{ + return _highlightedButtonAlpha; +} + +- (CGFloat)progressAlpha +{ + return _progressAlpha; +} + +- (UIImage *)image +{ + UIGraphicsBeginImageContextWithOptions(CGSizeMake(1, 1), true, 0.0f); + + CGContextRef context = UIGraphicsGetCurrentContext(); + CGContextSetFillColorWithColor(context, UIColorRGB(_color).CGColor); + CGContextFillRect(context, CGRectMake(0.0f, 0.0f, 1.0f, 1.0f)); + + UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + + return image; +} + +- (NSData *)imageData +{ + return nil; +} + +- (bool)hasData +{ + return false; +} + +- (BOOL)isEqual:(id)object +{ + if ([object isKindOfClass:[TGColorWallpaperInfo class]]) + { + if (((TGColorWallpaperInfo *)object)->_color == _color && + ((TGColorWallpaperInfo *)object)->_tintColor == _tintColor) + { + return true; + } + } + + return false; +} + +- (NSDictionary *)infoDictionary +{ + return @{ + @"_className": NSStringFromClass([self class]), + @"color": @(_color), + @"tintColor": @(_tintColor), + @"systemAlpha": @(_systemAlpha), + @"buttonsAlpha": @(_buttonsAlpha), + @"highlightedButtonAlpha": @(_highlightedButtonAlpha), + @"progressAlpha": @(_progressAlpha) + }; +} + ++ (TGWallpaperInfo *)infoWithDictionary:(NSDictionary *)dict +{ + return [[TGColorWallpaperInfo alloc] initWithColor:[dict[@"color"] intValue] tintColor:[dict[@"tintColor"] intValue] systemAlpha:[dict[@"systemAlpha"] floatValue] buttonsAlpha:[dict[@"buttonsAlpha"] floatValue] highlightedButtonAlpha:[dict[@"highlightedButtonAlpha"] floatValue] progressAlpha:[dict[@"progressAlpha"] floatValue]]; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGContactMediaAttachment.h b/submodules/LegacyComponents/LegacyComponents/TGContactMediaAttachment.h new file mode 100644 index 0000000000..9d9d643926 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGContactMediaAttachment.h @@ -0,0 +1,15 @@ + + +#import + +#define TGContactMediaAttachmentType ((int)0xB90A5663) + +@interface TGContactMediaAttachment : TGMediaAttachment + +@property (nonatomic) int uid; +@property (nonatomic, strong) NSString *firstName; +@property (nonatomic, strong) NSString *lastName; +@property (nonatomic, strong) NSString *phoneNumber; +@property (nonatomic, strong) NSString *vcard; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGContactMediaAttachment.m b/submodules/LegacyComponents/LegacyComponents/TGContactMediaAttachment.m new file mode 100644 index 0000000000..c8f8c5d4ca --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGContactMediaAttachment.m @@ -0,0 +1,120 @@ +#import "TGContactMediaAttachment.h" + +@implementation TGContactMediaAttachment + +@synthesize uid = _uid; +@synthesize firstName = _firstName; +@synthesize lastName = _lastName; +@synthesize phoneNumber = _phoneNumber; + +- (id)init +{ + self = [super init]; + if (self != nil) + { + self.type = TGContactMediaAttachmentType; + } + return self; +} + +- (void)serialize:(NSMutableData *)data +{ + int dataLengthPtr = (int)data.length; + int zero = 0; + [data appendBytes:&zero length:4]; + + [data appendBytes:&_uid length:4]; + + NSData *firstNameData = [_firstName dataUsingEncoding:NSUTF8StringEncoding]; + int length = (int)firstNameData.length; + [data appendBytes:&length length:4]; + [data appendData:firstNameData]; + + NSData *lastNameData = [_lastName dataUsingEncoding:NSUTF8StringEncoding]; + length = (int)lastNameData.length; + [data appendBytes:&length length:4]; + [data appendData:lastNameData]; + + NSData *phoneData = [_phoneNumber dataUsingEncoding:NSUTF8StringEncoding]; + length = (int)phoneData.length; + [data appendBytes:&length length:4]; + [data appendData:phoneData]; + + NSData *vcardData = [_vcard dataUsingEncoding:NSUTF8StringEncoding]; + length = (int)vcardData.length; + [data appendBytes:&length length:4]; + [data appendData:vcardData]; + + int dataLength = (int)data.length - dataLengthPtr - 4; + [data replaceBytesInRange:NSMakeRange(dataLengthPtr, 4) withBytes:&dataLength]; +} + +- (TGMediaAttachment *)parseMediaAttachment:(NSInputStream *)is +{ + int dataLength = 0; + int read = 0; + [is read:(uint8_t *)&dataLength maxLength:4]; + + TGContactMediaAttachment *contactAttachment = [[TGContactMediaAttachment alloc] init]; + + int uid = 0; + [is read:(uint8_t *)&uid maxLength:4]; + read += 4; + contactAttachment.uid = uid; + + int length = 0; + [is read:(uint8_t *)&length maxLength:4]; + uint8_t *firstNameBytes = malloc(length); + [is read:firstNameBytes maxLength:length]; + read += length + 4; + contactAttachment.firstName = [[NSString alloc] initWithBytesNoCopy:firstNameBytes length:length encoding:NSUTF8StringEncoding freeWhenDone:true]; + + length = 0; + [is read:(uint8_t *)&length maxLength:4]; + uint8_t *lastNameBytes = malloc(length); + [is read:lastNameBytes maxLength:length]; + read += length + 4; + contactAttachment.lastName = [[NSString alloc] initWithBytesNoCopy:lastNameBytes length:length encoding:NSUTF8StringEncoding freeWhenDone:true]; + + length = 0; + [is read:(uint8_t *)&length maxLength:4]; + uint8_t *phoneBytes = malloc(length); + [is read:phoneBytes maxLength:length]; + read += length + 4; + contactAttachment.phoneNumber = [[NSString alloc] initWithBytesNoCopy:phoneBytes length:length encoding:NSUTF8StringEncoding freeWhenDone:true]; + + if (read < dataLength) + { + length = 0; + [is read:(uint8_t *)&length maxLength:4]; + uint8_t *vcardBytes = malloc(length); + [is read:vcardBytes maxLength:length]; + read += length; + contactAttachment.vcard = [[NSString alloc] initWithBytesNoCopy:vcardBytes length:length encoding:NSUTF8StringEncoding freeWhenDone:true]; + } + + return contactAttachment; +} + +- (instancetype)initWithCoder:(NSCoder *)aDecoder { + self = [super init]; + if (self != nil) { + self.type = TGContactMediaAttachmentType; + _uid = [aDecoder decodeInt32ForKey:@"uid"]; + _firstName = [aDecoder decodeObjectForKey:@"firstName"]; + _lastName = [aDecoder decodeObjectForKey:@"lastName"]; + _phoneNumber = [aDecoder decodeObjectForKey:@"phoneNumber"]; + _vcard = [aDecoder decodeObjectForKey:@"vcard"]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder { + [aCoder encodeInt32:_uid forKey:@"uid"]; + [aCoder encodeObject:_firstName forKey:@"firstName"]; + [aCoder encodeObject:_lastName forKey:@"lastName"]; + [aCoder encodeObject:_phoneNumber forKey:@"phoneNumber"]; + [aCoder encodeObject:_vcard forKey:@"vcard"]; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGConversation.h b/submodules/LegacyComponents/LegacyComponents/TGConversation.h new file mode 100644 index 0000000000..1640dd7cc0 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGConversation.h @@ -0,0 +1,304 @@ + + +#import + +#import +#import +#import +#import +#import +#import + +#define TGConversationKindPersistentChannel 0 +#define TGConversationKindTemporaryChannel 1 + +#define TGChannelDisplayVariantImportant 0 +#define TGChannelDisplayVariantAll 1 + +#define TGConversationPinnedDateBase 1600000000 + +typedef enum { + TGConversationFlagPostAsChannel = (1 << 1), + TGConversationFlagKicked = (1 << 2), + TGConversationFlagVerified = (1 << 3), + TGConversationFlagHasAdmins = (1 << 4), + TGConversationFlagIsAdmin = (1 << 5), + TGConversationFlagIsCreator = (1 << 6), + TGConversationFlagIsChannelGroup = (1 << 7), + TGConversationFlagIsDeactivated = (1 << 8), + TGConversationFlagHasExplicitContent = (1 << 9), + TGConversationFlagEverybodyCanAddMembers = (1 << 10), + TGConversationFlagSignaturesEnabled = (1 << 11), + TGConversationFlagPinnedMessageHidden = (1 << 12), + TGConversationFlagIsMin = (1 << 13), + TGConversationFlagCanNotSetUsername = (1 << 14) +} TGConversationFlags; + +typedef struct { + uint8_t key[9]; +} TGConversationSortKey; + +static inline int TGConversationSortKeyCompare(TGConversationSortKey lhs, TGConversationSortKey rhs) { + return memcmp(lhs.key, rhs.key, 9); +} + +static inline TGConversationSortKey TGConversationSortKeyDecode(PSKeyValueCoder *coder, const char *name) { + TGConversationSortKey key; + [coder decodeBytesForCKey:name value:key.key length:9]; + return key; +} + +static inline void TGConversationSortKeyEncode(PSKeyValueCoder *coder, const char *name, TGConversationSortKey key) { + [coder encodeBytes:key.key length:9 forCKey:name]; +} + +static inline TGConversationSortKey TGConversationSortKeyMake(uint8_t kind, int32_t timestamp, int32_t mid) { + TGConversationSortKey key; + key.key[0] = kind; + + timestamp = NSSwapInt(timestamp); + memcpy(key.key + 1, ×tamp, 4); + + mid = NSSwapInt(mid); + memcpy(key.key + 1 + 4, &mid, 4); + + return key; +} + +static inline TGConversationSortKey TGConversationSortKeyLowerBound(uint8_t kind) { + return TGConversationSortKeyMake(kind, 0, 0); +} + +static inline TGConversationSortKey TGConversationSortKeyUpperBound(uint8_t kind) { + return TGConversationSortKeyMake(kind, INT32_MAX, INT32_MAX); +} + +static inline uint8_t TGConversationSortKeyKind(TGConversationSortKey key) { + return key.key[0]; +} + +static inline TGConversationSortKey TGConversationSortKeyUpdateKind(TGConversationSortKey key, uint8_t kind) { + TGConversationSortKey updatedKey; + memcpy(updatedKey.key, key.key, 8); + updatedKey.key[0] = kind; + return updatedKey; +} + +static inline int32_t TGConversationSortKeyTimestamp(TGConversationSortKey key) { + int32_t timestamp = 0; + memcpy(×tamp, key.key + 1, 4); + return NSSwapInt(timestamp); +} + +static inline int32_t TGConversationSortKeyMid(TGConversationSortKey key) { + int32_t mid = 0; + memcpy(&mid, key.key + 1 + 4, 4); + return NSSwapInt(mid); +} + +static inline NSData *TGConversationSortKeyData(TGConversationSortKey key) { + return [NSData dataWithBytes:key.key length:9]; +} + +static inline TGConversationSortKey TGConversationSortKeyFromData(NSData *data) { + TGConversationSortKey key; + memcpy(key.key, data.bytes, 9); + return key; +} + +typedef enum { + TGChannelRoleMember, + TGChannelRoleCreator, + TGChannelRoleModerator, + TGChannelRolePublisher +} TGChannelRole; + +@interface TGConversationParticipantsData : NSObject +{ + NSData *_serializedData; +} + +@property (nonatomic, strong) NSArray *chatParticipantUids; +@property (nonatomic, strong) NSDictionary *chatInvitedBy; +@property (nonatomic, strong) NSDictionary *chatInvitedDates; +@property (nonatomic, strong) NSSet *chatAdminUids; + +@property (nonatomic, strong) NSArray *chatParticipantSecretChatPeerIds; +@property (nonatomic, strong) NSArray *chatParticipantChatPeerIds; + +@property (nonatomic) int chatAdminId; + +@property (nonatomic) int version; + +@property (nonatomic, strong) NSString *exportedChatInviteString; + ++ (TGConversationParticipantsData *)deserializeData:(NSData *)data; +- (NSData *)serializedData; + +- (void)addParticipantWithId:(int32_t)uid invitedBy:(int32_t)invitedBy date:(int32_t)date; +- (void)removeParticipantWithId:(int32_t)uid; + +- (void)addSecretChatPeerWithId:(int64_t)peerId; +- (void)removeSecretChatPeerWithId:(int64_t)peerId; +- (void)addChatPeerWithId:(int64_t)peerId; +- (void)removeChatPeerWithId:(int64_t)peerId; + +@end + +@interface TGEncryptedConversationData : NSObject + +@property (nonatomic) int64_t encryptedConversationId; +@property (nonatomic) int64_t accessHash; +@property (nonatomic) int64_t keyFingerprint; +@property (nonatomic) int32_t handshakeState; +@property (nonatomic) int64_t currentRekeyExchangeId; +@property (nonatomic) bool currentRekeyIsInitiatedByLocalClient; +@property (nonatomic) NSData *currentRekeyNumber; +@property (nonatomic) NSData *currentRekeyKey; +@property (nonatomic) int64_t currentRekeyKeyId; + +@end + +@interface TGConversation : NSObject + +@property (nonatomic) int64_t conversationId; +@property (nonatomic) int64_t accessHash; + +@property (nonatomic) int32_t displayVariant; +@property (nonatomic) uint8_t kind; + +@property (nonatomic, readonly) TGConversationSortKey databaseSortKey; +@property (nonatomic) TGConversationSortKey variantSortKey; +@property (nonatomic) TGConversationSortKey importantSortKey; +@property (nonatomic) TGConversationSortKey unimportantSortKey; +@property (nonatomic) int32_t pts; + +@property (nonatomic) int32_t maxReadMessageId; +@property (nonatomic) int32_t maxOutgoingReadMessageId; +@property (nonatomic) int32_t maxKnownMessageId; +@property (nonatomic) int32_t maxLocallyReadMessageId; + +@property (nonatomic) int32_t maxReadDate; +@property (nonatomic) int32_t maxOutgoingReadDate; + +@property (nonatomic, strong) NSString *about; +@property (nonatomic, strong) NSString *username; + +@property (nonatomic) id additionalProperties; + +@property (nonatomic) bool outgoing; +@property (nonatomic) bool unread; +@property (nonatomic) bool deliveryError; +@property (nonatomic) TGMessageDeliveryState deliveryState; +@property (nonatomic) int32_t messageDate; +@property (nonatomic) int32_t minMessageDate; +@property (nonatomic) int32_t pinnedDate; +@property (nonatomic) int fromUid; +@property (nonatomic, strong) NSString *text; +@property (nonatomic, strong) NSArray *media; +@property (nonatomic, strong) NSData *mediaData; +@property (nonatomic) int64_t messageFlags; + +@property (nonatomic) bool unreadMark; + +@property (nonatomic) int unreadCount; +@property (nonatomic) int serviceUnreadCount; + +@property (nonatomic, strong) NSString *chatTitle; +@property (nonatomic, strong) NSString *chatPhotoSmall; +@property (nonatomic, strong) NSString *chatPhotoMedium; +@property (nonatomic, strong) NSString *chatPhotoBig; +@property (nonatomic) NSData *chatPhotoFileReferenceSmall; +@property (nonatomic) NSData *chatPhotoFileReferenceBig; + +@property (nonatomic, strong) NSString *chatPhotoFullSmall; +@property (nonatomic, strong) NSString *chatPhotoFullBig; + +@property (nonatomic) int chatParticipantCount; + +@property (nonatomic) bool leftChat; +@property (nonatomic) bool kickedFromChat; + +@property (nonatomic) TGChannelRole channelRole; + +@property (nonatomic) int chatCreationDate; +@property (nonatomic) int chatVersion; +@property (nonatomic) bool chatIsAdmin; +@property (nonatomic) bool channelIsReadOnly; +@property (nonatomic) bool isVerified; +@property (nonatomic) bool hasExplicitContent; +@property (nonatomic, strong) NSString *restrictionReason; + +@property (nonatomic, strong) TGConversationParticipantsData *chatParticipants; + +@property (nonatomic, strong) NSDictionary *dialogListData; + +@property (nonatomic) bool isChat; +@property (nonatomic) bool isDeleted; +@property (nonatomic) bool isBroadcast; +@property (nonatomic) bool isChannel; + +@property (nonatomic) bool postAsChannel; +@property (nonatomic) bool hasAdmins; +@property (nonatomic) bool isAdmin; +@property (nonatomic) bool isCreator; +@property (nonatomic) bool isChannelGroup; +@property (nonatomic) bool everybodyCanAddMembers; +@property (nonatomic) bool signaturesEnabled; + +@property (nonatomic) bool isDeactivated; +@property (nonatomic) bool isMigrated; +@property (nonatomic) int32_t migratedToChannelId; +@property (nonatomic) int64_t migratedToChannelAccessHash; + +@property (nonatomic) int32_t pinnedMessageId; +@property (nonatomic) bool pinnedMessageHidden; + +@property (nonatomic) int64_t flags; + +@property (nonatomic) bool isMin; +@property (nonatomic) bool canNotSetUsername; + +@property (nonatomic, strong) TGEncryptedConversationData *encryptedData; + +@property (nonatomic, strong, readonly) TGDatabaseMessageDraft *draft; +@property (nonatomic) int32_t unreadMentionCount; + +@property (nonatomic, readonly) int32_t date; +@property (nonatomic, readonly) int32_t unpinnedDate; + +@property (nonatomic, strong) TGChannelAdminRights *channelAdminRights; +@property (nonatomic, strong) TGChannelBannedRights *channelBannedRights; + +@property (nonatomic, strong) NSNumber *feedId; +- (int64_t)conversationFeedId; + +- (id)initWithConversationId:(int64_t)conversationId unreadCount:(int)unreadCount serviceUnreadCount:(int)serviceUnreadCount; + +- (void)mergeMessage:(TGMessage *)message; +- (void)mergeEmptyMessage; + +- (BOOL)isEqualToConversation:(TGConversation *)other; +- (BOOL)isEqualToConversationIgnoringMessage:(TGConversation *)other; + +- (NSData *)serializeChatPhoto; +- (void)deserializeChatPhoto:(NSData *)data; + +- (bool)isEncrypted; + +- (void)mergeConversation:(TGConversation *)conversation; +- (void)mergeChannel:(TGConversation *)channel; +- (void)mergeDraft:(TGDatabaseMessageDraft *)draft; + +- (bool)currentUserCanSendMessages; + ++ (NSString *)chatTitleForDecoder:(PSKeyValueCoder *)coder; + +- (bool)isMessageUnread:(TGMessage *)message; +- (bool)isMessageUnread:(int32_t)messageId date:(int32_t)messageDate outgoing:(bool)outgoing; + +- (bool)pinnedToTop; + +- (int32_t)searchMessageId; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGConversation.m b/submodules/LegacyComponents/LegacyComponents/TGConversation.m new file mode 100644 index 0000000000..6bdd274cb8 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGConversation.m @@ -0,0 +1,1414 @@ +#import "TGConversation.h" + +#import "LegacyComponentsInternal.h" +#import "TGStringUtils.h" +#import "TGMessage.h" + +#import "PSKeyValueCoder.h" + +#import "TGPeerIdAdapter.h" + +#import "TGImageInfo.h" +#import "TGMediaOriginInfo.h" + +@implementation TGEncryptedConversationData + +- (BOOL)isEqualToEncryptedData:(TGEncryptedConversationData *)other +{ + if (_encryptedConversationId != other->_encryptedConversationId || _accessHash != other->_accessHash || _keyFingerprint != other->_keyFingerprint || _handshakeState != other->_handshakeState || _currentRekeyExchangeId != other->_currentRekeyExchangeId) + return false; + + return true; +} + +- (id)copyWithZone:(NSZone *)__unused zone +{ + TGEncryptedConversationData *data = [[TGEncryptedConversationData alloc] init]; + data->_encryptedConversationId = _encryptedConversationId; + data->_accessHash = _accessHash; + data->_keyFingerprint = _keyFingerprint; + data->_handshakeState = _handshakeState; + data->_currentRekeyExchangeId = _currentRekeyExchangeId; + data->_currentRekeyIsInitiatedByLocalClient = _currentRekeyIsInitiatedByLocalClient; + data->_currentRekeyNumber = _currentRekeyNumber; + data->_currentRekeyKey = _currentRekeyKey; + data->_currentRekeyKeyId = _currentRekeyKeyId; + + return data; +} + +- (void)serialize:(NSMutableData *)data +{ + uint8_t version = 3; + [data appendBytes:&version length:1]; + [data appendBytes:&_encryptedConversationId length:8]; + [data appendBytes:&_accessHash length:8]; + [data appendBytes:&_keyFingerprint length:8]; + [data appendBytes:&_handshakeState length:4]; + [data appendBytes:&_currentRekeyExchangeId length:8]; + uint8_t currentRekeyIsInitiatedByLocalClient = _currentRekeyIsInitiatedByLocalClient ? 1 : 0; + [data appendBytes:¤tRekeyIsInitiatedByLocalClient length:1]; + int32_t currentRekeyNumberLength = (int32_t)_currentRekeyNumber.length; + [data appendBytes:¤tRekeyNumberLength length:4]; + if (_currentRekeyNumber != nil) + [data appendData:_currentRekeyNumber]; + int32_t currentRekeyKeyLength = (int32_t)_currentRekeyKey.length; + [data appendBytes:¤tRekeyKeyLength length:4]; + if (_currentRekeyKey != nil) + [data appendData:_currentRekeyKey]; + [data appendBytes:&_currentRekeyKeyId length:8]; +} + ++ (TGEncryptedConversationData *)deserialize:(NSData *)data ptr:(int *)ptr +{ + uint8_t version = 0; + [data getBytes:&version range:NSMakeRange(*ptr, 1)]; + (*ptr) += 1; + + if (version != 1 && version != 2 && version != 3) + { + TGLegacyLog(@"***** Invalid encrypted data version"); + return nil; + } + + TGEncryptedConversationData *encryptedData = [TGEncryptedConversationData new]; + + [data getBytes:&encryptedData->_encryptedConversationId range:NSMakeRange(*ptr, 8)]; + (*ptr) += 8; + + [data getBytes:&encryptedData->_accessHash range:NSMakeRange(*ptr, 8)]; + (*ptr) += 8; + + [data getBytes:&encryptedData->_keyFingerprint range:NSMakeRange(*ptr, 8)]; + (*ptr) += 8; + + if (version >= 2) + { + [data getBytes:&encryptedData->_handshakeState range:NSMakeRange(*ptr, 4)]; + *ptr += 4; + } + + if (version >= 3) + { + [data getBytes:&encryptedData->_currentRekeyExchangeId range:NSMakeRange(*ptr, 8)]; + *ptr += 8; + + uint8_t currentRekeyIsInitiatedByLocalClient = 0; + [data getBytes:¤tRekeyIsInitiatedByLocalClient range:NSMakeRange(*ptr, 1)]; + encryptedData->_currentRekeyIsInitiatedByLocalClient = currentRekeyIsInitiatedByLocalClient; + *ptr += 1; + + int32_t currentRekeyNumberLength = 0; + [data getBytes:¤tRekeyNumberLength range:NSMakeRange(*ptr, 4)]; + *ptr += 4; + + if (currentRekeyNumberLength != 0) + { + encryptedData->_currentRekeyNumber = [data subdataWithRange:NSMakeRange(*ptr, currentRekeyNumberLength)]; + *ptr += currentRekeyNumberLength; + } + + int32_t currentRekeyKeyLength = 0; + [data getBytes:¤tRekeyKeyLength range:NSMakeRange(*ptr, 4)]; + *ptr += 4; + + if (currentRekeyKeyLength != 0) + { + encryptedData->_currentRekeyKey = [data subdataWithRange:NSMakeRange(*ptr, currentRekeyKeyLength)]; + *ptr += currentRekeyKeyLength; + } + + [data getBytes:&encryptedData->_currentRekeyKeyId range:NSMakeRange(*ptr, 8)]; + *ptr += 8; + } + + return encryptedData; +} + +@end + +@implementation TGConversationParticipantsData + +- (id)init +{ + self = [super init]; + if (self != nil) + { + _serializedData = nil; + } + return self; +} + +- (id)copyWithZone:(NSZone *)__unused zone +{ + TGConversationParticipantsData *participantsData = [[TGConversationParticipantsData alloc] init]; + + participantsData.chatAdminId = _chatAdminId; + participantsData.chatInvitedBy = _chatInvitedBy; + participantsData.chatInvitedDates = _chatInvitedDates; + participantsData.chatParticipantUids = _chatParticipantUids; + participantsData.chatParticipantSecretChatPeerIds = _chatParticipantSecretChatPeerIds; + participantsData.chatParticipantChatPeerIds = _chatParticipantChatPeerIds; + participantsData.chatAdminUids = _chatAdminUids; + participantsData.version = _version; + participantsData.exportedChatInviteString = _exportedChatInviteString; + + return participantsData; +} + +- (void)addParticipantWithId:(int32_t)uid invitedBy:(int32_t)invitedBy date:(int32_t)date +{ + NSMutableArray *chatParticipantUids = [[NSMutableArray alloc] initWithArray:_chatParticipantUids]; + if (![chatParticipantUids containsObject:@(uid)]) + { + [chatParticipantUids addObject:@(uid)]; + _chatParticipantUids = chatParticipantUids; + + NSMutableDictionary *chatInvitedBy = [[NSMutableDictionary alloc] initWithDictionary:_chatInvitedBy]; + chatInvitedBy[@(uid)] = @(invitedBy); + _chatInvitedBy = chatInvitedBy; + + NSMutableDictionary *chatInvitedDates = [[NSMutableDictionary alloc] initWithDictionary:_chatInvitedDates]; + chatInvitedDates[@(uid)] = @(date); + _chatInvitedDates = chatInvitedDates; + } +} + +- (void)removeParticipantWithId:(int32_t)uid +{ + NSMutableArray *chatParticipantUids = [[NSMutableArray alloc] initWithArray:_chatParticipantUids]; + [chatParticipantUids removeObject:@(uid)]; + _chatParticipantUids = chatParticipantUids; + + NSMutableDictionary *chatInvitedBy = [[NSMutableDictionary alloc] initWithDictionary:_chatInvitedBy]; + [chatInvitedBy removeObjectForKey:@(uid)]; + _chatInvitedBy = chatInvitedBy; + + NSMutableDictionary *chatInvitedDates = [[NSMutableDictionary alloc] initWithDictionary:_chatInvitedDates]; + [chatInvitedDates removeObjectForKey:@(uid)]; + _chatInvitedDates = chatInvitedDates; + + NSMutableSet *chatAdminUids = [[NSMutableSet alloc] initWithSet:_chatAdminUids]; + [chatAdminUids removeObject:@(uid)]; + _chatAdminUids = chatAdminUids; +} + +- (void)addSecretChatPeerWithId:(int64_t)peerId +{ + NSMutableArray *chatParticipantSecretChatPeerIds = [[NSMutableArray alloc] initWithArray:_chatParticipantSecretChatPeerIds]; + if (![chatParticipantSecretChatPeerIds containsObject:@(peerId)]) + { + [chatParticipantSecretChatPeerIds addObject:@(peerId)]; + _chatParticipantSecretChatPeerIds = chatParticipantSecretChatPeerIds; + } +} + +- (void)removeSecretChatPeerWithId:(int64_t)peerId +{ + NSMutableArray *chatParticipantSecretChatPeerIds = [[NSMutableArray alloc] initWithArray:_chatParticipantSecretChatPeerIds]; + [chatParticipantSecretChatPeerIds removeObject:@(peerId)]; + _chatParticipantSecretChatPeerIds = chatParticipantSecretChatPeerIds; +} + +- (void)addChatPeerWithId:(int64_t)peerId +{ + NSMutableArray *chatParticipantChatPeerIds = [[NSMutableArray alloc] initWithArray:_chatParticipantChatPeerIds]; + if (![chatParticipantChatPeerIds containsObject:@(peerId)]) + { + [chatParticipantChatPeerIds addObject:@(peerId)]; + _chatParticipantChatPeerIds = chatParticipantChatPeerIds; + } +} + +- (void)removeChatPeerWithId:(int64_t)peerId +{ + NSMutableArray *chatParticipantChatPeerIds = [[NSMutableArray alloc] initWithArray:_chatParticipantChatPeerIds]; + [chatParticipantChatPeerIds removeObject:@(peerId)]; + _chatParticipantChatPeerIds = chatParticipantChatPeerIds; +} + ++ (TGConversationParticipantsData *)deserializeData:(NSData *)data +{ + TGConversationParticipantsData *participantsData = [[TGConversationParticipantsData alloc] init]; + + int length = (int)data.length; + int ptr = 0; + if (ptr + 12 > length) + { + return nil; + } + + int version = 0; + [data getBytes:&version range:NSMakeRange(ptr, 4)]; + ptr += 4; + + int32_t formatVersion = 0; + if (version == (int)0xabcdef12) + { + [data getBytes:&formatVersion range:NSMakeRange(ptr, 4)]; + ptr += 4; + + [data getBytes:&version range:NSMakeRange(ptr, 4)]; + ptr += 4; + } + + int adminId = 0; + [data getBytes:&adminId range:NSMakeRange(ptr, 4)]; + ptr += 4; + + int count = 0; + [data getBytes:&count range:NSMakeRange(ptr, 4)]; + ptr += 4; + + NSMutableArray *uids = [[NSMutableArray alloc] init]; + NSMutableDictionary *invitedBy = [[NSMutableDictionary alloc] init]; + NSMutableDictionary *invitedDates = [[NSMutableDictionary alloc] init]; + + for (int i = 0; i < count; i++) + { + if (ptr + 4 > length) + { + TGLegacyLog(@"***** Invalid participants data"); + return nil; + } + + int uid = 0; + [data getBytes:&uid range:NSMakeRange(ptr, 4)]; + ptr += 4; + + if (ptr + 4 > length) + { + TGLegacyLog(@"***** Invalid participants data"); + return nil; + } + int inviter = 0; + [data getBytes:&inviter range:NSMakeRange(ptr, 4)]; + ptr += 4; + + if (ptr + 4 > length) + { + TGLegacyLog(@"***** Invalid participants data"); + return nil; + } + int date = 0; + [data getBytes:&date range:NSMakeRange(ptr, 4)]; + ptr += 4; + + NSNumber *nUid = [[NSNumber alloc] initWithInt:uid]; + + [uids addObject:nUid]; + [invitedBy setObject:[[NSNumber alloc] initWithInt:inviter] forKey:nUid]; + [invitedDates setObject:[[NSNumber alloc] initWithInt:date] forKey:nUid]; + } + + NSMutableArray *chatParticipantSecretChatPeerIds = [[NSMutableArray alloc] init]; + + if (formatVersion >= 1) + { + int count = 0; + [data getBytes:&count range:NSMakeRange(ptr, 4)]; + ptr += 4; + + for (int i = 0; i < count; i++) + { + if (ptr + 8 > length) + { + TGLegacyLog(@"***** Invalid participants data"); + return nil; + } + + int64_t peerId = 0; + [data getBytes:&peerId range:NSMakeRange(ptr, 8)]; + ptr += 8; + + [chatParticipantSecretChatPeerIds addObject:@(peerId)]; + } + } + + NSMutableArray *chatParticipantChatPeerIds = [[NSMutableArray alloc] init]; + + if (formatVersion >= 2) + { + int count = 0; + [data getBytes:&count range:NSMakeRange(ptr, 4)]; + ptr += 4; + + for (int i = 0; i < count; i++) + { + if (ptr + 8 > length) + { + TGLegacyLog(@"***** Invalid participants data"); + return nil; + } + + int64_t peerId = 0; + [data getBytes:&peerId range:NSMakeRange(ptr, 8)]; + ptr += 8; + + [chatParticipantChatPeerIds addObject:@(peerId)]; + } + } + + if (formatVersion >= 3) + { + int32_t length = 0; + [data getBytes:&length range:NSMakeRange(ptr, 4)]; + ptr += 4; + + NSData *linkData = [data subdataWithRange:NSMakeRange(ptr, length)]; + ptr += length; + + participantsData.exportedChatInviteString = [[NSString alloc] initWithData:linkData encoding:NSUTF8StringEncoding]; + } + + if (formatVersion >= 4) { + int32_t length = 0; + [data getBytes:&length range:NSMakeRange(ptr, 4)]; + ptr += 4; + + NSMutableSet *chatAdminUids = [[NSMutableSet alloc] init]; + for (int32_t i = 0; i < length; i++) { + int32_t item = 0; + [data getBytes:&item range:NSMakeRange(ptr, 4)]; + ptr += 4; + [chatAdminUids addObject:@(item)]; + } + + participantsData.chatAdminUids = chatAdminUids; + } + + participantsData.version = version; + participantsData.chatAdminId = adminId; + participantsData.chatParticipantUids = uids; + participantsData.chatInvitedBy = invitedBy; + participantsData.chatInvitedDates = invitedDates; + participantsData.chatParticipantSecretChatPeerIds = chatParticipantSecretChatPeerIds; + participantsData.chatParticipantChatPeerIds = chatParticipantChatPeerIds; + + return participantsData; +} + +- (NSData *)serializedData +{ + if (_serializedData == nil) + { + NSMutableData *data = [[NSMutableData alloc] init]; + + int32_t magic = 0xabcdef12; + [data appendBytes:&magic length:4]; + + int32_t formatVersion = 4; + [data appendBytes:&formatVersion length:4]; + + [data appendBytes:&_version length:4]; + [data appendBytes:&_chatAdminId length:4]; + + int count = (int)_chatParticipantUids.count; + [data appendBytes:&count length:4]; + for (NSNumber *nUid in _chatParticipantUids) + { + int uid = [nUid intValue]; + [data appendBytes:&uid length:4]; + + int invitedBy = [[_chatInvitedBy objectForKey:nUid] intValue]; + [data appendBytes:&invitedBy length:4]; + + int invitedDate = [[_chatInvitedDates objectForKey:nUid] intValue]; + [data appendBytes:&invitedDate length:4]; + } + + int32_t chatParticipantSecretChatPeerIdsCount = (int32_t)_chatParticipantSecretChatPeerIds.count; + [data appendBytes:&chatParticipantSecretChatPeerIdsCount length:4]; + + for (NSNumber *nPeerId in _chatParticipantSecretChatPeerIds) + { + int64_t peerId = [nPeerId longLongValue]; + [data appendBytes:&peerId length:8]; + } + + int32_t chatParticipantChatPeerIdsCount = (int32_t)_chatParticipantChatPeerIds.count; + [data appendBytes:&chatParticipantChatPeerIdsCount length:4]; + + for (NSNumber *nPeerId in _chatParticipantChatPeerIds) + { + int64_t peerId = [nPeerId longLongValue]; + [data appendBytes:&peerId length:8]; + } + + int32_t linkLength = (int32_t)_exportedChatInviteString.length; + [data appendBytes:&linkLength length:4]; + if (linkLength != 0) + [data appendData:[_exportedChatInviteString dataUsingEncoding:NSUTF8StringEncoding]]; + + int32_t chatAdminUidsCount = (int32_t)_chatAdminUids.count; + [data appendBytes:&chatAdminUidsCount length:4]; + for (NSNumber *nUid in _chatAdminUids) { + int32_t uid = [nUid intValue]; + [data appendBytes:&uid length:4]; + } + + _serializedData = data; + } + + return _serializedData; +} + +@end + +#pragma mark - + +@implementation TGConversation + +- (instancetype)initWithConversationId:(int64_t)conversationId unreadCount:(int)unreadCount serviceUnreadCount:(int)serviceUnreadCount +{ + self = [super init]; + if (self != nil) + { + _conversationId = conversationId; + _unreadCount = unreadCount; + _serviceUnreadCount = serviceUnreadCount; + } + return self; +} + +- (instancetype)initWithKeyValueCoder:(PSKeyValueCoder *)coder +{ + self = [super init]; + if (self != nil) { + _conversationId = [coder decodeInt64ForCKey:"i"]; + _accessHash = [coder decodeInt64ForCKey:"ah"]; + _displayVariant = [coder decodeInt32ForCKey:"dv"]; + _kind = (uint8_t)[coder decodeInt32ForCKey:"kind"]; + _pts = [coder decodeInt32ForCKey:"pts"]; + _variantSortKey = TGConversationSortKeyDecode(coder, "vsort"); + _importantSortKey = TGConversationSortKeyDecode(coder, "isort"); + _unimportantSortKey = TGConversationSortKeyDecode(coder, "usort"); + _maxReadMessageId = [coder decodeInt32ForCKey:"mread"]; + _maxOutgoingReadMessageId = [coder decodeInt32ForCKey:"moutread"]; + _maxKnownMessageId = [coder decodeInt32ForCKey:"mknown"]; + _maxLocallyReadMessageId = [coder decodeInt32ForCKey:"mlr"]; + _maxReadDate = [coder decodeInt32ForCKey:"mrd"]; + _maxOutgoingReadDate = [coder decodeInt32ForCKey:"mrod"]; + _about = [coder decodeStringForCKey:"about"]; + _username = [coder decodeStringForCKey:"username"]; + _outgoing = [coder decodeInt32ForCKey:"out"]; + _unread = [coder decodeInt32ForCKey:"unr"]; + _deliveryError = [coder decodeInt32ForCKey:"der"]; + _deliveryState = [coder decodeInt32ForCKey:"ds"]; + _messageDate = [coder decodeInt32ForCKey:"date"]; + _fromUid = [coder decodeInt32ForCKey:"from"]; + _text = [coder decodeStringForCKey:"text"]; + _media = [TGMessage parseMediaAttachments:[coder decodeDataCorCKey:"media"]]; + _unreadCount = [coder decodeInt32ForCKey:"ucount"]; + _serviceUnreadCount = [coder decodeInt32ForCKey:"sucount"]; + _chatTitle = [coder decodeStringForCKey:"ct"]; + _chatPhotoSmall = [coder decodeStringForCKey:"cp.s"]; + _chatPhotoMedium = [coder decodeStringForCKey:"cp.m"]; + _chatPhotoBig = [coder decodeStringForCKey:"cp.l"]; + _chatPhotoFileReferenceSmall = [coder decodeDataCorCKey:"cp.frs"]; + _chatPhotoFileReferenceBig = [coder decodeDataCorCKey:"cp.frb"]; + _chatParticipants = nil; + _chatParticipantCount = 0; + _chatVersion = [coder decodeInt32ForCKey:"ver"]; + _chatIsAdmin = [coder decodeInt32ForCKey:"adm"]; + _channelRole = [coder decodeInt32ForCKey:"role"]; + _channelIsReadOnly = [coder decodeInt32ForCKey:"ro"]; + _flags = [coder decodeInt64ForCKey:"flags"]; + _leftChat = [coder decodeInt32ForCKey:"lef"]; + _kickedFromChat = [coder decodeInt32ForCKey:"kk"]; + _isChat = false; + _isChannel = true; + _isDeleted = false; + _encryptedData = nil; + _isBroadcast = false; + _migratedToChannelId = [coder decodeInt32ForCKey:"mtci"]; + _migratedToChannelAccessHash = [coder decodeInt64ForCKey:"mtch"]; + _restrictionReason = [coder decodeStringForCKey:"rr"]; + _pinnedMessageId = [coder decodeInt32ForCKey:"pmi"]; + _chatCreationDate = [coder decodeInt32ForCKey:"ccd"]; + _pinnedDate = [coder decodeInt32ForCKey:"pdt"]; + _channelAdminRights = [coder decodeObjectForCKey:"car"]; + _channelBannedRights = [coder decodeObjectForCKey:"cbr"]; + _messageFlags = [coder decodeInt64ForCKey:"mf"]; + + int32_t feedId = [coder decodeInt32ForCKey:"fi"]; + _feedId = feedId != -1 ? @(feedId) : nil; + + _unreadMark = [coder decodeInt32ForCKey:"unrm"]; + } + return self; +} + +- (void)encodeWithKeyValueCoder:(PSKeyValueCoder *)coder { + [coder encodeInt64:_conversationId forCKey:"i"]; + [coder encodeInt64:_accessHash forCKey:"ah"]; + [coder encodeInt32:_displayVariant forCKey:"dv"]; + [coder encodeInt32:_kind forCKey:"kind"]; + [coder encodeInt32:_pts forCKey:"pts"]; + TGConversationSortKeyEncode(coder, "vsort", _variantSortKey); + TGConversationSortKeyEncode(coder, "isort", _importantSortKey); + TGConversationSortKeyEncode(coder, "usort", _unimportantSortKey); + [coder encodeInt32:_maxReadMessageId forCKey:"mread"]; + [coder encodeInt32:_maxOutgoingReadMessageId forCKey:"moutread"]; + [coder encodeInt32:_maxKnownMessageId forCKey:"mknown"]; + [coder encodeInt32:_maxLocallyReadMessageId forCKey:"mlr"]; + [coder encodeInt32:_maxReadDate forCKey:"mrd"]; + [coder encodeInt32:_maxOutgoingReadDate forCKey:"mrod"]; + [coder encodeString:_about forCKey:"about"]; + [coder encodeString:_username forCKey:"username"]; + [coder encodeInt32:_outgoing ? 1 : 0 forCKey:"out"]; + [coder encodeInt32:_unread ? 1 : 0 forCKey:"unr"]; + [coder encodeInt32:_deliveryError ? 1 : 0 forCKey:"der"]; + [coder encodeInt32:_deliveryState forCKey:"ds"]; + [coder encodeInt32:_messageDate forCKey:"date"]; + [coder encodeInt32:_fromUid forCKey:"from"]; + [coder encodeString:_text forCKey:"text"]; + [coder encodeData:[TGMessage serializeMediaAttachments:true attachments:_media] forCKey:"media"]; + [coder encodeInt32:_unreadCount forCKey:"ucount"]; + [coder encodeInt32:_serviceUnreadCount forCKey:"sucount"]; + [coder encodeString:_chatTitle forCKey:"ct"]; + [coder encodeString:_chatPhotoSmall forCKey:"cp.s"]; + [coder encodeString:_chatPhotoMedium forCKey:"cp.m"]; + [coder encodeString:_chatPhotoBig forCKey:"cp.l"]; + [coder encodeData:_chatPhotoFileReferenceSmall forCKey:"cp.frs"]; + [coder encodeData:_chatPhotoFileReferenceBig forCKey:"cp.frb"]; + [coder encodeInt32:_chatVersion forCKey:"ver"]; + [coder encodeInt32:_chatIsAdmin ? 1 : 0 forCKey:"adm"]; + [coder encodeInt32:_channelRole forCKey:"role"]; + [coder encodeInt32:_channelIsReadOnly ? 1 : 0 forCKey:"ro"]; + [coder encodeInt64:_flags forCKey:"flags"]; + [coder encodeInt32:_leftChat forCKey:"lef"]; + [coder encodeInt32:_kickedFromChat forCKey:"kk"]; + [coder encodeInt32:_migratedToChannelId forCKey:"mtci"]; + [coder encodeInt64:_migratedToChannelAccessHash forCKey:"mtch"]; + [coder encodeString:_restrictionReason forCKey:"rr"]; + [coder encodeInt32:_pinnedMessageId forCKey:"pmi"]; + [coder encodeInt32:_chatCreationDate forCKey:"ccd"]; + [coder encodeObject:_channelAdminRights forCKey:"car"]; + [coder encodeObject:_channelBannedRights forCKey:"cbr"]; + [coder encodeInt64:_messageFlags forCKey:"mf"]; + [coder encodeInt32:_feedId != nil ? _feedId.intValue : -1 forCKey:"fi"]; + [coder encodeInt32:_unreadMark ? 1 : 0 forCKey:"unrm"]; +} + +- (id)copyWithZone:(NSZone *)__unused zone +{ + TGConversation *conversation = [[TGConversation alloc] init]; + + conversation.conversationId = _conversationId; + conversation.accessHash = _accessHash; + conversation.displayVariant = _displayVariant; + conversation->_kind = _kind; + conversation.pts = _pts; + conversation.variantSortKey = _variantSortKey; + conversation.importantSortKey = _importantSortKey; + conversation.unimportantSortKey = _unimportantSortKey; + conversation.maxReadMessageId = _maxReadMessageId; + conversation.maxOutgoingReadMessageId = _maxOutgoingReadMessageId; + conversation.maxKnownMessageId = _maxKnownMessageId; + conversation.maxLocallyReadMessageId = _maxLocallyReadMessageId; + conversation.maxReadDate = _maxReadDate; + conversation.maxOutgoingReadDate = _maxOutgoingReadDate; + conversation.about = _about; + conversation.username = _username; + conversation.outgoing = _outgoing; + conversation.unread = _unread; + conversation.deliveryError = _deliveryError; + conversation.deliveryState = _deliveryState; + conversation.messageDate = _messageDate; + conversation.minMessageDate = _minMessageDate; + conversation.fromUid = _fromUid; + conversation.text = _text; + conversation.media = _media; + conversation.unreadCount = _unreadCount; + conversation.serviceUnreadCount = _serviceUnreadCount; + conversation.chatTitle = _chatTitle; + conversation.chatPhotoSmall = _chatPhotoSmall; + conversation.chatPhotoMedium = _chatPhotoMedium; + conversation.chatPhotoBig = _chatPhotoBig; + conversation.chatPhotoFileReferenceSmall = _chatPhotoFileReferenceSmall; + conversation.chatPhotoFileReferenceBig = _chatPhotoFileReferenceBig; + conversation.chatParticipants = [_chatParticipants copy]; + conversation.chatParticipantCount = _chatParticipantCount; + conversation.chatVersion = _chatVersion; + conversation.chatIsAdmin = _chatIsAdmin; + conversation.channelRole = _channelRole; + conversation.leftChat = _leftChat; + conversation.kickedFromChat = _kickedFromChat; + conversation.dialogListData = _dialogListData; + conversation.isChat = _isChat; + conversation.isDeleted = _isDeleted; + conversation.restrictionReason = _restrictionReason; + conversation->_chatCreationDate = _chatCreationDate; + conversation->_unreadMark = _unreadMark; + + conversation.encryptedData = _encryptedData == nil ? nil : [_encryptedData copy]; + + conversation.isBroadcast = _isBroadcast; + conversation.isChannel = _isChannel; + conversation.channelIsReadOnly = _channelIsReadOnly; + conversation.flags = _flags; + conversation.migratedToChannelId = _migratedToChannelId; + conversation.migratedToChannelAccessHash = _migratedToChannelAccessHash; + conversation.pinnedMessageId = _pinnedMessageId; + + conversation->_draft = _draft; + conversation->_unreadMentionCount = _unreadMentionCount; + conversation.pinnedDate = _pinnedDate; + + conversation->_channelAdminRights = _channelAdminRights; + conversation->_channelBannedRights = _channelBannedRights; + + conversation->_messageFlags = _messageFlags; + + conversation->_feedId = _feedId; + + return conversation; +} + +- (void)setKind:(uint8_t)kind { + if (_kind != kind || kind != TGConversationSortKeyKind(_variantSortKey)) { + _kind = kind; + + _variantSortKey = TGConversationSortKeyUpdateKind(_variantSortKey, kind); + _importantSortKey = TGConversationSortKeyUpdateKind(_importantSortKey, kind); + _unimportantSortKey = TGConversationSortKeyUpdateKind(_unimportantSortKey, kind); + } +} + +- (void)setVariantSortKey:(TGConversationSortKey)variantSortKey { + _variantSortKey = variantSortKey; + + _messageDate = TGConversationSortKeyTimestamp(variantSortKey); +} + +- (void)mergeMessage:(TGMessage *)message +{ + _outgoing = message.outgoing; + _messageDate = (int)message.date; + _fromUid = (int)message.fromUid; + _text = message.text; + _media = message.mediaAttachments; + _unread = [self isMessageUnread:message]; + _deliveryError = message.deliveryState == TGMessageDeliveryStateFailed; + _deliveryState = message.deliveryState; + _messageFlags = message.flags; + if (_maxKnownMessageId > TGMessageLocalMidBaseline) + _maxKnownMessageId = 0; + if (message.mid < TGMessageLocalMidBaseline && (message.mid > _maxKnownMessageId)) + _maxKnownMessageId = message.mid; +} + +- (void)mergeEmptyMessage { + _outgoing = false; + _fromUid = 0; + _text = nil; + _media = nil; + _unread = false; + _deliveryError = false; + _deliveryState = TGMessageDeliveryStateDelivered; + _messageFlags = 0; +} + +- (NSData *)mediaData +{ + if (_mediaData != nil) + return _mediaData; + + _mediaData = [TGMessage serializeMediaAttachments:false attachments:_media]; + return _mediaData; +} + +- (BOOL)isEqual:(id)object { + return [object isKindOfClass:[TGConversation class]] && [((TGConversation *)object) isEqualToConversation:self]; +} + +- (BOOL)isEqualToConversation:(TGConversation *)other +{ + if (_conversationId != other.conversationId || _outgoing != other.outgoing || _messageDate != other.messageDate || _fromUid != other.fromUid || ![_text isEqualToString:other.text] || _unreadCount != other.unreadCount || _serviceUnreadCount != other.serviceUnreadCount || _unread != other.unread || _isChat != other.isChat || _deliveryError != other.deliveryError || _deliveryState != other.deliveryState) + return false; + + if (_media.count != other.media.count) + return false; + if (_media != nil && ![self.mediaData isEqualToData:other.mediaData]) + return false; + + if (_isChat) + { + if (![_chatTitle isEqualToString:other.chatTitle] || _chatVersion != other.chatVersion || _leftChat != other.leftChat || _kickedFromChat != other.kickedFromChat || + (((_chatParticipants != nil) != (other.chatParticipants != nil)) || (_chatParticipants != nil && ![_chatParticipants.serializedData isEqualToData:other.chatParticipants.serializedData])) + ) + return false; + if ((_chatPhotoSmall != nil) != (other.chatPhotoSmall != nil) || (_chatPhotoSmall != nil && ![_chatPhotoSmall isEqualToString:other.chatPhotoSmall])) + return false; + if ((_chatPhotoMedium != nil) != (other.chatPhotoMedium != nil) || (_chatPhotoMedium != nil && ![_chatPhotoMedium isEqualToString:other.chatPhotoMedium])) + return false; + if ((_chatPhotoBig != nil) != (other.chatPhotoBig != nil) || (_chatPhotoBig != nil && ![_chatPhotoBig isEqualToString:other.chatPhotoBig])) + return false; + + if (!TGObjectCompare(other.chatPhotoFileReferenceSmall, _chatPhotoFileReferenceSmall) || !TGObjectCompare(other.chatPhotoFileReferenceBig, _chatPhotoFileReferenceBig)) + return false; + } + + if (_encryptedData != nil || other->_encryptedData != nil) + { + if ((_encryptedData != nil) != (other->_encryptedData != nil) || (_encryptedData != nil && ![_encryptedData isEqualToEncryptedData:other->_encryptedData])) + return false; + } + + if (_flags != other->_flags) { + return false; + } + + if (_pinnedMessageId != other->_pinnedMessageId) { + return false; + } + + if (!TGStringCompare(_restrictionReason, other->_restrictionReason)) { + return false; + } + + if (_maxReadDate != other->_maxReadDate) { + return false; + } + + if (_maxReadMessageId != other->_maxReadMessageId) { + return false; + } + + if (_maxOutgoingReadDate != other->_maxOutgoingReadDate) { + return false; + } + + if (_maxOutgoingReadMessageId != other->_maxOutgoingReadMessageId) { + return false; + } + + if (_maxKnownMessageId != other->_maxKnownMessageId) { + return false; + } + + if (!TGObjectCompare(_draft, other->_draft)) { + return false; + } + + if (_unreadMentionCount != other->_unreadMentionCount) { + return false; + } + + if (_pinnedDate != other->_pinnedDate) { + return false; + } + + if (!TGObjectCompare(_channelAdminRights, other->_channelAdminRights)) { + return false; + } + + if (!TGObjectCompare(_channelBannedRights, other->_channelBannedRights)) { + return false; + } + + return true; +} + +- (BOOL)isEqualToConversationIgnoringMessage:(TGConversation *)other +{ + if (_conversationId != other.conversationId || _isChat != other.isChat) + return false; + + if (_isChat) + { + if (![_chatTitle isEqualToString:other.chatTitle] || _chatVersion != other.chatVersion || _leftChat != other.leftChat || _kickedFromChat != other.kickedFromChat || + (((_chatParticipants != nil) != (other.chatParticipants != nil)) || (_chatParticipants != nil && ![_chatParticipants.serializedData isEqualToData:other.chatParticipants.serializedData])) + ) + return false; + if ((_chatPhotoSmall != nil) != (other.chatPhotoSmall != nil) || (_chatPhotoSmall != nil && ![_chatPhotoSmall isEqualToString:other.chatPhotoSmall])) + return false; + if ((_chatPhotoMedium != nil) != (other.chatPhotoMedium != nil) || (_chatPhotoMedium != nil && ![_chatPhotoMedium isEqualToString:other.chatPhotoMedium])) + return false; + if ((_chatPhotoBig != nil) != (other.chatPhotoBig != nil) || (_chatPhotoBig != nil && ![_chatPhotoBig isEqualToString:other.chatPhotoBig])) + return false; + + if (!TGObjectCompare(other.chatPhotoFileReferenceSmall, _chatPhotoFileReferenceSmall) || !TGObjectCompare(other.chatPhotoFileReferenceBig, _chatPhotoFileReferenceBig)) + return false; + } + + if (_flags != other->_flags) { + return false; + } + + if (!TGStringCompare(_restrictionReason, other->_restrictionReason)) { + return false; + } + + if (_maxReadDate != other->_maxReadDate) { + return false; + } + + if (_maxReadMessageId != other->_maxReadMessageId) { + return false; + } + + if (_maxOutgoingReadDate != other->_maxOutgoingReadDate) { + return false; + } + + if (_maxOutgoingReadMessageId != other->_maxOutgoingReadMessageId) { + return false; + } + + if (_maxKnownMessageId != other->_maxKnownMessageId) { + return false; + } + + if (!TGObjectCompare(_channelAdminRights, other->_channelAdminRights)) { + return false; + } + + if (!TGObjectCompare(_channelBannedRights, other->_channelBannedRights)) { + return false; + } + + return true; +} + +- (NSData *)serializeChatPhoto +{ + NSMutableData *data = [[NSMutableData alloc] init]; + + int32_t magic = 0x7acde441; + [data appendBytes:&magic length:4]; + int32_t version = 9; + [data appendBytes:&version length:4]; + + for (int i = 0; i < 3; i++) + { + NSString *value = nil; + if (i == 0) + value = _chatPhotoSmall; + else if (i == 1) + value = _chatPhotoMedium; + else if (i == 2) + value = _chatPhotoBig; + + NSData *valueData = [value dataUsingEncoding:NSUTF8StringEncoding]; + int length = (int)valueData.length; + [data appendBytes:&length length:4]; + if (valueData != nil) + [data appendData:valueData]; + } + + int8_t hasEncryptedData = _encryptedData != nil ? 1 : 0; + [data appendBytes:&hasEncryptedData length:1]; + if (_encryptedData != nil) + [_encryptedData serialize:data]; + + [data appendBytes:&_flags length:4]; + + [data appendBytes:&_migratedToChannelId length:4]; + [data appendBytes:&_migratedToChannelAccessHash length:8]; + + [data appendBytes:&_maxReadMessageId length:4]; + [data appendBytes:&_maxOutgoingReadMessageId length:4]; + [data appendBytes:&_maxKnownMessageId length:4]; + [data appendBytes:&_maxLocallyReadMessageId length:4]; + + [data appendBytes:&_maxReadDate length:4]; + [data appendBytes:&_maxOutgoingReadDate length:4]; + + [data appendBytes:&_messageDate length:4]; + [data appendBytes:&_minMessageDate length:4]; + + [data appendBytes:&_messageFlags length:8]; + + { + int length = (int)_chatPhotoFileReferenceSmall.length; + [data appendBytes:&length length:4]; + if (_chatPhotoFileReferenceSmall != nil) + [data appendData:_chatPhotoFileReferenceSmall]; + } + + { + int length = (int)_chatPhotoFileReferenceBig.length; + [data appendBytes:&length length:4]; + if (_chatPhotoFileReferenceBig != nil) + [data appendData:_chatPhotoFileReferenceBig]; + } + + return data; +} + +- (void)deserializeChatPhoto:(NSData *)data +{ + int ptr = 0; + + int32_t version = 1; + if (data.length >= 4) { + int32_t magic = 0; + [data getBytes:&magic length:4]; + if (magic == 0x7acde441) { + ptr += 4; + + [data getBytes:&version range:NSMakeRange(ptr, 4)]; + ptr += 4; + } + } + + for (int i = 0; i < 3; i++) + { + int length = 0; + [data getBytes:&length range:NSMakeRange(ptr, 4)]; + ptr += 4; + + uint8_t *valueBytes = malloc(length); + [data getBytes:valueBytes range:NSMakeRange(ptr, length)]; + ptr += length; + NSString *value = [[NSString alloc] initWithBytesNoCopy:valueBytes length:length encoding:NSUTF8StringEncoding freeWhenDone:true]; + + if (i == 0) + _chatPhotoSmall = value; + else if (i == 1) + _chatPhotoMedium = value; + else if (i == 2) + _chatPhotoBig = value; + } + + if (version == 1) { + if (ptr + 4 <= (int)data.length) { + _encryptedData = [TGEncryptedConversationData deserialize:data ptr:&ptr]; + } + } else { + if (version >= 2) { + int8_t hasEncryptedData = 0; + [data getBytes:&hasEncryptedData range:NSMakeRange(ptr, 1)]; + ptr += 1; + + if (hasEncryptedData) { + _encryptedData = [TGEncryptedConversationData deserialize:data ptr:&ptr]; + } + + [data getBytes:&_flags range:NSMakeRange(ptr, 4)]; + ptr += 4; + + if (version >= 4) { + [data getBytes:&_migratedToChannelId range:NSMakeRange(ptr, 4)]; + ptr += 4; + [data getBytes:&_migratedToChannelAccessHash range:NSMakeRange(ptr, 8)]; + ptr += 8; + + if (version >= 5) { + [data getBytes:&_maxReadMessageId range:NSMakeRange(ptr, 4)]; + ptr += 4; + + [data getBytes:&_maxOutgoingReadMessageId range:NSMakeRange(ptr, 4)]; + ptr += 4; + + [data getBytes:&_maxKnownMessageId range:NSMakeRange(ptr, 4)]; + ptr += 4; + + [data getBytes:&_maxLocallyReadMessageId range:NSMakeRange(ptr, 4)]; + ptr += 4; + + [data getBytes:&_maxReadDate range:NSMakeRange(ptr, 4)]; + ptr += 4; + + [data getBytes:&_maxOutgoingReadDate range:NSMakeRange(ptr, 4)]; + ptr += 4; + + if (version >= 6) { + [data getBytes:&_messageDate range:NSMakeRange(ptr, 4)]; + ptr += 4; + + if (version >= 7) { + [data getBytes:&_minMessageDate range:NSMakeRange(ptr, 4)]; + ptr += 4; + } + } + + if (version >= 8) { + [data getBytes:&_messageFlags range:NSMakeRange(ptr, 8)]; + ptr += 8; + } + + if (version >= 9) { + int length = 0; + [data getBytes:&length range:NSMakeRange(ptr, 4)]; + ptr += 4; + + uint8_t *valueBytes = malloc(length); + [data getBytes:valueBytes range:NSMakeRange(ptr, length)]; + ptr += length; + + _chatPhotoFileReferenceSmall = [NSData dataWithBytesNoCopy:valueBytes length:length]; + + [data getBytes:&length range:NSMakeRange(ptr, 4)]; + ptr += 4; + + valueBytes = malloc(length); + [data getBytes:valueBytes range:NSMakeRange(ptr, length)]; + ptr += length; + + _chatPhotoFileReferenceBig = [NSData dataWithBytesNoCopy:valueBytes length:length]; + } + } + } + } + } +} + +- (bool)isEncrypted +{ + return _encryptedData != nil; +} + +- (void)mergeConversation:(TGConversation *)conversation { + self.accessHash = conversation.accessHash; + self.about = conversation.about; + self.username = conversation.username; + self.chatTitle = conversation.chatTitle; + self.chatPhotoSmall = conversation.chatPhotoSmall; + self.chatPhotoMedium = conversation.chatPhotoMedium; + self.chatPhotoBig = conversation.chatPhotoBig; + self.chatPhotoFileReferenceSmall = conversation.chatPhotoFileReferenceSmall; + self.chatPhotoFileReferenceBig = conversation.chatPhotoFileReferenceBig; + self.chatParticipantCount = conversation.chatParticipantCount; + self.leftChat = conversation.leftChat; + self.kickedFromChat = conversation.kickedFromChat; + self.chatVersion = conversation.chatVersion; + self.chatIsAdmin = conversation.chatIsAdmin; + self.hasAdmins = conversation.hasAdmins; + self.isAdmin = conversation.isAdmin; + self.isVerified = conversation.isVerified; + if (conversation.chatParticipants != nil) { + self.chatParticipants = conversation.chatParticipants; + } + self.isChat = conversation.isChat; + self.isDeactivated = conversation.isDeactivated; + self.isMigrated = conversation.isMigrated; + self.migratedToChannelId = conversation.migratedToChannelId; + self.migratedToChannelAccessHash = conversation.migratedToChannelAccessHash; + self.canNotSetUsername = conversation.canNotSetUsername; + if (conversation.encryptedData != nil) { + self.encryptedData = conversation.encryptedData; + } + self.channelAdminRights = conversation.channelAdminRights; + self.channelBannedRights = conversation.channelBannedRights; + _feedId = conversation->_feedId; +} + +- (void)mergeChannel:(TGConversation *)channel { + _chatTitle = channel.chatTitle; + _chatVersion = channel.chatVersion; + _chatPhotoBig = channel.chatPhotoBig; + _chatPhotoMedium = channel.chatPhotoMedium; + _chatPhotoSmall = channel.chatPhotoSmall; + _chatPhotoFileReferenceSmall = channel.chatPhotoFileReferenceSmall; + _chatPhotoFileReferenceBig = channel.chatPhotoFileReferenceBig; + _username = channel.username; + if (!channel.isMin) { + _chatIsAdmin = channel.chatIsAdmin; + self.channelRole = channel.channelRole; + _leftChat = channel.leftChat; + _kickedFromChat = channel.kickedFromChat; + self.kind = channel.leftChat || channel.kickedFromChat ? TGConversationKindTemporaryChannel : TGConversationKindPersistentChannel; + _accessHash = channel.accessHash; + self.hasExplicitContent = channel.hasExplicitContent; + self.signaturesEnabled = channel.signaturesEnabled; + self.restrictionReason = channel.restrictionReason; + self.channelAdminRights = channel.channelAdminRights; + self.channelBannedRights = channel.channelBannedRights; + + if (channel->_feedId != nil) + _feedId = channel->_feedId; + } + self.everybodyCanAddMembers = channel.everybodyCanAddMembers; + _channelIsReadOnly = channel.channelIsReadOnly; + self.isVerified = channel.isVerified; +} + +- (void)mergeDraft:(TGDatabaseMessageDraft *)draft { + _draft = draft; + + if (_draft.date > TGConversationSortKeyTimestamp(_variantSortKey)) { + _variantSortKey = TGConversationSortKeyMake(TGConversationSortKeyKind(_variantSortKey), _draft.date, TGConversationSortKeyMid(_variantSortKey)); + } +} + +- (void)setPinnedDate:(int32_t)pinnedDate { + _pinnedDate = pinnedDate; + if (pinnedDate > TGConversationSortKeyTimestamp(_variantSortKey)) { + _variantSortKey = TGConversationSortKeyMake(TGConversationSortKeyKind(_variantSortKey), pinnedDate, TGConversationSortKeyMid(_variantSortKey)); + } +} + +- (bool)currentUserCanSendMessages { + if (self.isChannelGroup) { + return true; + } + + return (_channelRole == TGChannelRoleCreator || _channelAdminRights.canPostMessages || !_channelIsReadOnly) && !_leftChat && !_kickedFromChat; +} + ++ (NSString *)chatTitleForDecoder:(PSKeyValueCoder *)coder { + return [coder decodeStringForCKey:"ct"]; +} + +- (bool)postAsChannel { + return _flags & TGConversationFlagPostAsChannel; +} + +- (void)setPostAsChannel:(bool)postAsChannel { + if (postAsChannel) { + _flags |= TGConversationFlagPostAsChannel; + } else { + _flags &= ~TGConversationFlagPostAsChannel; + } +} + +- (bool)isVerified { + return _flags & TGConversationFlagVerified; +} + +- (void)setIsVerified:(bool)isVerified { + if (isVerified) { + _flags |= TGConversationFlagVerified; + } else { + _flags &= ~TGConversationFlagVerified; + } +} + +- (bool)hasExplicitContent { + return _flags & TGConversationFlagHasExplicitContent; +} + +- (void)setHasExplicitContent:(bool)hasExplicitContent { + if (hasExplicitContent) { + _flags |= TGConversationFlagHasExplicitContent; + } else { + _flags &= ~TGConversationFlagHasExplicitContent; + } +} + +- (bool)hasAdmins { + return _flags & TGConversationFlagHasAdmins; +} + +- (void)setHasAdmins:(bool)hasAdmins { + if (hasAdmins) { + _flags |= TGConversationFlagHasAdmins; + } else { + _flags &= ~TGConversationFlagHasAdmins; + } +} + +- (bool)isAdmin { + return _flags & TGConversationFlagIsAdmin; +} + +- (void)setIsAdmin:(bool)isAdmin { + if (isAdmin) { + _flags |= TGConversationFlagIsAdmin; + } else { + _flags &= ~TGConversationFlagIsAdmin; + } +} + +- (bool)isCreator { + return _flags & TGConversationFlagIsCreator; +} + +- (void)setIsCreator:(bool)isCreator { + if (isCreator) { + _flags |= TGConversationFlagIsCreator; + } else { + _flags &= ~TGConversationFlagIsCreator; + } +} + +- (bool)isChannelGroup { + return _flags & TGConversationFlagIsChannelGroup; +} + +- (void)setIsChannelGroup:(bool)isChannelGroup { + if (isChannelGroup) { + _flags |= TGConversationFlagIsChannelGroup; + } else { + _flags &= ~TGConversationFlagIsChannelGroup; + } +} + +- (bool)everybodyCanAddMembers { + return _flags & TGConversationFlagEverybodyCanAddMembers; +} + +- (void)setEverybodyCanAddMembers:(bool)everybodyCanAddMembers { + if (everybodyCanAddMembers) { + _flags |= TGConversationFlagEverybodyCanAddMembers; + } else { + _flags &= ~TGConversationFlagEverybodyCanAddMembers; + } +} + +- (bool)isMin { + return _flags & TGConversationFlagIsMin; +} + +- (void)setIsMin:(bool)isMin { + if (isMin) { + _flags |= TGConversationFlagIsMin; + } else { + _flags &= ~TGConversationFlagIsMin; + } +} + +- (bool)canNotSetUsername { + return _flags & TGConversationFlagCanNotSetUsername; +} + +- (void)setCanNotSetUsername:(bool)canNotSetUsername { + if (canNotSetUsername) { + _flags |= TGConversationFlagCanNotSetUsername; + } else { + _flags &= ~TGConversationFlagCanNotSetUsername; + } +} + +- (bool)signaturesEnabled { + return _flags & TGConversationFlagSignaturesEnabled; +} + +- (void)setSignaturesEnabled:(bool)signaturesEnabled { + if (signaturesEnabled) { + _flags |= TGConversationFlagSignaturesEnabled; + } else { + _flags &= ~TGConversationFlagSignaturesEnabled; + } +} + +- (bool)isDeactivated { + return _flags & TGConversationFlagIsDeactivated; +} + +- (void)setIsDeactivated:(bool)isDeactivated { + if (isDeactivated) { + _flags |= TGConversationFlagIsDeactivated; + } else { + _flags &= ~TGConversationFlagIsDeactivated; + } +} + +- (bool)pinnedMessageHidden { + return _flags & TGConversationFlagPinnedMessageHidden; +} + +- (void)setPinnedMessageHidden:(bool)pinnedMessageHidden { + if (pinnedMessageHidden) { + _flags |= TGConversationFlagPinnedMessageHidden; + } else { + _flags &= ~TGConversationFlagPinnedMessageHidden; + } +} + +- (bool)isMessageUnread:(TGMessage *)message { + return [self isMessageUnread:message.mid date:(int32_t)message.date outgoing:message.outgoing]; +} + +- (bool)isMessageUnread:(int32_t)messageId date:(int32_t)messageDate outgoing:(bool)outgoing { + if (TGPeerIdIsSecretChat(_conversationId)) { + if (outgoing) { + return ((int32_t)messageDate) > _maxOutgoingReadDate; + } else { + return ((int32_t)messageDate) > _maxReadDate; + } + } else { + if (TGPeerIdIsChannel(_conversationId) && _kind != TGConversationKindPersistentChannel) { + return false; + } + + if (outgoing) { + if (messageId < TGMessageLocalMidBaseline) { + return messageId > _maxOutgoingReadMessageId; + } else { + return true; + } + } else { + if (messageId < TGMessageLocalMidBaseline) { + return messageId > _maxReadMessageId; + } else { + return false; + } + } + } +} + +- (int32_t)date { + return MAX(_pinnedDate, MAX(_minMessageDate, MAX(_draft.date, _messageDate))); +} + +- (int32_t)unpinnedDate { + return MAX(_minMessageDate, MAX(_draft.date, _messageDate)); +} + +- (bool)pinnedToTop { + return _pinnedDate >= TGConversationPinnedDateBase; +} + +- (int64_t)conversationFeedId { + if (_feedId.intValue == 0) + return 0; + return TGPeerIdFromAdminLogId(_feedId.intValue); +} + +- (int32_t)searchMessageId { + return [self.additionalProperties[@"searchMessageId"] intValue]; +} + +- (bool)isAd { + return TGPeerIdIsAd(_conversationId); +} + +- (NSString *)chatPhotoFullSmall +{ + NSString *finalAvatarUrl = self.chatPhotoSmall; + if (finalAvatarUrl.length == 0) + return finalAvatarUrl; + + int64_t volumeId = 0; + int32_t localId = 0; + if (extractFileUrlComponents(self.chatPhotoSmall, NULL, &volumeId, &localId, NULL)) + { + NSString *key = [NSString stringWithFormat:@"%lld_%d", volumeId, localId]; + NSDictionary *fileReferences = nil; + if (self.chatPhotoFileReferenceSmall != nil) { + fileReferences = @{ key: self.chatPhotoFileReferenceSmall }; + } + TGMediaOriginInfo *originInfo = [TGMediaOriginInfo mediaOriginInfoWithFileReference:self.chatPhotoFileReferenceSmall fileReferences:fileReferences peerId:_conversationId]; + finalAvatarUrl = [finalAvatarUrl stringByAppendingFormat:@"_o%@", [originInfo stringRepresentation]]; + } + + return finalAvatarUrl; +} + +- (NSString *)chatPhotoFullBig +{ + NSString *finalAvatarUrl = self.chatPhotoBig; + if (finalAvatarUrl.length == 0) + return finalAvatarUrl; + + int64_t volumeId = 0; + int32_t localId = 0; + if (extractFileUrlComponents(self.chatPhotoBig, NULL, &volumeId, &localId, NULL)) + { + NSString *key = [NSString stringWithFormat:@"%lld_%d", volumeId, localId]; + NSDictionary *fileReferences = nil; + if (self.chatPhotoFileReferenceBig != nil) { + fileReferences = @{ key: self.chatPhotoFileReferenceBig }; + } + TGMediaOriginInfo *originInfo = [TGMediaOriginInfo mediaOriginInfoWithFileReference:self.chatPhotoFileReferenceBig fileReferences:fileReferences peerId:_conversationId]; + finalAvatarUrl = [finalAvatarUrl stringByAppendingFormat:@"_o%@", [originInfo stringRepresentation]]; + } + + return finalAvatarUrl; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGCustomImageWallpaperInfo.h b/submodules/LegacyComponents/LegacyComponents/TGCustomImageWallpaperInfo.h new file mode 100644 index 0000000000..5c73d2d755 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGCustomImageWallpaperInfo.h @@ -0,0 +1,7 @@ +#import + +@interface TGCustomImageWallpaperInfo : TGWallpaperInfo + +- (instancetype)initWithImage:(UIImage *)image; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGCustomImageWallpaperInfo.m b/submodules/LegacyComponents/LegacyComponents/TGCustomImageWallpaperInfo.m new file mode 100644 index 0000000000..e52cffc3f3 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGCustomImageWallpaperInfo.m @@ -0,0 +1,77 @@ + + +#import "TGCustomImageWallpaperInfo.h" + +@interface TGCustomImageWallpaperInfo () +{ + NSData *_imageData; +} + +@end + +@implementation TGCustomImageWallpaperInfo + +- (instancetype)initWithImage:(UIImage *)image +{ + self = [super init]; + if (self != nil) + { + if (image != nil) + _imageData = UIImageJPEGRepresentation(image, 0.98f); + } + return self; +} + +- (NSString *)thumbnailUrl +{ + return nil; +} + +- (NSString *)fullscreenUrl +{ + return nil; +} + +- (int)tintColor +{ + return 0x000000; +} + +- (UIImage *)image +{ + return [[UIImage alloc] initWithData:_imageData]; +} + +- (NSData *)imageData +{ + return _imageData; +} + +- (bool)hasData +{ + return true; +} + +- (BOOL)isEqual:(id)object +{ + if ([object isKindOfClass:[TGCustomImageWallpaperInfo class]]) + { + return self == object; + } + + return false; +} + +- (NSDictionary *)infoDictionary +{ + return @{ + @"_className": NSStringFromClass([self class]) + }; +} + ++ (TGWallpaperInfo *)infoWithDictionary:(NSDictionary *)__unused dict +{ + return [[TGCustomImageWallpaperInfo alloc] initWithImage:nil]; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGDataResource.h b/submodules/LegacyComponents/LegacyComponents/TGDataResource.h new file mode 100644 index 0000000000..2cc2874288 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGDataResource.h @@ -0,0 +1,15 @@ +#import +#import + +@interface TGDataResource : NSObject + +- (NSData *)data; +- (NSInputStream *)stream; +- (UIImage *)image; +- (bool)isImageDecoded; + +- (instancetype)initWithData:(NSData *)data; +- (instancetype)initWithInputStream:(NSInputStream *)stream; +- (instancetype)initWithImage:(UIImage *)image decoded:(bool)decoded; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGDataResource.m b/submodules/LegacyComponents/LegacyComponents/TGDataResource.m new file mode 100644 index 0000000000..914234a4e3 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGDataResource.m @@ -0,0 +1,71 @@ +#import "TGDataResource.h" + +@interface TGDataResource () +{ + NSData *_data; + NSInputStream *_stream; + UIImage *_image; + bool _imageDecoded; +} + +@end + +@implementation TGDataResource + +- (instancetype)initWithData:(NSData *)data +{ + self = [super init]; + if (self != nil) + { + _data = data; + } + return self; +} + +- (instancetype)initWithInputStream:(NSInputStream *)stream +{ + self = [super init]; + if (self != nil) + { + _stream = stream; + } + return self; +} + +- (instancetype)initWithImage:(UIImage *)image decoded:(bool)decoded +{ + self = [super init]; + if (self != nil) + { + _image = image; + _imageDecoded = decoded; + } + return self; +} + +- (void)dealloc +{ + [_stream close]; +} + +- (NSData *)data +{ + return _data; +} + +- (NSInputStream *)stream +{ + return _stream; +} + +- (UIImage *)image +{ + return _image; +} + +- (bool)isImageDecoded +{ + return _imageDecoded; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGDatabaseMessageDraft.h b/submodules/LegacyComponents/LegacyComponents/TGDatabaseMessageDraft.h new file mode 100644 index 0000000000..25d96d709a --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGDatabaseMessageDraft.h @@ -0,0 +1,20 @@ +#import + +#import +#import + +@interface TGDatabaseMessageDraft : NSObject + +@property (nonatomic, strong, readonly) NSString *text; +@property (nonatomic, strong, readonly) NSArray *entities; +@property (nonatomic, readonly) bool disableLinkPreview; +@property (nonatomic, readonly) int32_t replyToMessageId; +@property (nonatomic, readonly) int32_t date; + +- (instancetype)initWithText:(NSString *)text entities:(NSArray *)entities disableLinkPreview:(bool)disableLinkPreview replyToMessageId:(int32_t)replyToMessageId date:(int32_t)date; + +- (bool)isEmpty; + +- (TGDatabaseMessageDraft *)updateDate:(int32_t)date; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGDatabaseMessageDraft.m b/submodules/LegacyComponents/LegacyComponents/TGDatabaseMessageDraft.m new file mode 100644 index 0000000000..2ad2d56a31 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGDatabaseMessageDraft.m @@ -0,0 +1,46 @@ +#import "TGDatabaseMessageDraft.h" + +#import "LegacyComponentsInternal.h" + +#import "PSKeyValueEncoder.h" +#import "PSKeyValueDecoder.h" + +@implementation TGDatabaseMessageDraft + +- (instancetype)initWithText:(NSString *)text entities:(NSArray *)entities disableLinkPreview:(bool)disableLinkPreview replyToMessageId:(int32_t)replyToMessageId date:(int32_t)date { + self = [super init]; + if (self != nil) { + _text = text; + _entities = entities; + _disableLinkPreview = disableLinkPreview; + _replyToMessageId = replyToMessageId; + _date = date; + } + return self; +} + +- (instancetype)initWithKeyValueCoder:(PSKeyValueCoder *)coder { + return [self initWithText:[coder decodeStringForCKey:"text"] entities:[coder decodeArrayForCKey:"entities"] disableLinkPreview:[coder decodeInt32ForCKey:"disableLinkPreview"] replyToMessageId:[coder decodeInt32ForCKey:"replyToMessageId"] date:[coder decodeInt32ForCKey:"date"]]; +} + +- (void)encodeWithKeyValueCoder:(PSKeyValueCoder *)coder { + [coder encodeString:_text forCKey:"text"]; + [coder encodeArray:_entities forCKey:"entities"]; + [coder encodeInt32:_disableLinkPreview ? 1 : 0 forCKey:"disableLinkPreview"]; + [coder encodeInt32:_replyToMessageId forCKey:"replyToMessageId"]; + [coder encodeInt32:_date forCKey:"date"]; +} + +- (bool)isEqual:(id)object { + return [object isKindOfClass:[TGDatabaseMessageDraft class]] && TGStringCompare(((TGDatabaseMessageDraft *)object)->_text, _text) && TGObjectCompare(((TGDatabaseMessageDraft *)object)->_entities, _entities) && ((TGDatabaseMessageDraft *)object)->_disableLinkPreview == _disableLinkPreview && ((TGDatabaseMessageDraft *)object)->_replyToMessageId == _replyToMessageId && ((TGDatabaseMessageDraft *)object)->_date == _date; +} + +- (bool)isEmpty { + return _text.length == 0 && _replyToMessageId == 0; +} + +- (TGDatabaseMessageDraft *)updateDate:(int32_t)date { + return [[TGDatabaseMessageDraft alloc] initWithText:_text entities:_entities disableLinkPreview:_disableLinkPreview replyToMessageId:_replyToMessageId date:date]; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGDateUtils.h b/submodules/LegacyComponents/LegacyComponents/TGDateUtils.h new file mode 100644 index 0000000000..a4833b6a16 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGDateUtils.h @@ -0,0 +1,36 @@ +#import + +@interface TGDateUtils : NSObject + ++ (void)reset; + ++ (NSString *)stringForShortTime:(int)time; ++ (NSString *)stringForShortTime:(int)time daytimeVariant:(int *)daytimeVariant; ++ (NSString *)stringForShortTimeWithHours:(int)hours minutes:(int)minutes; ++ (NSString *)stringForDialogTime:(int)time; ++ (NSString *)stringForDayOfWeek:(int)date; ++ (NSString *)stringForMonthOfYear:(int)date; ++ (NSString *)stringForPreciseDate:(int)date; ++ (NSString *)stringForMessageListDate:(int)date; ++ (NSString *)stringForLastSeen:(int)date; ++ (NSString *)stringForApproximateDate:(int)date; ++ (NSString *)stringForRelativeLastSeen:(int)date; ++ (NSString *)stringForRelativeUpdate:(int)date; ++ (NSString *)stringForFullDate:(int)date; ++ (NSString *)stringForCallsListDate:(int)date; + +@end + +#ifdef __cplusplus +extern "C" { +#endif + +bool TGUse12hDateFormat(); + +NSString *TGWeekdayNameFull(int number); +NSString *TGMonthNameFull(int number); +NSString *TGMonthNameShort(int number); + +#ifdef __cplusplus +} +#endif diff --git a/submodules/LegacyComponents/LegacyComponents/TGDateUtils.mm b/submodules/LegacyComponents/LegacyComponents/TGDateUtils.mm new file mode 100644 index 0000000000..8f0478e948 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGDateUtils.mm @@ -0,0 +1,556 @@ +#include "TGDateUtils.h" +#include + +#import "LegacyComponentsInternal.h" +#import "TGStringUtils.h" +#import "TGUser.h" +#import "TGLocalization.h" + +static bool value_dateHas12hFormat = false; +static __strong NSString *value_monthNamesGenShort[] = { + nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil +}; +static __strong NSString *value_monthNamesGenFull[] = { + nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil +}; +static __strong NSString *value_weekdayNamesShort[] = { + nil, nil, nil, nil, nil, nil, nil +}; +static __strong NSString *value_weekdayNamesFull[] = { + nil, nil, nil, nil, nil, nil, nil +}; + +static NSString *value_dialogTimeFormat = nil; + +static NSString *value_date_separator = @"."; +static bool value_monthFirst = false; + +static bool isArabic = false; +static bool isKorean = false; + +static bool TGDateUtilsInitialized = false; +static void initializeTGDateUtils() +{ + NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; + [dateFormatter setLocale:[NSLocale currentLocale]]; + [dateFormatter setDateStyle:NSDateFormatterNoStyle]; + [dateFormatter setTimeStyle:NSDateFormatterMediumStyle]; + NSTimeZone *timeZone = [NSTimeZone localTimeZone]; + [dateFormatter setTimeZone:timeZone]; + NSString *dateString = [dateFormatter stringFromDate:[NSDate date]]; + NSRange amRange = [dateString rangeOfString:[dateFormatter AMSymbol]]; + NSRange pmRange = [dateString rangeOfString:[dateFormatter PMSymbol]]; + value_dateHas12hFormat = !(amRange.location == NSNotFound && pmRange.location == NSNotFound); + + if ([dateFormatter.locale respondsToSelector:@selector(countryCode)] && [dateFormatter.locale.countryCode isEqualToString:@"UA"]) + value_dateHas12hFormat = false; + + dateString = [NSDateFormatter dateFormatFromTemplate:@"MdY" options:0 locale:[NSLocale currentLocale]]; + if ([dateString rangeOfString:@"."].location != NSNotFound) + { + value_date_separator = @"."; + } + else if ([dateString rangeOfString:@"/"].location != NSNotFound) + { + value_date_separator = @"/"; + } + else if ([dateString rangeOfString:@"-"].location != NSNotFound) + { + value_date_separator = @"-"; + } + + if ([dateString rangeOfString:[NSString stringWithFormat:@"M%@d", value_date_separator]].location != NSNotFound) + { + value_monthFirst = true; + } + + NSString *identifier = [[NSLocale currentLocale] localeIdentifier]; + if ([identifier isEqualToString:@"ar"] || [identifier hasPrefix:@"ar_"]) + { + isArabic = true; + value_date_separator = @"\u060d"; + } + else if ([identifier isEqualToString:@"ko"] || [identifier hasPrefix:@"ko-"]) + { + isKorean = true; + } + + value_monthNamesGenShort[0] = TGLocalized(@"Month.ShortJanuary"); + value_monthNamesGenShort[1] = TGLocalized(@"Month.ShortFebruary"); + value_monthNamesGenShort[2] = TGLocalized(@"Month.ShortMarch"); + value_monthNamesGenShort[3] = TGLocalized(@"Month.ShortApril"); + value_monthNamesGenShort[4] = TGLocalized(@"Month.ShortMay"); + value_monthNamesGenShort[5] = TGLocalized(@"Month.ShortJune"); + value_monthNamesGenShort[6] = TGLocalized(@"Month.ShortJuly"); + value_monthNamesGenShort[7] = TGLocalized(@"Month.ShortAugust"); + value_monthNamesGenShort[8] = TGLocalized(@"Month.ShortSeptember"); + value_monthNamesGenShort[9] = TGLocalized(@"Month.ShortOctober"); + value_monthNamesGenShort[10] = TGLocalized(@"Month.ShortNovember"); + value_monthNamesGenShort[11] = TGLocalized(@"Month.ShortDecember"); + + value_monthNamesGenFull[0] = TGLocalized(@"Month.GenJanuary"); + value_monthNamesGenFull[1] = TGLocalized(@"Month.GenFebruary"); + value_monthNamesGenFull[2] = TGLocalized(@"Month.GenMarch"); + value_monthNamesGenFull[3] = TGLocalized(@"Month.GenApril"); + value_monthNamesGenFull[4] = TGLocalized(@"Month.GenMay"); + value_monthNamesGenFull[5] = TGLocalized(@"Month.GenJune"); + value_monthNamesGenFull[6] = TGLocalized(@"Month.GenJuly"); + value_monthNamesGenFull[7] = TGLocalized(@"Month.GenAugust"); + value_monthNamesGenFull[8] = TGLocalized(@"Month.GenSeptember"); + value_monthNamesGenFull[9] = TGLocalized(@"Month.GenOctober"); + value_monthNamesGenFull[10] = TGLocalized(@"Month.GenNovember"); + value_monthNamesGenFull[11] = TGLocalized(@"Month.GenDecember"); + + value_weekdayNamesShort[0] = TGLocalized(@"Weekday.ShortMonday"); + value_weekdayNamesShort[1] = TGLocalized(@"Weekday.ShortTuesday"); + value_weekdayNamesShort[2] = TGLocalized(@"Weekday.ShortWednesday"); + value_weekdayNamesShort[3] = TGLocalized(@"Weekday.ShortThursday"); + value_weekdayNamesShort[4] = TGLocalized(@"Weekday.ShortFriday"); + value_weekdayNamesShort[5] = TGLocalized(@"Weekday.ShortSaturday"); + value_weekdayNamesShort[6] = TGLocalized(@"Weekday.ShortSunday"); + + value_weekdayNamesFull[0] = TGLocalized(@"Weekday.Monday"); + value_weekdayNamesFull[1] = TGLocalized(@"Weekday.Tuesday"); + value_weekdayNamesFull[2] = TGLocalized(@"Weekday.Wednesday"); + value_weekdayNamesFull[3] = TGLocalized(@"Weekday.Thursday"); + value_weekdayNamesFull[4] = TGLocalized(@"Weekday.Friday"); + value_weekdayNamesFull[5] = TGLocalized(@"Weekday.Saturday"); + value_weekdayNamesFull[6] = TGLocalized(@"Weekday.Sunday"); + + value_dialogTimeFormat = [[TGLocalized(@"Date.DialogDateFormat") stringByReplacingOccurrencesOfString:@"{month}" withString:@"%1$@"] stringByReplacingOccurrencesOfString:@"{day}" withString:@"%2$@"]; + + TGDateUtilsInitialized = true; +} + +static inline bool dateHas12hFormat() +{ + if (!TGDateUtilsInitialized) + initializeTGDateUtils(); + + return value_dateHas12hFormat; +} + +bool TGUse12hDateFormat() +{ + if (!TGDateUtilsInitialized) + initializeTGDateUtils(); + + return value_dateHas12hFormat; +} + +static inline NSString *weekdayNameShort(int number) +{ + if (!TGDateUtilsInitialized) + initializeTGDateUtils(); + + if (number < 0) + number = 0; + if (number > 6) + number = 6; + + if (number == 0) + number = 6; + else + number--; + + return value_weekdayNamesShort[number]; +} + +static inline NSString *weekdayNameFull(int number) +{ + if (!TGDateUtilsInitialized) + initializeTGDateUtils(); + + if (number < 0) + number = 0; + if (number > 6) + number = 6; + + if (number == 0) + number = 6; + else + number--; + + return value_weekdayNamesFull[number]; +} + +NSString *TGWeekdayNameFull(int number) +{ + return weekdayNameFull(number); +} + +static inline NSString *monthNameGenFull(int number) +{ + if (!TGDateUtilsInitialized) + initializeTGDateUtils(); + + if (number < 0) + number = 0; + if (number > 11) + number = 11; + + return value_monthNamesGenFull[number]; +} + +static inline NSString *monthNameGenShort(int number) +{ + if (!TGDateUtilsInitialized) + initializeTGDateUtils(); + + if (number < 0) + number = 0; + if (number > 11) + number = 11; + + return value_monthNamesGenShort[number]; +} + +NSString *TGMonthNameFull(int number) +{ + return monthNameGenFull(number); +} + +NSString *TGMonthNameShort(int number) +{ + return monthNameGenShort(number); +} + +static inline NSString *dialogTimeFormat() +{ + if (!TGDateUtilsInitialized) + initializeTGDateUtils(); + + return value_dialogTimeFormat; +} + +@implementation TGDateUtils + ++ (void)reset +{ + TGDateUtilsInitialized = false; +} + ++ (NSString *)stringForShortTime:(int)time +{ + time_t t = time; + struct tm timeinfo; + localtime_r(&t, &timeinfo); + + return [self stringForShortTimeWithHours:timeinfo.tm_hour minutes:timeinfo.tm_min]; +} + ++ (NSString *)stringForShortTimeWithHours:(int)hours minutes:(int)minutes +{ + if (!TGDateUtilsInitialized) + initializeTGDateUtils(); + + if (isArabic) + { + if (dateHas12hFormat()) + { + if (hours < 12) + return [TGStringUtils stringWithLocalizedNumberCharacters:[[NSString alloc] initWithFormat:@"%d:%02d ص", hours == 0 ? 12 : hours, minutes]]; + else + return [TGStringUtils stringWithLocalizedNumberCharacters:[[NSString alloc] initWithFormat:@"%d:%02d م", (hours - 12 == 0) ? 12 : (hours - 12), minutes]]; + } + else + return [TGStringUtils stringWithLocalizedNumberCharacters:[[NSString alloc] initWithFormat:@"%02d:%02d", hours, minutes]]; + } + else if (isKorean) + { + return [[NSString alloc] initWithFormat:@"%02d:%02d", hours, minutes]; + } + else + { + if (dateHas12hFormat()) + { + if (hours < 12) + return [[NSString alloc] initWithFormat:@"%d:%02d AM", hours == 0 ? 12 : hours, minutes]; + else + return [[NSString alloc] initWithFormat:@"%d:%02d PM", (hours - 12 == 0) ? 12 : (hours - 12), minutes]; + } + else + return [[NSString alloc] initWithFormat:@"%02d:%02d", hours, minutes]; + } +} + ++ (NSString *)stringForShortTime:(int)time daytimeVariant:(int *)__unused daytimeVariant +{ + return [self stringForShortTime:time]; +} + ++ (NSString *)stringForDialogTime:(int)time +{ + time_t t = time; + struct tm timeinfo; + gmtime_r(&t, &timeinfo); + + return [[NSString alloc] initWithFormat:dialogTimeFormat(), monthNameGenFull(timeinfo.tm_mon), [TGStringUtils stringWithLocalizedNumber:timeinfo.tm_mday]]; +} + ++ (NSString *)stringForDayOfWeek:(int)date +{ + time_t t = date; + struct tm timeinfo; + localtime_r(&t, &timeinfo); + + return weekdayNameFull(timeinfo.tm_wday); +} + ++ (NSString *)stringForMonthOfYear:(int)date +{ + time_t t = date; + struct tm timeinfo; + localtime_r(&t, &timeinfo); + + NSString *format = TGLocalized([[NSString alloc] initWithFormat:@"Time.MonthOfYear_m%d", (int)timeinfo.tm_mon + 1]); + + return [[NSString alloc] initWithFormat:format, [[NSString alloc] initWithFormat:@"%d", 2000 + timeinfo.tm_year - 100]]; +} + ++ (NSString *)stringForFullDateWithDay:(int)day month:(int)month year:(int)year +{ + if (isArabic) + { + return [TGStringUtils stringWithLocalizedNumberCharacters:[[NSString alloc] initWithFormat:@"%d%@%d%@%02d", day, value_date_separator, month, value_date_separator, year - 100]]; + } + else if (isKorean) + { + return [TGStringUtils stringWithLocalizedNumberCharacters:[[NSString alloc] initWithFormat:@"%04d년 %d월 %d일", year - 100 + 2000, month, day]]; + } + else + { + if (value_monthFirst) + { + return [[NSString alloc] initWithFormat:@"%d%@%d%@%02d", month, value_date_separator, day, value_date_separator, year - 100]; + } + else + { + return [[NSString alloc] initWithFormat:@"%d%@%02d%@%02d", day, value_date_separator, month, value_date_separator, year - 100]; + } + } +} + ++ (NSString *)stringForPreciseDate:(int)date +{ + time_t t = date; + struct tm timeinfo; + localtime_r(&t, &timeinfo); + + NSString *format = TGLocalized([[NSString alloc] initWithFormat:@"Time.PreciseDate_m%d", (int)timeinfo.tm_mon + 1]); + return [[NSString alloc] initWithFormat:format, [[NSString alloc] initWithFormat:@"%d", timeinfo.tm_mday], [[NSString alloc] initWithFormat:@"%d", (int)(2000 + timeinfo.tm_year - 100)], [self stringForShortTimeWithHours:timeinfo.tm_hour minutes:timeinfo.tm_min]]; +} + ++ (NSString *)stringForFullDate:(int)date { + time_t t = date; + struct tm timeinfo; + localtime_r(&t, &timeinfo); + + return [self stringForFullDateWithDay:timeinfo.tm_mday month:timeinfo.tm_mon + 1 year:timeinfo.tm_year]; +} + ++ (NSString *)stringForMessageListDate:(int)date +{ + time_t t = date; + struct tm timeinfo; + localtime_r(&t, &timeinfo); + + time_t t_now; + time(&t_now); + struct tm timeinfo_now; + localtime_r(&t_now, &timeinfo_now); + + if (timeinfo.tm_year != timeinfo_now.tm_year) + { + return [self stringForFullDateWithDay:timeinfo.tm_mday month:timeinfo.tm_mon + 1 year:timeinfo.tm_year]; + } + else + { + int dayDiff = timeinfo.tm_yday - timeinfo_now.tm_yday; + + if(dayDiff == 0) + return [self stringForShortTime:date]; + else if(dayDiff == -1) + return weekdayNameShort(timeinfo.tm_wday); + else if(dayDiff == -2) + return weekdayNameShort(timeinfo.tm_wday); + else if(dayDiff > -7 && dayDiff <= -2) + return weekdayNameShort(timeinfo.tm_wday); + else + return [self stringForFullDateWithDay:timeinfo.tm_mday month:timeinfo.tm_mon + 1 year:timeinfo.tm_year]; + } + + return nil; +} + ++ (NSString *)stringForCallsListDate:(int)date +{ + time_t t = date; + struct tm timeinfo; + localtime_r(&t, &timeinfo); + + time_t t_now; + time(&t_now); + struct tm timeinfo_now; + localtime_r(&t_now, &timeinfo_now); + + if (timeinfo.tm_year != timeinfo_now.tm_year) + { + return [self stringForFullDateWithDay:timeinfo.tm_mday month:timeinfo.tm_mon + 1 year:timeinfo.tm_year]; + } + else + { + int dayDiff = timeinfo.tm_yday - timeinfo_now.tm_yday; + + if(dayDiff == 0) + return TGLocalized(@"Weekday.Today"); + else if(dayDiff == -1) + return TGLocalized(@"Weekday.Yesterday"); + else if(dayDiff > -7 && dayDiff <= -2) + return weekdayNameShort(timeinfo.tm_wday); + else + return [self stringForDialogTime:date]; + } + + return nil; +} + ++ (NSString *)stringForApproximateDate:(int)date +{ + time_t t = date; + struct tm timeinfo; + localtime_r(&t, &timeinfo); + + time_t t_now; + time(&t_now); + struct tm timeinfo_now; + localtime_r(&t_now, &timeinfo_now); + + if (timeinfo.tm_year != timeinfo_now.tm_year) + return [self stringForFullDateWithDay:timeinfo.tm_mday month:timeinfo.tm_mon + 1 year:timeinfo.tm_year]; + else + { + int dayDiff = timeinfo.tm_yday - timeinfo_now.tm_yday; + + if(dayDiff == 0 || dayDiff == -1) + return [self stringForTodayOrYesterday:dayDiff == 0 hours:timeinfo.tm_hour minutes:timeinfo.tm_min]; + else + return [self stringForFullDateWithDay:timeinfo.tm_mday month:timeinfo.tm_mon + 1 year:timeinfo.tm_year]; + } + + return nil; +} + ++ (NSString *)stringForTodayOrYesterday:(bool)today hours:(int)hours minutes:(int)minutes +{ + NSString *timeString = [self stringForShortTimeWithHours:hours minutes:minutes]; + + return [[NSString alloc] initWithFormat:today ? TGLocalized(@"Time.TodayAt") : TGLocalized(@"Time.YesterdayAt"), timeString]; +} + ++ (NSString *)stringForLastSeenYesterday:(bool)today hours:(int)hours minutes:(int)minutes +{ + NSString *timeString = [self stringForShortTimeWithHours:hours minutes:minutes]; + + return [[NSString alloc] initWithFormat:TGLocalized(@"LastSeen.YesterdayAt"), timeString]; +} + ++ (NSString *)stringForRelativeLastSeen:(int)date +{ + if (date == -1) + return TGLocalized(@"Presence.invisible"); + else if (date == TGUserPresenceValueLately) + return TGLocalized(@"LastSeen.Lately"); + else if (date == TGUserPresenceValueWithinAWeek) + return TGLocalized(@"LastSeen.WithinAWeek"); + else if (date == TGUserPresenceValueWithinAMonth) + return TGLocalized(@"LastSeen.WithinAMonth"); + else if (date == TGUserPresenceValueALongTimeAgo) + return TGLocalized(@"LastSeen.ALongTimeAgo"); + else if (date <= 0) + return TGLocalized(@"Presence.offline"); + + time_t t = date; + struct tm timeinfo; + localtime_r(&t, &timeinfo); + + time_t t_now; + time(&t_now); + struct tm timeinfo_now; + localtime_r(&t_now, &timeinfo_now); + + if (timeinfo.tm_year != timeinfo_now.tm_year) + return [[NSString alloc] initWithFormat:TGLocalized(@"LastSeen.AtDate"), [self stringForFullDateWithDay:timeinfo.tm_mday month:timeinfo.tm_mon + 1 year:timeinfo.tm_year]]; + else + { + int dayDiff = timeinfo.tm_yday - timeinfo_now.tm_yday; + + int minutesDiff = (int)((t_now - date) / 60); + int hoursDiff = (int)((t_now - date) / (60 * 60)); + + if (dayDiff == 0 && hoursDiff <= 23) + { + if (minutesDiff < 1) + return TGLocalized(@"LastSeen.JustNow"); + else if (minutesDiff < 60) + { + return [legacyEffectiveLocalization() getPluralized:@"LastSeen.MinutesAgo" count:(int32_t)minutesDiff]; + } + else + { + return [legacyEffectiveLocalization() getPluralized:@"LastSeen.HoursAgo" count:(int32_t)hoursDiff]; + } + } + else if (dayDiff == -1) + return [self stringForLastSeenYesterday:dayDiff == 0 hours:timeinfo.tm_hour minutes:timeinfo.tm_min]; + else + return [[NSString alloc] initWithFormat:TGLocalized(@"LastSeen.AtDate"), [self stringForFullDateWithDay:timeinfo.tm_mday month:timeinfo.tm_mon + 1 year:timeinfo.tm_year]]; + } + + return nil; +} + ++ (NSString *)stringForRelativeUpdate:(int)date +{ + time_t t = date; + struct tm timeinfo; + localtime_r(&t, &timeinfo); + + time_t t_now; + time(&t_now); + struct tm timeinfo_now; + localtime_r(&t_now, &timeinfo_now); + + if (timeinfo.tm_year != timeinfo_now.tm_year) + return [self stringForPreciseDate:date]; + else + { + int dayDiff = timeinfo.tm_yday - timeinfo_now.tm_yday; + + int minutesDiff = (int)((t_now - date) / 60); + int hoursDiff = (int)((t_now - date) / (60 * 60)); + + if (dayDiff == 0 && hoursDiff < 1) + { + if (minutesDiff < 1) + return TGLocalized(@"LiveLocationUpdated.JustNow"); + else if (minutesDiff < 60) + { + return [legacyEffectiveLocalization() getPluralized:@"LiveLocationUpdated.MinutesAgo" count:(int32_t)minutesDiff]; + } + } + else if (dayDiff == 0) + { + NSString *timeString = [self stringForShortTimeWithHours:timeinfo.tm_hour minutes:timeinfo.tm_min]; + return [[NSString alloc] initWithFormat:TGLocalized(@"LiveLocationUpdated.TodayAt"), timeString]; + } + else + { + return [self stringForPreciseDate:date]; + } + } + + return nil; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGDefaultPasscodeBackground.h b/submodules/LegacyComponents/LegacyComponents/TGDefaultPasscodeBackground.h new file mode 100644 index 0000000000..dc45565175 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGDefaultPasscodeBackground.h @@ -0,0 +1,5 @@ +#import + +@interface TGDefaultPasscodeBackground : NSObject + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGDefaultPasscodeBackground.m b/submodules/LegacyComponents/LegacyComponents/TGDefaultPasscodeBackground.m new file mode 100644 index 0000000000..3a70a62913 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGDefaultPasscodeBackground.m @@ -0,0 +1,79 @@ +#import "TGDefaultPasscodeBackground.h" + +#import "LegacyComponentsInternal.h" + +#import + +@interface TGDefaultPasscodeBackground () +{ + CGSize _size; + UIImage *_backgroundImage; +} + +@end + +@implementation TGDefaultPasscodeBackground + +- (instancetype)initWithSize:(CGSize)size +{ + self = [super init]; + if (self != nil) + { + _size = size; + + UIGraphicsBeginImageContextWithOptions(CGSizeMake(8.0f, _size.height), true, 0.0f); + CGContextRef context = UIGraphicsGetCurrentContext(); + + CGColorRef colors[2] = { + CGColorRetain(UIColorRGB(0x466f92).CGColor), + CGColorRetain(UIColorRGB(0x244f74).CGColor) + }; + + CFArrayRef colorsArray = CFArrayCreate(kCFAllocatorDefault, (const void **)&colors, 2, NULL); + CGFloat locations[2] = {0.0f, 1.0f}; + + CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); + CGGradientRef gradient = CGGradientCreateWithColors(colorSpace, colorsArray, (CGFloat const *)&locations); + + CFRelease(colorsArray); + CFRelease(colors[0]); + CFRelease(colors[1]); + + CGColorSpaceRelease(colorSpace); + + CGContextDrawLinearGradient(context, gradient, CGPointMake(0.0f, 0.0f), CGPointMake(0.0f, _size.height), 0); + CGGradientRelease(gradient); + + _backgroundImage = [UIGraphicsGetImageFromCurrentImageContext() resizableImageWithCapInsets:UIEdgeInsetsMake(0.0f, 0.0f, 0.0f, 0.0f) resizingMode:UIImageResizingModeTile]; + UIGraphicsEndImageContext(); + } + return self; +} + +- (CGSize)size +{ + return _size; +} + +- (UIImage *)backgroundImage +{ + return _backgroundImage; +} + +- (UIImage *)foregroundImage +{ + static UIImage *image = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^ + { + UIGraphicsBeginImageContextWithOptions(CGSizeMake(1.0f, 1.0f), false, 0.0f); + CGContextRef context = UIGraphicsGetCurrentContext(); + CGContextSetFillColorWithColor(context, UIColorRGBA(0xffffff, 0.5f).CGColor); + CGContextFillRect(context, CGRectMake(0.0f, 0.0f, 1.0f, 1.0f)); + image = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + }); + return image; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGDocumentAttributeAnimated.h b/submodules/LegacyComponents/LegacyComponents/TGDocumentAttributeAnimated.h new file mode 100644 index 0000000000..c89a75a1ab --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGDocumentAttributeAnimated.h @@ -0,0 +1,5 @@ +#import + +@interface TGDocumentAttributeAnimated : NSObject + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGDocumentAttributeAnimated.m b/submodules/LegacyComponents/LegacyComponents/TGDocumentAttributeAnimated.m new file mode 100644 index 0000000000..3ec92c9639 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGDocumentAttributeAnimated.m @@ -0,0 +1,28 @@ +#import "TGDocumentAttributeAnimated.h" + +@implementation TGDocumentAttributeAnimated + +- (instancetype)initWithKeyValueCoder:(PSKeyValueCoder *)__unused coder +{ + return [self init]; +} + +- (void)encodeWithKeyValueCoder:(PSKeyValueCoder *)__unused coder +{ +} + +- (instancetype)initWithCoder:(NSCoder *)__unused aDecoder +{ + return [self init]; +} + +- (void)encodeWithCoder:(NSCoder *)__unused aCoder +{ +} + +- (BOOL)isEqual:(id)object +{ + return [object isEqual:[TGDocumentAttributeAnimated class]]; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGDocumentAttributeAudio.h b/submodules/LegacyComponents/LegacyComponents/TGDocumentAttributeAudio.h new file mode 100644 index 0000000000..eb1f94fadd --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGDocumentAttributeAudio.h @@ -0,0 +1,17 @@ +#import + +#import + +#import + +@interface TGDocumentAttributeAudio : NSObject + +@property (nonatomic, readonly) bool isVoice; +@property (nonatomic, strong, readonly) NSString *title; +@property (nonatomic, strong, readonly) NSString *performer; +@property (nonatomic, readonly) int32_t duration; +@property (nonatomic, strong, readonly) TGAudioWaveform *waveform; + +- (instancetype)initWithIsVoice:(bool)isVoice title:(NSString *)title performer:(NSString *)performer duration:(int32_t)duration waveform:(TGAudioWaveform *)waveform; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGDocumentAttributeAudio.m b/submodules/LegacyComponents/LegacyComponents/TGDocumentAttributeAudio.m new file mode 100644 index 0000000000..988899977a --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGDocumentAttributeAudio.m @@ -0,0 +1,56 @@ +#import "TGDocumentAttributeAudio.h" + +#import "LegacyComponentsInternal.h" + +#import "PSKeyValueCoder.h" + +@implementation TGDocumentAttributeAudio + +- (instancetype)initWithIsVoice:(bool)isVoice title:(NSString *)title performer:(NSString *)performer duration:(int32_t)duration waveform:(TGAudioWaveform *)waveform +{ + self = [super init]; + if (self != nil) + { + _isVoice = isVoice; + _title = title; + _performer = performer; + _duration = duration; + _waveform = waveform; + } + return self; +} + +- (instancetype)initWithCoder:(NSCoder *)aDecoder +{ + return [self initWithIsVoice:[aDecoder decodeBoolForKey:@"isVoice"] title:[aDecoder decodeObjectForKey:@"title"] performer:[aDecoder decodeObjectForKey:@"performer"] duration:[aDecoder decodeInt32ForKey:@"duration"] waveform:[aDecoder decodeObjectForKey:@"waveform"]]; +} + +- (instancetype)initWithKeyValueCoder:(PSKeyValueCoder *)coder +{ + return [self initWithIsVoice:[coder decodeInt32ForCKey:"isVoice"] title:[coder decodeStringForCKey:"title"] performer:[coder decodeStringForCKey:"performer"] duration:[coder decodeInt32ForCKey:"duration"] waveform:(TGAudioWaveform *)[coder decodeObjectForCKey:"waveform"]]; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder +{ + [aCoder encodeBool:_isVoice forKey:@"isVoice"]; + [aCoder encodeObject:_title forKey:@"title"]; + [aCoder encodeObject:_performer forKey:@"performer"]; + [aCoder encodeInt32:_duration forKey:@"duration"]; + [aCoder encodeObject:_waveform forKey:@"waveform"]; +} + +- (void)encodeWithKeyValueCoder:(PSKeyValueCoder *)coder +{ + [coder encodeInt32:_isVoice ? 1 : 0 forCKey:"isVoice"]; + [coder encodeString:_title forCKey:"title"]; + [coder encodeString:_performer forCKey:"performer"]; + [coder encodeInt32:_duration forCKey:"duration"]; + [coder encodeObject:_waveform forCKey:"waveform"]; +} + +- (BOOL)isEqual:(id)object +{ + return [object isKindOfClass:[TGDocumentAttributeAudio class]] && TGStringCompare(((TGDocumentAttributeAudio *)object)->_title, _title) && TGStringCompare(((TGDocumentAttributeAudio *)object)->_performer, _performer) && ((TGDocumentAttributeAudio *)object)->_duration == _duration && _isVoice == ((TGDocumentAttributeAudio *)object)->_isVoice; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGDocumentAttributeFilename.h b/submodules/LegacyComponents/LegacyComponents/TGDocumentAttributeFilename.h new file mode 100644 index 0000000000..d2635839af --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGDocumentAttributeFilename.h @@ -0,0 +1,9 @@ +#import + +@interface TGDocumentAttributeFilename : NSObject + +@property (nonatomic, strong, readonly) NSString *filename; + +- (instancetype)initWithFilename:(NSString *)filename; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGDocumentAttributeFilename.m b/submodules/LegacyComponents/LegacyComponents/TGDocumentAttributeFilename.m new file mode 100644 index 0000000000..3cad9315e6 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGDocumentAttributeFilename.m @@ -0,0 +1,45 @@ +#import "TGDocumentAttributeFilename.h" + +#import "LegacyComponentsInternal.h" + +#import "PSKeyValueCoder.h" + +@implementation TGDocumentAttributeFilename + +- (instancetype)initWithFilename:(NSString *)filename +{ + self = [super init]; + if (self != nil) + { + _filename = filename; + } + return self; +} + +- (instancetype)initWithKeyValueCoder:(PSKeyValueCoder *)coder +{ + return [self initWithFilename:[coder decodeStringForCKey:"f"]]; +} + +- (void)encodeWithKeyValueCoder:(PSKeyValueCoder *)coder +{ + [coder encodeString:_filename forCKey:"f"]; +} + +- (instancetype)initWithCoder:(NSCoder *)aDecoder +{ + return [self initWithFilename:[aDecoder decodeObjectForKey:@"filename"]]; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder +{ + if (_filename != nil) + [aCoder encodeObject:_filename forKey:@"filename"]; +} + +- (BOOL)isEqual:(id)object +{ + return [object isKindOfClass:[TGDocumentAttributeFilename class]] && TGObjectCompare(_filename, ((TGDocumentAttributeFilename *)object)->_filename); +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGDocumentAttributeImageSize.h b/submodules/LegacyComponents/LegacyComponents/TGDocumentAttributeImageSize.h new file mode 100644 index 0000000000..f8156c70b3 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGDocumentAttributeImageSize.h @@ -0,0 +1,11 @@ +#import + +#import + +@interface TGDocumentAttributeImageSize : NSObject + +@property (nonatomic, readonly) CGSize size; + +- (instancetype)initWithSize:(CGSize)size; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGDocumentAttributeImageSize.m b/submodules/LegacyComponents/LegacyComponents/TGDocumentAttributeImageSize.m new file mode 100644 index 0000000000..2d018d271f --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGDocumentAttributeImageSize.m @@ -0,0 +1,43 @@ +#import "TGDocumentAttributeImageSize.h" + +#import "PSKeyValueCoder.h" + +@implementation TGDocumentAttributeImageSize + +- (instancetype)initWithSize:(CGSize)size +{ + self = [super init]; + if (self != nil) + { + _size = size; + } + return self; +} + +- (instancetype)initWithKeyValueCoder:(PSKeyValueCoder *)coder +{ + return [self initWithSize:CGSizeMake([coder decodeInt32ForCKey:"w"], [coder decodeInt32ForCKey:"h"])]; +} + +- (void)encodeWithKeyValueCoder:(PSKeyValueCoder *)coder +{ + [coder encodeInt32:(int32_t)_size.width forCKey:"w"]; + [coder encodeInt32:(int32_t)_size.height forCKey:"h"]; +} + +- (instancetype)initWithCoder:(NSCoder *)aDecoder +{ + return [self initWithSize:[aDecoder decodeCGSizeForKey:@"size"]]; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder +{ + [aCoder encodeCGSize:_size forKey:@"size"]; +} + +- (BOOL)isEqual:(id)object +{ + return [object isKindOfClass:[TGDocumentAttributeImageSize class]] && CGSizeEqualToSize(_size, ((TGDocumentAttributeImageSize *)object)->_size); +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGDocumentAttributeSticker.h b/submodules/LegacyComponents/LegacyComponents/TGDocumentAttributeSticker.h new file mode 100644 index 0000000000..73ffb3a7e4 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGDocumentAttributeSticker.h @@ -0,0 +1,25 @@ +#import + +#import + +#import + +@interface TGStickerMaskDescription : NSObject + +@property (nonatomic, readonly) int32_t n; +@property (nonatomic, readonly) CGPoint point; +@property (nonatomic, readonly) CGFloat zoom; + +- (instancetype)initWithN:(int32_t)n point:(CGPoint)point zoom:(CGFloat)zoom; + +@end + +@interface TGDocumentAttributeSticker : NSObject + +@property (nonatomic, strong, readonly) NSString *alt; +@property (nonatomic, strong, readonly) id packReference; +@property (nonatomic, strong, readonly) TGStickerMaskDescription *mask; + +- (instancetype)initWithAlt:(NSString *)alt packReference:(id)packReference mask:(TGStickerMaskDescription *)mask; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGDocumentAttributeSticker.m b/submodules/LegacyComponents/LegacyComponents/TGDocumentAttributeSticker.m new file mode 100644 index 0000000000..5b96289448 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGDocumentAttributeSticker.m @@ -0,0 +1,122 @@ +#import "TGDocumentAttributeSticker.h" + +#import "LegacyComponentsInternal.h" + +#import "PSKeyValueCoder.h" + +@implementation TGStickerMaskDescription + +- (instancetype)initWithN:(int32_t)n point:(CGPoint)point zoom:(CGFloat)zoom { + self = [super init]; + if (self != nil) { + _n = n; + _point = point; + _zoom = zoom; + } + return self; +} + +- (instancetype)initWithKeyValueCoder:(PSKeyValueCoder *)coder { + return [self initWithN:[coder decodeInt32ForCKey:"n"] point:CGPointMake([coder decodeDoubleForCKey:"x"], [coder decodeDoubleForCKey:"y"]) zoom:[coder decodeDoubleForCKey:"z"]]; +} + +- (void)encodeWithKeyValueCoder:(PSKeyValueCoder *)coder { + [coder encodeInt32:_n forCKey:"n"]; + [coder encodeDouble:_point.x forCKey:"x"]; + [coder encodeDouble:_point.y forCKey:"y"]; + [coder encodeDouble:_zoom forCKey:"z"]; +} + +- (instancetype)initWithCoder:(NSCoder *)aDecoder { + return [self initWithN:[aDecoder decodeInt32ForKey:@"n"] point:CGPointMake([aDecoder decodeDoubleForKey:@"x"], [aDecoder decodeDoubleForKey:@"y"]) zoom:[aDecoder decodeDoubleForKey:@"z"]]; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder { + [aCoder encodeInt32:_n forKey:@"n"]; + [aCoder encodeDouble:_point.x forKey:@"x"]; + [aCoder encodeDouble:_point.y forKey:@"y"]; + [aCoder encodeDouble:_zoom forKey:@"z"]; +} + +- (BOOL)isEqual:(id)object { + if (![object isKindOfClass:[TGStickerMaskDescription class]]) { + return false; + } + TGStickerMaskDescription *other = object; + if (_n != other->_n) { + return false; + } + if (!CGPointEqualToPoint(_point, other->_point)) { + return false; + } + if (ABS(_zoom - other->_zoom) > FLT_EPSILON) { + return false; + } + return true; +} + +@end + +@implementation TGDocumentAttributeSticker + +- (instancetype)initWithAlt:(NSString *)alt packReference:(id)packReference mask:(TGStickerMaskDescription *)mask +{ + self = [super init]; + if (self != nil) + { + _alt = alt; + _packReference = packReference; + _mask = mask; + } + return self; +} + +- (instancetype)initWithKeyValueCoder:(PSKeyValueCoder *)coder +{ + return [self initWithAlt:[coder decodeStringForCKey:"alt"] packReference:(id)[coder decodeObjectForCKey:"packReference"] mask:(TGStickerMaskDescription *)[coder decodeObjectForCKey:"mask"]]; +} + +- (void)encodeWithKeyValueCoder:(PSKeyValueCoder *)coder +{ + [coder encodeString:_alt forCKey:"alt"]; + [coder encodeObject:_packReference forCKey:"packReference"]; + [coder encodeObject:_mask forCKey:"mask"]; +} + +- (instancetype)initWithCoder:(NSCoder *)aDecoder +{ + return [self initWithAlt:[aDecoder decodeObjectForKey:@"alt"] packReference:[aDecoder decodeObjectForKey:@"packReference"] mask:[aDecoder decodeObjectForKey:@"mask"]]; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder +{ + if (_alt != nil) + [aCoder encodeObject:_alt forKey:@"alt"]; + if (_packReference != nil) + [aCoder encodeObject:_packReference forKey:@"packReference"]; + if (_mask != nil) + [aCoder encodeObject:_mask forKey:@"mask"]; +} + +- (BOOL)isEqual:(id)object +{ + if (![object isKindOfClass:[TGDocumentAttributeSticker class]]) { + return false; + } + + if (!TGObjectCompare(_packReference, ((TGDocumentAttributeSticker *)object)->_packReference)) { + return false; + } + + if (!TGObjectCompare(_mask, ((TGDocumentAttributeSticker *)object)->_mask)) { + return false; + } + + if (!TGStringCompare(_alt, ((TGDocumentAttributeSticker *)object)->_alt)) { + return false; + } + + return true; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGDocumentAttributeVideo.h b/submodules/LegacyComponents/LegacyComponents/TGDocumentAttributeVideo.h new file mode 100644 index 0000000000..2c51dbd122 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGDocumentAttributeVideo.h @@ -0,0 +1,13 @@ +#import + +#import + +@interface TGDocumentAttributeVideo : NSObject + +@property (nonatomic, readonly) bool isRoundMessage; +@property (nonatomic, readonly) CGSize size; +@property (nonatomic, readonly) int32_t duration; + +- (instancetype)initWithRoundMessage:(bool)isRoundMessage size:(CGSize)size duration:(int32_t)duration; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGDocumentAttributeVideo.m b/submodules/LegacyComponents/LegacyComponents/TGDocumentAttributeVideo.m new file mode 100644 index 0000000000..fa9d9df3f5 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGDocumentAttributeVideo.m @@ -0,0 +1,42 @@ +#import "TGDocumentAttributeVideo.h" + +#import "PSKeyValueCoder.h" + +@implementation TGDocumentAttributeVideo + +- (instancetype)initWithRoundMessage:(bool)isRoundMessage size:(CGSize)size duration:(int32_t)duration { + self = [super init]; + if (self != nil) { + _isRoundMessage = isRoundMessage; + _size = size; + _duration = duration; + } + return self; +} + +- (instancetype)initWithCoder:(NSCoder *)aDecoder { + return [self initWithRoundMessage:[aDecoder decodeBoolForKey:@"roundMessage"] size:[aDecoder decodeCGSizeForKey:@"size"] duration:[aDecoder decodeInt32ForKey:@"duration"]]; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder { + [aCoder encodeBool:_isRoundMessage forKey:@"roundMessage"]; + [aCoder encodeCGSize:_size forKey:@"size"]; + [aCoder encodeInt32:_duration forKey:@"duration"]; +} + +- (instancetype)initWithKeyValueCoder:(PSKeyValueCoder *)coder { + return [self initWithRoundMessage:[coder decodeInt32ForCKey:"r"] size:CGSizeMake([coder decodeInt32ForCKey:"s.w"], [coder decodeInt32ForCKey:"s.h"]) duration:[coder decodeInt32ForCKey:"d"]]; +} + +- (void)encodeWithKeyValueCoder:(PSKeyValueCoder *)coder { + [coder encodeInt32:(int32_t)_isRoundMessage forKey:@"r"]; + [coder encodeInt32:(int32_t)_size.width forCKey:"s.w"]; + [coder encodeInt32:(int32_t)_size.height forCKey:"s.h"]; + [coder encodeInt32:_duration forCKey:"d"]; +} + +- (BOOL)isEqual:(id)object { + return [object isKindOfClass:[TGDocumentAttributeVideo class]] && _isRoundMessage == ((TGDocumentAttributeVideo *)object)->_isRoundMessage && CGSizeEqualToSize(_size, ((TGDocumentAttributeVideo *)object)->_size) && _duration == ((TGDocumentAttributeVideo *)object)->_duration; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGDocumentMediaAttachment.h b/submodules/LegacyComponents/LegacyComponents/TGDocumentMediaAttachment.h new file mode 100644 index 0000000000..44632dd3b4 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGDocumentMediaAttachment.h @@ -0,0 +1,52 @@ + + +#import + +#import +#import + +#import +#import +#import +#import +#import +#import + +#define TGDocumentMediaAttachmentType ((int)0xE6C64318) + +@interface TGDocumentMediaAttachment : TGMediaAttachment + +@property (nonatomic) int64_t localDocumentId; + +@property (nonatomic) int64_t documentId; +@property (nonatomic) int64_t accessHash; +@property (nonatomic) int datacenterId; +@property (nonatomic) int32_t userId; +@property (nonatomic) int date; +@property (nonatomic, strong) NSString *mimeType; +@property (nonatomic) int size; +@property (nonatomic) int32_t version; +@property (nonatomic, strong) TGImageInfo *thumbnailInfo; + +@property (nonatomic, strong) NSString *documentUri; + +@property (nonatomic, strong) NSArray *attributes; +@property (nonatomic, strong) NSString *caption; + +@property (nonatomic, readonly) NSArray *textCheckingResults; + +@property (nonatomic, strong) TGMediaOriginInfo *originInfo; + +- (NSString *)safeFileName; ++ (NSString *)safeFileNameForFileName:(NSString *)fileName; +- (NSString *)fileName; + +- (bool)isAnimated; +- (bool)isSticker; +- (bool)isStickerWithPack; +- (id)stickerPackReference; +- (bool)isVoice; +- (CGSize)pictureSize; +- (bool)isRoundVideo; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGDocumentMediaAttachment.m b/submodules/LegacyComponents/LegacyComponents/TGDocumentMediaAttachment.m new file mode 100644 index 0000000000..5e8b328d64 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGDocumentMediaAttachment.m @@ -0,0 +1,437 @@ +#import "TGDocumentMediaAttachment.h" + +#import "LegacyComponentsInternal.h" + +#import "PSCoding.h" +#import "PSKeyValueEncoder.h" +#import "PSKeyValueDecoder.h" + +#import "TGMessage.h" + +@interface TGDocumentMediaAttachment () { + NSArray *_textCheckingResults; +} + +@end + +@implementation TGDocumentMediaAttachment + +- (id)init +{ + self = [super init]; + if (self != nil) + { + self.type = TGDocumentMediaAttachmentType; + } + return self; +} + +- (instancetype)copyWithZone:(NSZone *)__unused zone { + TGDocumentMediaAttachment *document = [[TGDocumentMediaAttachment alloc] init]; + + document->_documentId = _documentId; + document->_localDocumentId = _localDocumentId; + document->_accessHash = _accessHash; + document->_datacenterId = _datacenterId; + document->_userId = _userId; + document->_date = _date; + document->_mimeType = _mimeType; + document->_size = _size; + document->_thumbnailInfo = _thumbnailInfo; + document->_documentUri = _documentUri; + document->_attributes = _attributes; + document->_caption = _caption; + document->_textCheckingResults = _textCheckingResults; + document->_version = _version; + document->_originInfo = _originInfo; + + return document; +} + +- (instancetype)initWithCoder:(NSCoder *)aDecoder +{ + self = [super init]; + if (self != nil) + { + self.type = TGDocumentMediaAttachmentType; + + _documentId = [aDecoder decodeInt64ForKey:@"documentId"]; + _localDocumentId = [aDecoder decodeInt64ForKey:@"localDocumentId"]; + _accessHash = [aDecoder decodeInt64ForKey:@"accessHash"]; + _datacenterId = [aDecoder decodeInt32ForKey:@"datacenterId"]; + _userId = [aDecoder decodeInt32ForKey:@"userId"]; + _date = [aDecoder decodeInt32ForKey:@"date"]; + _mimeType = [aDecoder decodeObjectForKey:@"mimeType"]; + _size = [aDecoder decodeInt32ForKey:@"size"]; + _thumbnailInfo = [aDecoder decodeObjectForKey:@"thumbnailInfo"]; + _documentUri = [aDecoder decodeObjectForKey:@"documentUri"]; + _attributes = [aDecoder decodeObjectForKey:@"attributes"]; + _caption = [aDecoder decodeObjectForKey:@"caption"]; + _version = [aDecoder decodeInt32ForKey:@"version"]; + _originInfo = [aDecoder decodeObjectForKey:@"origin"]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder +{ + [aCoder encodeInt64:_documentId forKey:@"documentId"]; + [aCoder encodeInt64:_localDocumentId forKey:@"localDocumentId"]; + [aCoder encodeInt64:_accessHash forKey:@"accessHash"]; + [aCoder encodeInt32:_datacenterId forKey:@"datacenterId"]; + [aCoder encodeInt32:_userId forKey:@"userId"]; + [aCoder encodeInt32:_date forKey:@"date"]; + [aCoder encodeInt32:_version forKey:@"version"]; + if (_mimeType != nil) + [aCoder encodeObject:_mimeType forKey:@"mimeType"]; + [aCoder encodeInt32:_size forKey:@"size"]; + if (_thumbnailInfo != nil) + [aCoder encodeObject:_thumbnailInfo forKey:@"thumbnailInfo"]; + if (_documentUri != nil) + [aCoder encodeObject:_documentUri forKey:@"documentUri"]; + if (_attributes != nil) + [aCoder encodeObject:_attributes forKey:@"attributes"]; + if (_caption != nil) + [aCoder encodeObject:_caption forKey:@"caption"]; + if (_originInfo != nil) + [aCoder encodeObject:_originInfo forKey:@"origin"]; +} + +- (BOOL)isEqual:(id)object +{ + if (object == self) + return true; + + if (![object isKindOfClass:[TGDocumentMediaAttachment class]]) + return false; + + TGDocumentMediaAttachment *other = object; + if (_documentId == other->_documentId && _localDocumentId == other->_localDocumentId && _accessHash == other->_accessHash && _datacenterId == other->_datacenterId && _userId == other->_userId && _date == other->_date && TGObjectCompare(_mimeType, other->_mimeType) && _size == other->_size && TGObjectCompare(_thumbnailInfo, other->_thumbnailInfo) && TGObjectCompare(_documentUri, other->_documentUri) && TGObjectCompare(_attributes, other->_attributes) && TGStringCompare(_caption, other->_caption) && _version == other->_version) + return true; + return false; +} + +- (void)serialize:(NSMutableData *)data +{ + int dataLengthPtr = (int)data.length; + int zero = 0; + [data appendBytes:&zero length:4]; + + uint8_t version = 7; + [data appendBytes:&version length:sizeof(version)]; + + [data appendBytes:&_localDocumentId length:sizeof(_localDocumentId)]; + + [data appendBytes:&_documentId length:sizeof(_documentId)]; + [data appendBytes:&_accessHash length:sizeof(_accessHash)]; + [data appendBytes:&_datacenterId length:sizeof(_datacenterId)]; + [data appendBytes:&_userId length:sizeof(_userId)]; + [data appendBytes:&_date length:sizeof(_date)]; + + NSData *filenameData = [[self fileName] dataUsingEncoding:NSUTF8StringEncoding]; + int filenameLength = (int)filenameData.length; + [data appendBytes:&filenameLength length:sizeof(filenameLength)]; + [data appendData:filenameData]; + + NSData *mimeData = [_mimeType dataUsingEncoding:NSUTF8StringEncoding]; + int mimeLength = (int)mimeData.length; + [data appendBytes:&mimeLength length:sizeof(mimeLength)]; + [data appendData:mimeData]; + + [data appendBytes:&_size length:sizeof(_size)]; + + uint8_t thumbnailExists = _thumbnailInfo != nil; + [data appendBytes:&thumbnailExists length:sizeof(thumbnailExists)]; + [_thumbnailInfo serialize:data]; + + NSData *uriData = [_documentUri dataUsingEncoding:NSUTF8StringEncoding]; + int uriLength = (int)uriData.length; + [data appendBytes:&uriLength length:sizeof(uriLength)]; + if (uriData != nil) + [data appendData:uriData]; + + PSKeyValueEncoder *encoder = [[PSKeyValueEncoder alloc] init]; + [encoder encodeArray:_attributes forCKey:"attributes"]; + int32_t attributesLength = (int32_t)encoder.data.length; + [data appendBytes:&attributesLength length:4]; + [data appendData:encoder.data]; + + NSData *captionData = [_caption dataUsingEncoding:NSUTF8StringEncoding]; + int32_t captionLength = (int)captionData.length; + [data appendBytes:&captionLength length:sizeof(captionLength)]; + if (captionData != nil) { + [data appendData:captionData]; + } + + [data appendBytes:&_version length:4]; + + NSData *originData = nil; + @try { + originData = [NSKeyedArchiver archivedDataWithRootObject:_originInfo]; + } @catch (NSException *e) { + + } + int32_t originLength = (int)originData.length; + [data appendBytes:&originLength length:sizeof(originLength)]; + if (originData != nil) { + [data appendData:originData]; + } + + int dataLength = (int)(data.length - dataLengthPtr - 4); + [data replaceBytesInRange:NSMakeRange(dataLengthPtr, 4) withBytes:&dataLength]; +} + +- (TGMediaAttachment *)parseMediaAttachment:(NSInputStream *)is +{ + int dataLength = 0; + [is read:(uint8_t *)&dataLength maxLength:4]; + + uint8_t version = 0; + [is read:&version maxLength:sizeof(version)]; + if (version != 1 && version != 2 && version != 3 && version != 4 && version != 5 && version != 6 && version != 7) + { + TGLegacyLog(@"***** Document serialized version mismatch"); + return nil; + } + + TGDocumentMediaAttachment *documentAttachment = [[TGDocumentMediaAttachment alloc] init]; + + if (version >= 2) + [is read:(uint8_t *)&documentAttachment->_localDocumentId maxLength:sizeof(documentAttachment->_localDocumentId)]; + + [is read:(uint8_t *)&documentAttachment->_documentId maxLength:sizeof(documentAttachment->_documentId)]; + [is read:(uint8_t *)&documentAttachment->_accessHash maxLength:sizeof(documentAttachment->_accessHash)]; + [is read:(uint8_t *)&documentAttachment->_datacenterId maxLength:sizeof(documentAttachment->_datacenterId)]; + [is read:(uint8_t *)&documentAttachment->_userId maxLength:sizeof(documentAttachment->_userId)]; + [is read:(uint8_t *)&documentAttachment->_date maxLength:sizeof(documentAttachment->_date)]; + + NSString *legacyFileName = @"file"; + int filenameLength = 0; + [is read:(uint8_t *)&filenameLength maxLength:sizeof(filenameLength)]; + if (filenameLength != 0) + { + uint8_t *filenameBytes = malloc(filenameLength); + [is read:filenameBytes maxLength:filenameLength]; + legacyFileName = [[NSString alloc] initWithBytesNoCopy:filenameBytes length:filenameLength encoding:NSUTF8StringEncoding freeWhenDone:true]; + } + + int mimeLength = 0; + [is read:(uint8_t *)&mimeLength maxLength:sizeof(mimeLength)]; + if (mimeLength != 0) + { + uint8_t *mimeBytes = malloc(mimeLength); + [is read:mimeBytes maxLength:mimeLength]; + documentAttachment.mimeType = [[NSString alloc] initWithBytesNoCopy:mimeBytes length:mimeLength encoding:NSUTF8StringEncoding freeWhenDone:true]; + } + + [is read:(uint8_t *)&documentAttachment->_size maxLength:sizeof(documentAttachment->_size)]; + + uint8_t thumbnailExists = 0; + [is read:&thumbnailExists maxLength:sizeof(thumbnailExists)]; + if (thumbnailExists) + { + documentAttachment.thumbnailInfo = [TGImageInfo deserialize:is]; + } + + if (version >= 3) + { + int uriLength = 0; + [is read:(uint8_t *)&uriLength maxLength:sizeof(uriLength)]; + if (uriLength > 0) + { + uint8_t *uriBytes = malloc(uriLength); + [is read:uriBytes maxLength:uriLength]; + documentAttachment.documentUri = [[NSString alloc] initWithBytesNoCopy:uriBytes length:uriLength encoding:NSUTF8StringEncoding freeWhenDone:true]; + } + } + + if (version >= 4) + { + int32_t attributesSize = 0; + [is read:(uint8_t *)&attributesSize maxLength:4]; + if (attributesSize > 0) + { + uint8_t *attributeBytes = malloc(attributesSize); + [is read:attributeBytes maxLength:attributesSize]; + NSData *data = [[NSData alloc] initWithBytesNoCopy:attributeBytes length:attributesSize freeWhenDone:true]; + PSKeyValueDecoder *decoder = [[PSKeyValueDecoder alloc] initWithData:data]; + documentAttachment.attributes = [decoder decodeArrayForCKey:"attributes"]; + } + } + + if (version >= 5) { + int32_t captionLength = 0; + [is read:(uint8_t *)&captionLength maxLength:sizeof(captionLength)]; + if (captionLength > 0) + { + uint8_t *captionBytes = malloc(captionLength); + [is read:captionBytes maxLength:captionLength]; + documentAttachment.caption = [[NSString alloc] initWithBytesNoCopy:captionBytes length:captionLength encoding:NSUTF8StringEncoding freeWhenDone:true]; + } + } + + if (version >= 6) { + int32_t fileVersion = 0; + [is read:(uint8_t *)&fileVersion maxLength:4]; + documentAttachment.version = fileVersion; + } + + if (version >= 7) { + int32_t originLength = 0; + [is read:(uint8_t *)&originLength maxLength:sizeof(originLength)]; + if (originLength > 0) + { + uint8_t *originBytes = malloc(originLength); + [is read:originBytes maxLength:originLength]; + NSData *data = [[NSData alloc] initWithBytesNoCopy:originBytes length:originLength freeWhenDone:true]; + TGMediaOriginInfo *origin = nil; + @try { + origin = [NSKeyedUnarchiver unarchiveObjectWithData:data]; + } @catch (NSException *e) { + + } + documentAttachment.originInfo = origin; + } + } + + bool hasFilenameAttribute = false; + for (id attribute in documentAttachment.attributes) + { + if ([attribute isKindOfClass:[TGDocumentAttributeFilename class]]) + { + hasFilenameAttribute = true; + break; + } + } + + if (!hasFilenameAttribute) + { + NSMutableArray *array = [[NSMutableArray alloc] initWithArray:documentAttachment.attributes]; + [array addObject:[[TGDocumentAttributeFilename alloc] initWithFilename:legacyFileName]]; + documentAttachment.attributes = array; + } + + return documentAttachment; +} + +- (NSString *)fileName +{ + NSString *fileName = @"file"; + for (id attribute in _attributes) + { + if ([attribute isKindOfClass:[TGDocumentAttributeFilename class]]) + { + fileName = ((TGDocumentAttributeFilename *)attribute).filename; + break; + } + } + + return fileName; +} + +- (NSString *)safeFileName +{ + return [TGDocumentMediaAttachment safeFileNameForFileName:[self fileName]]; +} + ++ (NSString *)safeFileNameForFileName:(NSString *)fileName +{ + if (fileName.length == 0) + return @"file"; + + return [fileName stringByReplacingOccurrencesOfString:@"/" withString:@"_"]; +} + +- (bool)isAnimated { + for (id attribute in _attributes) { + if ([attribute isKindOfClass:[TGDocumentAttributeAnimated class]]) { + return true; + } + } + + return false; +} + + +- (bool)isSticker { + for (id attribute in _attributes) { + if ([attribute isKindOfClass:[TGDocumentAttributeSticker class]]) { + return true; + } + } + + return false; +} + +- (bool)isStickerWithPack { + for (id attribute in _attributes) { + if ([attribute isKindOfClass:[TGDocumentAttributeSticker class]]) { + return ((TGDocumentAttributeSticker *)attribute).packReference != nil; + } + } + + return false; +} + +- (id)stickerPackReference { + for (id attribute in _attributes) { + if ([attribute isKindOfClass:[TGDocumentAttributeSticker class]]) { + return ((TGDocumentAttributeSticker *)attribute).packReference; + } + } + return nil; +} + +- (bool)isVoice { + for (id attribute in _attributes) { + if ([attribute isKindOfClass:[TGDocumentAttributeAudio class]]) { + TGDocumentAttributeAudio *audio = attribute; + if (audio.isVoice) { + return true; + } + } + } + + return false; +} + +- (bool)isRoundVideo { + for (id attribute in _attributes) { + if ([attribute isKindOfClass:[TGDocumentAttributeVideo class]]) { + TGDocumentAttributeVideo *video = attribute; + if (video.isRoundMessage) { + return true; + } + } + } + + return false; +} + +- (CGSize)pictureSize { + for (id attribute in _attributes) { + if ([attribute isKindOfClass:[TGDocumentAttributeImageSize class]]) { + return ((TGDocumentAttributeImageSize *)attribute).size; + } else if ([attribute isKindOfClass:[TGDocumentAttributeVideo class]]) { + return ((TGDocumentAttributeVideo *)attribute).size; + } + } + + return CGSizeZero; +} + +- (NSArray *)textCheckingResults +{ + if (_caption.length < 2) + _textCheckingResults = [NSArray array]; + + if (_textCheckingResults == nil) + { + NSArray *textCheckingResults = [TGMessage textCheckingResultsForText:_caption highlightMentionsAndTags:true highlightCommands:true entities:nil]; + _textCheckingResults = textCheckingResults ?: [NSArray array]; + } + + return _textCheckingResults; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGDoubleTapGestureRecognizer.h b/submodules/LegacyComponents/LegacyComponents/TGDoubleTapGestureRecognizer.h new file mode 100644 index 0000000000..151d7d6555 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGDoubleTapGestureRecognizer.h @@ -0,0 +1,27 @@ +#import + +@interface TGDoubleTapGestureRecognizer : UIGestureRecognizer + +@property (nonatomic) bool consumeSingleTap; +@property (nonatomic) bool doubleTapped; +@property (nonatomic) bool longTapped; +@property (nonatomic) bool avoidControls; + +- (bool)canScrollViewStealTouches; + +@end + +@protocol TGDoubleTapGestureRecognizerDelegate + +@optional + +- (int)gestureRecognizer:(TGDoubleTapGestureRecognizer *)recognizer shouldFailTap:(CGPoint)point; +- (void)gestureRecognizer:(TGDoubleTapGestureRecognizer *)recognizer shouldBeginAtPoint:(CGPoint)point; +- (void)gestureRecognizer:(TGDoubleTapGestureRecognizer *)recognizer didBeginAtPoint:(CGPoint)point; +- (void)gestureRecognizerDidFail:(TGDoubleTapGestureRecognizer *)recognizer; +- (bool)gestureRecognizerShouldHandleLongTap:(TGDoubleTapGestureRecognizer *)recognizer; +- (void)doubleTapGestureRecognizerSingleTapped:(TGDoubleTapGestureRecognizer *)recognizer; +- (bool)gestureRecognizerShouldLetScrollViewStealTouches:(TGDoubleTapGestureRecognizer *)recognizer; +- (bool)gestureRecognizerShouldFailOnMove:(TGDoubleTapGestureRecognizer *)recognizer; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGDoubleTapGestureRecognizer.m b/submodules/LegacyComponents/LegacyComponents/TGDoubleTapGestureRecognizer.m new file mode 100644 index 0000000000..c2bd807c98 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGDoubleTapGestureRecognizer.m @@ -0,0 +1,274 @@ +#import "TGDoubleTapGestureRecognizer.h" + +#import + +@interface TGDoubleTapGestureRecognizerTimerTarget : NSObject + +@property (nonatomic, weak) id target; +@property (nonatomic) SEL action; + ++ (NSTimer *)scheduledMainThreadTimerWithTarget:(id)target action:(SEL)action interval:(NSTimeInterval)interval repeat:(bool)repeat; ++ (NSTimer *)scheduledMainThreadTimerWithTarget:(id)target action:(SEL)action interval:(NSTimeInterval)interval repeat:(bool)repeat runLoopModes:(NSString *)runLoopModes; + +@end + +@implementation TGDoubleTapGestureRecognizerTimerTarget + ++ (NSTimer *)scheduledMainThreadTimerWithTarget:(id)target action:(SEL)action interval:(NSTimeInterval)interval repeat:(bool)repeat +{ + return [self scheduledMainThreadTimerWithTarget:target action:action interval:interval repeat:repeat runLoopModes:NSRunLoopCommonModes]; +} + ++ (NSTimer *)scheduledMainThreadTimerWithTarget:(id)target action:(SEL)action interval:(NSTimeInterval)interval repeat:(bool)repeat runLoopModes:(NSString *)runLoopModes +{ + TGDoubleTapGestureRecognizerTimerTarget *timerTarget = [[TGDoubleTapGestureRecognizerTimerTarget alloc] init]; + timerTarget.target = target; + timerTarget.action = action; + + NSTimer *timer = [[NSTimer alloc] initWithFireDate:[NSDate dateWithTimeIntervalSinceNow:interval] interval:interval target:timerTarget selector:@selector(timerEvent) userInfo:nil repeats:repeat]; + [[NSRunLoop mainRunLoop] addTimer:timer forMode:runLoopModes]; + return timer; +} + +- (void)timerEvent +{ + id target = _target; + if ([target respondsToSelector:_action]) + { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Warc-performSelector-leaks" + [target performSelector:_action]; +#pragma clang diagnostic pop + } +} + +@end + +@interface TGDoubleTapGestureRecognizer () { + CGPoint _touchLocation; +} + +@property (nonatomic, strong) NSTimer *tapTimer; +@property (nonatomic, strong) NSTimer *longPressTimer; +@property (nonatomic) int currentTapCount; + +@end + +@implementation TGDoubleTapGestureRecognizer + +- (id)initWithTarget:(id)target action:(SEL)action +{ + self = [super initWithTarget:target action:action]; + if (self != nil) + { + } + return self; +} + +- (void)failGesture +{ + if (_tapTimer != nil) + { + [_tapTimer invalidate]; + _tapTimer = nil; + } + + if (_longPressTimer != nil) + { + [_longPressTimer invalidate]; + _longPressTimer = nil; + } + + self.state = UIGestureRecognizerStateFailed; + + if ([self.delegate respondsToSelector:@selector(gestureRecognizerDidFail:)]) + [(id)self.delegate gestureRecognizerDidFail:self]; +} + +- (void)endGesture +{ + if (_tapTimer != nil) + { + [_tapTimer invalidate]; + _tapTimer = nil; + } + + if (_longPressTimer != nil) + { + [_longPressTimer invalidate]; + _longPressTimer = nil; + } + + self.state = UIGestureRecognizerStateRecognized; +} + +- (void)reset +{ + if (_tapTimer != nil) + { + [_tapTimer invalidate]; + _tapTimer = nil; + } + + if (_longPressTimer != nil) + { + [_longPressTimer invalidate]; + _longPressTimer = nil; + } + + _currentTapCount = 0; + + _doubleTapped = false; + _longTapped = false; + + [super reset]; +} + +- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event +{ + if (_avoidControls) + { + for (UITouch *touch in [touches allObjects]) + { + UIView *hitResult = [self.view hitTest:[touch locationInView:self.view] withEvent:event]; + if ([hitResult isKindOfClass:[UIControl class]]) + { + [self failGesture]; + return; + } + } + } + + _touchLocation = [(UITouch *)[touches anyObject] locationInView:[self view]]; + + [super touchesBegan:touches withEvent:event]; + + if ([self numberOfTouches] > 1) + { + [self failGesture]; + return; + } + + if (_tapTimer != nil) + { + [_tapTimer invalidate]; + _tapTimer = nil; + } + + if (_longPressTimer != nil) + { + [_longPressTimer invalidate]; + _longPressTimer = nil; + } + + if (_currentTapCount == 0) + { + _currentTapCount++; + + if ([self.delegate respondsToSelector:@selector(gestureRecognizer:didBeginAtPoint:)]) + { + [(id)self.delegate gestureRecognizer:self didBeginAtPoint:[[touches anyObject] locationInView:self.view]]; + } + + _longPressTimer = [TGDoubleTapGestureRecognizerTimerTarget scheduledMainThreadTimerWithTarget:self action:@selector(longTapEvent) interval:0.4 repeat:false]; + } + else if (_currentTapCount >= 1) + { + _doubleTapped = true; + + [self endGesture]; + } +} + +- (void)touchesMoved:(NSSet *)__unused touches withEvent:(UIEvent *)__unused event +{ + CGPoint location = [(UITouch *)[touches anyObject] locationInView:[self view]]; + CGPoint distance = CGPointMake(location.x - _touchLocation.x, location.y - _touchLocation.y); + if (distance.x * distance.x + distance.y * distance.y > 4.0 * 4.0) { + + if ([self.delegate respondsToSelector:@selector(gestureRecognizerShouldFailOnMove:)]) + { + if (![(id)self.delegate gestureRecognizerShouldFailOnMove:self]) + return; + } + + [self failGesture]; + } +} + +- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event +{ + if (_currentTapCount == 1) + { + UITouch *touch = [touches anyObject]; + int failTapType = 0; + if ([self.delegate conformsToProtocol:@protocol(TGDoubleTapGestureRecognizerDelegate)] && (failTapType = [(id)self.delegate gestureRecognizer:self shouldFailTap:[touch locationInView:self.view]])) + { + _doubleTapped = false; + if ((_consumeSingleTap && failTapType != 2) || failTapType == 3) + [self endGesture]; + else + [self failGesture]; + } + else + { + _tapTimer = [TGDoubleTapGestureRecognizerTimerTarget scheduledMainThreadTimerWithTarget:self action:@selector(tapTimerEvent) interval:0.2 repeat:false]; + } + } + + [super touchesEnded:touches withEvent:event]; +} + +- (void)tapTimerEvent +{ + _tapTimer = nil; + + _doubleTapped = false; + + if ([self.delegate conformsToProtocol:@protocol(TGDoubleTapGestureRecognizerDelegate)] && [self.delegate respondsToSelector:@selector(doubleTapGestureRecognizerSingleTapped:)]) + { + [(id)self.delegate doubleTapGestureRecognizerSingleTapped:self]; + } + + if (_consumeSingleTap) + [self endGesture]; + else + [self failGesture]; +} + +- (void)longTapEvent +{ + _longPressTimer = nil; + + if ([self.delegate respondsToSelector:@selector(gestureRecognizerShouldHandleLongTap:)]) + { + if ([(id)self.delegate gestureRecognizerShouldHandleLongTap:self]) + { + _longTapped = true; + + [self endGesture]; + } + } +} + +- (bool)canScrollViewStealTouches +{ + if ([self.delegate respondsToSelector:@selector(gestureRecognizerShouldLetScrollViewStealTouches:)]) + { + if ([(id)self.delegate gestureRecognizerShouldLetScrollViewStealTouches:self]) + return true; + else + return false; + } + + return true; +} + +- (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event +{ + [super touchesCancelled:touches withEvent:event]; + + [self failGesture]; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGDraggableCollectionView.h b/submodules/LegacyComponents/LegacyComponents/TGDraggableCollectionView.h new file mode 100644 index 0000000000..e61f41d011 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGDraggableCollectionView.h @@ -0,0 +1,22 @@ +#import + +@interface TGDraggableCollectionView : UICollectionView + +@property (nonatomic, assign) bool draggable; +@property (nonatomic, assign) UIEdgeInsets scrollingTriggerEdgeInsets; +@property (nonatomic, assign) CGFloat scrollingSpeed; + +@property (nonatomic, weak) UIView *draggedViewSuperview; + +@end + +@protocol TGDraggableCollectionViewDataSource +@optional + +- (void)collectionView:(UICollectionView *)collectionView itemAtIndexPath:(NSIndexPath *)sourceIndexPath willMoveToIndexPath:(NSIndexPath *)destinationIndexPath; +- (void)collectionView:(UICollectionView *)collectionView itemAtIndexPath:(NSIndexPath *)sourceIndexPath didMoveToIndexPath:(NSIndexPath *)destinationIndexPath; + +- (bool)collectionView:(UICollectionView *)collectionView canMoveItemAtIndexPath:(NSIndexPath *)sourceIndexPath; +- (bool)collectionView:(UICollectionView *)collectionView canMoveItemAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGDraggableCollectionView.m b/submodules/LegacyComponents/LegacyComponents/TGDraggableCollectionView.m new file mode 100644 index 0000000000..81dcf9b0b7 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGDraggableCollectionView.m @@ -0,0 +1,498 @@ +#import "TGDraggableCollectionView.h" + +#import + +#import "TGDraggableCollectionViewFlowLayout.h" + +NSString * const TGDraggableCollectionViewLayoutKey = @"collectionViewLayout"; +const UIEdgeInsets TGDraggableDefaultScrollingTriggerEdgeInsets = { 60, 60, 60, 60 }; +const CGFloat TGDraggableDefaultScrollingSpeed = 300.0f; + +typedef enum +{ + TGDraggableCollectionScrollingDirectionUnknown, + TGDraggableCollectionScrollingDirectionUp, + TGDraggableCollectionScrollingDirectionLeft, + TGDraggableCollectionScrollingDirectionRight, + TGDraggableCollectionScrollingDirectionDown +} TGDraggableCollectionScrollingDirection; + +@interface TGDraggableCollectionView () +{ + UIView *_draggedView; + UIView *_insideSnapshotView; + UIView *_outsideSnapshotView; + + NSIndexPath *_lastIndexPath; + + UIPanGestureRecognizer *_panGestureRecognizer; + UILongPressGestureRecognizer *_pressGestureRecognizer; + + CADisplayLink *_scrollingDisplayLink; + TGDraggableCollectionScrollingDirection _scrollingDirection; +} + +@end + +@implementation TGDraggableCollectionView + +- (instancetype)initWithFrame:(CGRect)frame collectionViewLayout:(UICollectionViewLayout *)layout +{ + NSAssert([layout isKindOfClass:[TGDraggableCollectionViewFlowLayout class]], @"collectionViewLayout must be an instance of TGDraggableCollectionViewFlowLayout"); + + self = [super initWithFrame:frame collectionViewLayout:layout]; + if (self != nil) + { + _scrollingTriggerEdgeInsets = TGDraggableDefaultScrollingTriggerEdgeInsets; + _scrollingSpeed = TGDraggableDefaultScrollingSpeed; + + _panGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handleDragPan:)]; + _panGestureRecognizer.delegate = self; + [self addGestureRecognizer:_panGestureRecognizer]; + + _pressGestureRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleDragPress:)]; + _pressGestureRecognizer.minimumPressDuration = 0.25f; + _pressGestureRecognizer.delegate = self; + for (UIGestureRecognizer *gestureRecognizer in self.gestureRecognizers) + { + if ([gestureRecognizer isKindOfClass:[UILongPressGestureRecognizer class]]) + [gestureRecognizer requireGestureRecognizerToFail:_pressGestureRecognizer]; + } + [self addGestureRecognizer:_pressGestureRecognizer]; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(handleApplicationWillResignActive:) + name:UIApplicationWillResignActiveNotification + object:nil]; + } + return self; +} + +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self + name:UIApplicationWillResignActiveNotification + object:nil]; +} + +- (void)handleApplicationWillResignActive:(NSNotification *)__unused notification +{ + _pressGestureRecognizer.enabled = false; + _pressGestureRecognizer.enabled = true; +} + +- (void)setDraggable:(bool)draggable +{ + _draggable = draggable; + _pressGestureRecognizer.enabled = draggable; + _panGestureRecognizer.enabled = draggable; +} + +#pragma mark - Items + +- (TGDraggableCollectionViewFlowLayout *)draggableLayout +{ + return (TGDraggableCollectionViewFlowLayout *)self.draggableLayout; +} + +- (NSIndexPath *)indexPathForItemNearPoint:(CGPoint)point +{ + NSIndexPath *destinationIndexPath = self.draggableLayout.destinationIndexPath; + self.draggableLayout.destinationIndexPath = nil; + NSArray *layoutAttributes = [self.draggableLayout layoutAttributesForElementsInRect:self.bounds]; + self.draggableLayout.destinationIndexPath = destinationIndexPath; + + NSIndexPath *nearestItemIndexPath = nil; + CGFloat minDistance = FLT_MAX; + + for (UICollectionViewLayoutAttributes *attributes in layoutAttributes) + { + CGFloat deltaX = attributes.center.x - point.x; + CGFloat deltaY = attributes.center.y - point.y; + CGFloat distance = (CGFloat)sqrt(deltaX * deltaX + deltaY * deltaY); + + if (distance < minDistance) + { + minDistance = distance; + nearestItemIndexPath = attributes.indexPath; + } + } + + return nearestItemIndexPath; +} + +#pragma mark - Dragged View + +- (UIView *)_draggedViewSuperview +{ + return (self.draggedViewSuperview != nil) ? self.draggedViewSuperview : self; +} + +- (UIView *)_prepareDraggedViewForCell:(UICollectionViewCell *)cell +{ + UIView *view = [[UIView alloc] initWithFrame:[self convertRect:cell.frame toView:[self _draggedViewSuperview]]]; + view.layer.rasterizationScale = TGScreenScaling(); + + _outsideSnapshotView = [cell snapshotViewAfterScreenUpdates:false]; + [view addSubview:_outsideSnapshotView]; + + _insideSnapshotView = [cell snapshotViewAfterScreenUpdates:false]; + + _outsideSnapshotView.layer.shadowOffset = CGSizeZero; + _outsideSnapshotView.layer.shadowColor = [UIColor blackColor].CGColor; + _outsideSnapshotView.layer.shadowRadius = 3.0f; + _outsideSnapshotView.layer.shadowOpacity = 0.87f; + _outsideSnapshotView.layer.shadowPath = [UIBezierPath bezierPathWithRect:view.bounds].CGPath; + + return view; +} + +- (void)_performDraggedViewHighlightTransitionWithDuration:(CGFloat)duration completion:(void (^)(void))completion +{ + [self addSubview:_insideSnapshotView]; + _insideSnapshotView.center = [self convertPoint:_draggedView.center fromView:[self _draggedViewSuperview]]; + + _draggedView.layer.shouldRasterize = true; + _draggedView.alpha = 0.0f; + + [UIView animateWithDuration:duration delay:0.0f options:UIViewAnimationOptionBeginFromCurrentState animations:^ + { + _draggedView.alpha = 1.0f; + _draggedView.transform = CGAffineTransformMakeScale(1.12f, 1.12f); + _insideSnapshotView.transform = _draggedView.transform; + } completion:^(BOOL finished) + { + if (finished) + { + _insideSnapshotView.hidden = true; + if (completion != nil) + completion(); + } + }]; +} + +- (void)_performDraggedViewUnhighlightTransitionWithDuration:(CGFloat)duration targetCellCenter:(CGPoint)targetCellCenter completion:(void (^)(void))completion +{ + _insideSnapshotView.center = [self convertPoint:_draggedView.center fromView:[self _draggedViewSuperview]]; + _insideSnapshotView.hidden = false; + + [UIView animateWithDuration:duration delay:0.0f options:UIViewAnimationOptionBeginFromCurrentState animations:^ + { + _draggedView.alpha = 0.0f; + _draggedView.transform = CGAffineTransformIdentity; + _insideSnapshotView.transform = _draggedView.transform; + _draggedView.center = [self convertPoint:targetCellCenter toView:[self _draggedViewSuperview]]; + _insideSnapshotView.center = targetCellCenter; + } completion:^(__unused BOOL finished) + { + if (completion != nil) + completion(); + }]; +} + +- (void)_performDraggedViewDropToIndexPath:(NSIndexPath *)destinationIndexPath completion:(void (^)(void))completion +{ + UICollectionViewLayoutAttributes *layoutAttributes = [self layoutAttributesForItemAtIndexPath:destinationIndexPath]; + + [self _performDraggedViewUnhighlightTransitionWithDuration:0.25f targetCellCenter:layoutAttributes.center completion:^ + { + [_draggedView removeFromSuperview]; + _draggedView = nil; + [_outsideSnapshotView removeFromSuperview]; + _outsideSnapshotView = nil; + [_insideSnapshotView removeFromSuperview]; + _insideSnapshotView = nil; + + if (completion != nil) + completion(); + }]; +} + +- (void)updateDestinationIndexPath:(NSIndexPath *)indexPath +{ + if (indexPath == nil || [_lastIndexPath isEqual:indexPath]) + return; + + _lastIndexPath = indexPath; + + id dataSource = (id)self.dataSource; + if ([dataSource respondsToSelector:@selector(collectionView:canMoveItemAtIndexPath:toIndexPath:)] && + ![dataSource collectionView:self canMoveItemAtIndexPath:self.draggableLayout.sourceIndexPath toIndexPath:indexPath]) + { + return; + } + + [self performBatchUpdates:^ + { + self.draggableLayout.hiddenIndexPath = indexPath; + self.draggableLayout.destinationIndexPath = indexPath; + } completion:nil]; +} + +#pragma mark - Gesture Handling + +- (void)handleDragPress:(UILongPressGestureRecognizer *)gestureRecognizer +{ + switch (gestureRecognizer.state) + { + case UIGestureRecognizerStateBegan: + { + CGPoint location = [gestureRecognizer locationInView:self]; + NSIndexPath *indexPath = [self indexPathForItemAtPoint:location]; + if (indexPath == nil) + return; + + id dataSource = (id)self.dataSource; + if ([dataSource respondsToSelector:@selector(collectionView:canMoveItemAtIndexPath:)] && + ![dataSource collectionView:self canMoveItemAtIndexPath:indexPath]) + { + return; + } + + [_draggedView removeFromSuperview]; + _draggedView = [self _prepareDraggedViewForCell:[self cellForItemAtIndexPath:indexPath]]; + [[self _draggedViewSuperview] addSubview:_draggedView]; + + [self _performDraggedViewHighlightTransitionWithDuration:0.25f completion:nil]; + + _lastIndexPath = indexPath; + self.draggableLayout.sourceIndexPath = indexPath; + self.draggableLayout.destinationIndexPath = indexPath; + self.draggableLayout.hiddenIndexPath = indexPath; + [self.draggableLayout invalidateLayout]; + } + break; + + case UIGestureRecognizerStateEnded: + case UIGestureRecognizerStateCancelled: + { + if (self.draggableLayout.sourceIndexPath == nil) + return; + + NSIndexPath *sourceIndexPath = self.draggableLayout.sourceIndexPath; + NSIndexPath *destinationIndexPath = self.draggableLayout.destinationIndexPath; + + id dataSource = (id)self.dataSource; + [dataSource collectionView:self itemAtIndexPath:sourceIndexPath willMoveToIndexPath:destinationIndexPath]; + + [self performBatchUpdates:^ + { + [self moveItemAtIndexPath:sourceIndexPath toIndexPath:destinationIndexPath]; + self.draggableLayout.sourceIndexPath = nil; + self.draggableLayout.destinationIndexPath = nil; + } completion:nil]; + + _pressGestureRecognizer.enabled = false; + + [self _performDraggedViewDropToIndexPath:destinationIndexPath completion:^ + { + _pressGestureRecognizer.enabled = true; + + if ([dataSource respondsToSelector:@selector(collectionView:itemAtIndexPath:didMoveToIndexPath:)]) + [dataSource collectionView:self itemAtIndexPath:sourceIndexPath didMoveToIndexPath:destinationIndexPath]; + + self.draggableLayout.hiddenIndexPath = nil; + [self.draggableLayout invalidateLayout]; + }]; + + _lastIndexPath = nil; + [self invalidateScrolling]; + } + break; + + default: + break; + } +} + +- (void)handleDragPan:(UIPanGestureRecognizer *)gestureRecognizer +{ + if (self.draggableLayout.sourceIndexPath == nil) + return; + + switch (gestureRecognizer.state) + { + case UIGestureRecognizerStateBegan: + case UIGestureRecognizerStateChanged: + { + CGPoint translation = [gestureRecognizer translationInView:self]; + [gestureRecognizer setTranslation:CGPointZero inView:self]; + + _draggedView.center = CGPointMake(_draggedView.center.x + translation.x, + _draggedView.center.y + translation.y); + + CGPoint center = [self convertPoint:_draggedView.center fromView:[self _draggedViewSuperview]]; + _insideSnapshotView.center = center; + + UICollectionViewFlowLayout *layout = (UICollectionViewFlowLayout *)self.draggableLayout; + switch (layout.scrollDirection) + { + case UICollectionViewScrollDirectionHorizontal: + { + if (center.x < (CGRectGetMinX(self.bounds) + self.scrollingTriggerEdgeInsets.left)) + { + [self setupScrollForDraggingInDirection:TGDraggableCollectionScrollingDirectionLeft]; + } + else + { + if (center.x > (CGRectGetMaxX(self.bounds) - self.scrollingTriggerEdgeInsets.right)) + [self setupScrollForDraggingInDirection:TGDraggableCollectionScrollingDirectionRight]; + else + [self invalidateScrolling]; + } + } + break; + + case UICollectionViewScrollDirectionVertical: + { + if (center.y < (CGRectGetMinY(self.bounds) + self.scrollingTriggerEdgeInsets.top)) + { + [self setupScrollForDraggingInDirection:TGDraggableCollectionScrollingDirectionUp]; + } + else + { + if (center.y > (CGRectGetMaxY(self.bounds) - self.scrollingTriggerEdgeInsets.bottom)) + [self setupScrollForDraggingInDirection:TGDraggableCollectionScrollingDirectionDown]; + else + [self invalidateScrolling]; + } + } + break; + + default: + break; + } + } + break; + + case UIGestureRecognizerStateEnded: + case UIGestureRecognizerStateCancelled: + { + [self invalidateScrolling]; + } + break; + + default: + break; + } + + if (_scrollingDirection != TGDraggableCollectionScrollingDirectionUnknown) + return; + + CGPoint location = [gestureRecognizer locationInView:self]; + NSIndexPath *indexPath = [self indexPathForItemNearPoint:location]; + [self updateDestinationIndexPath:indexPath]; +} + +- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer +{ + if (self.draggable && gestureRecognizer == _panGestureRecognizer) + return (self.draggableLayout.sourceIndexPath != nil); + + return true; +} + +- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer +{ + if (gestureRecognizer == _pressGestureRecognizer && otherGestureRecognizer == _panGestureRecognizer) + return true; + else if (gestureRecognizer == _panGestureRecognizer && otherGestureRecognizer == _pressGestureRecognizer) + return true; + + return false; +} + +#pragma mark - Scrolling + +- (void)setupScrollForDraggingInDirection:(TGDraggableCollectionScrollingDirection)direction +{ + if (_scrollingDirection == direction) + return; + + [self invalidateScrolling]; + + _scrollingDirection = direction; + + _scrollingDisplayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(handleDragScroll:)]; + [_scrollingDisplayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; +} + +- (void)handleDragScroll:(CADisplayLink *)displayLink +{ + if (_scrollingDirection == TGDraggableCollectionScrollingDirectionUnknown) + return; + + CGSize frameSize = self.bounds.size; + CGSize contentSize = self.contentSize; + CGPoint contentOffset = self.contentOffset; + UIEdgeInsets contentInset = self.contentInset; + + CGFloat distance = (CGFloat)rint(self.scrollingSpeed * displayLink.duration); + CGPoint translation = CGPointZero; + + switch (_scrollingDirection) + { + case TGDraggableCollectionScrollingDirectionUp: + { + distance = -distance; + + if (contentOffset.y + distance <= -contentInset.top) + distance = -contentOffset.y - contentInset.top; + + translation = CGPointMake(0, distance); + } + break; + + case TGDraggableCollectionScrollingDirectionDown: + { + CGFloat maxY = contentSize.height - frameSize.height + contentInset.bottom; + + if ((contentOffset.y + distance) >= maxY) + distance = maxY - contentOffset.y; + + translation = CGPointMake(0, distance); + } + break; + + case TGDraggableCollectionScrollingDirectionLeft: + { + distance = -distance; + + if (contentOffset.x + distance <= -contentInset.left) + distance = -contentOffset.x - contentInset.left; + + translation = CGPointMake(distance, 0); + } + break; + + case TGDraggableCollectionScrollingDirectionRight: + { + CGFloat maxX = contentSize.width - frameSize.width + contentInset.right; + + if ((contentOffset.x + distance) >= maxX) + distance = maxX - contentOffset.x; + + translation = CGPointMake(distance, 0); + } + break; + + default: + break; + } + + self.contentOffset = CGPointMake(contentOffset.x + translation.x, + contentOffset.y + translation.y); + + NSIndexPath *indexPath = [self indexPathForItemNearPoint:_draggedView.center]; + [self updateDestinationIndexPath:indexPath]; +} + +- (void)invalidateScrolling +{ + [_scrollingDisplayLink invalidate]; + _scrollingDisplayLink = nil; + + _scrollingDirection = TGDraggableCollectionScrollingDirectionUnknown; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGDraggableCollectionViewFlowLayout.h b/submodules/LegacyComponents/LegacyComponents/TGDraggableCollectionViewFlowLayout.h new file mode 100644 index 0000000000..d380875c8a --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGDraggableCollectionViewFlowLayout.h @@ -0,0 +1,10 @@ +#import +#import + +@interface TGDraggableCollectionViewFlowLayout : UICollectionViewFlowLayout + +@property (nonatomic, strong) NSIndexPath *sourceIndexPath; +@property (nonatomic, strong) NSIndexPath *destinationIndexPath; +@property (nonatomic, strong) NSIndexPath *hiddenIndexPath; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGDraggableCollectionViewFlowLayout.m b/submodules/LegacyComponents/LegacyComponents/TGDraggableCollectionViewFlowLayout.m new file mode 100644 index 0000000000..ace72261a9 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGDraggableCollectionViewFlowLayout.m @@ -0,0 +1,74 @@ +#import "TGDraggableCollectionViewFlowLayout.h" + +@implementation TGDraggableCollectionViewFlowLayout + +- (UICollectionViewLayoutAttributes *)initialLayoutAttributesForAppearingItemAtIndexPath:(NSIndexPath *)itemIndexPath +{ + if (itemIndexPath == nil) + return nil; + + UICollectionViewLayoutAttributes *attributes = [super initialLayoutAttributesForAppearingItemAtIndexPath:itemIndexPath]; + attributes.transform3D = CATransform3DMakeTranslation(0, 0, itemIndexPath.row + 1); + attributes.zIndex = itemIndexPath.row + 1; + + return attributes; +} + +- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)itemIndexPath +{ + if (itemIndexPath == nil) + return nil; + + UICollectionViewLayoutAttributes *attributes = [super layoutAttributesForItemAtIndexPath:itemIndexPath]; + attributes.transform3D = CATransform3DMakeTranslation(0, 0, 1000 + itemIndexPath.row + 1); + attributes.zIndex = 1000 + itemIndexPath.row + 1; + + return attributes; +} + +- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect +{ + NSArray *originalAttributes = [super layoutAttributesForElementsInRect:rect]; + + if (self.destinationIndexPath == nil) + { + if (self.hiddenIndexPath == nil) + return originalAttributes; + + for (UICollectionViewLayoutAttributes *layoutAttributes in originalAttributes) + { + if (layoutAttributes.representedElementCategory != UICollectionElementCategoryCell) + continue; + + if ([layoutAttributes.indexPath isEqual:self.hiddenIndexPath]) + layoutAttributes.hidden = true; + } + return originalAttributes; + } + + for (UICollectionViewLayoutAttributes *layoutAttributes in originalAttributes) + { + if (layoutAttributes.representedElementCategory != UICollectionElementCategoryCell) + continue; + + NSIndexPath *indexPath = layoutAttributes.indexPath; + if ([indexPath isEqual:self.hiddenIndexPath]) + layoutAttributes.hidden = true; + + if ([indexPath isEqual:self.destinationIndexPath]) + { + layoutAttributes.indexPath = self.sourceIndexPath; + } + else + { + if (indexPath.item <= self.sourceIndexPath.item && indexPath.item > self.destinationIndexPath.item) + layoutAttributes.indexPath = [NSIndexPath indexPathForItem:indexPath.item - 1 inSection:indexPath.section]; + else if (indexPath.item >= self.sourceIndexPath.item && indexPath.item < self.destinationIndexPath.item) + layoutAttributes.indexPath = [NSIndexPath indexPathForItem:indexPath.item + 1 inSection:indexPath.section]; + } + } + + return originalAttributes; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGEmbedCoubPlayerView.h b/submodules/LegacyComponents/LegacyComponents/TGEmbedCoubPlayerView.h new file mode 100644 index 0000000000..0e4c94ee48 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGEmbedCoubPlayerView.h @@ -0,0 +1,12 @@ +#import "TGEmbedPlayerView.h" + +@interface TGEmbedCoubPlayerView : TGEmbedPlayerView + ++ (NSString *)_coubVideoIdFromText:(NSString *)text; + ++ (NSDictionary *)coubJSONByPermalink:(NSString *)permalink; ++ (void)setCoubJSON:(NSDictionary *)json forPermalink:(NSString *)permalink; + +- (void)setVideoPath:(NSString *)videoPath; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGEmbedCoubPlayerView.m b/submodules/LegacyComponents/LegacyComponents/TGEmbedCoubPlayerView.m new file mode 100644 index 0000000000..9007a46539 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGEmbedCoubPlayerView.m @@ -0,0 +1,561 @@ +#import "TGEmbedCoubPlayerView.h" +#import "TGEmbedPlayerState.h" + +#import "LegacyComponentsInternal.h" + +#import + +#import + +#import "CBPlayerView.h" +#import "CBCoubAsset.h" +#import "CBCoubPlayer.h" +#import "CBCoubNew.h" + +#import + +@interface TGEmbedCoubURLTaskAdapter : NSObject +{ + NSURLSession *_session; +} + +@property (nonatomic, copy) void (^redirectUrl)(NSString *); + +- (instancetype)initWithURL:(NSString *)url; +- (void)invalidate; + +@end + +@interface TGEmbedCoubPlayerView () +{ + NSString *_permalink; + + bool _started; + UIImage *_coverImage; + + CBPlayerView *_playerView; + CBCoubPlayer *_coubPlayer; + SVariable *_videoPath; + + SDisposableSet *_disposables; + + id _asset; +} +@end + +@implementation TGEmbedCoubPlayerView + +- (instancetype)initWithWebPageAttachment:(TGWebPageMediaAttachment *)webPage thumbnailSignal:(SSignal *)thumbnailSignal alternateCachePathSignal:(SSignal *)alternateCachePathSignal +{ + self = [super initWithWebPageAttachment:webPage thumbnailSignal:thumbnailSignal alternateCachePathSignal:alternateCachePathSignal]; + if (self != nil) + { + _permalink = [TGEmbedCoubPlayerView _coubVideoIdFromText:webPage.embedUrl]; + _disposables = [[SDisposableSet alloc] init]; + + TGDocumentMediaAttachment *document = webPage.document; + NSString *videoPath = nil; + if ([document.mimeType isEqualToString:@"video/mp4"]) + { + if (document.localDocumentId != 0) { + videoPath = [[[LegacyComponentsGlobals provider] localDocumentDirectoryForLocalDocumentId:document.localDocumentId version:document.version] stringByAppendingPathComponent:[document safeFileName]]; + } else { + videoPath = [[[LegacyComponentsGlobals provider] localDocumentDirectoryForDocumentId:document.documentId version:document.version] stringByAppendingPathComponent:[document safeFileName]]; + } + } + + __weak TGEmbedCoubPlayerView *weakSelf = self; + if (videoPath != nil && [[NSFileManager defaultManager] fileExistsAtPath:videoPath isDirectory:NULL]) + { + _videoPath = [[SVariable alloc] init]; + [_videoPath set:[SSignal single:[NSURL fileURLWithPath:videoPath]]]; + + if (thumbnailSignal == nil) + { + [_disposables add:[[[TGMediaAssetImageSignals videoThumbnailForAVAsset:[AVURLAsset assetWithURL:[NSURL fileURLWithPath:videoPath]] size:CGSizeMake(480, 480) timestamp:CMTimeMake(1, 100)] deliverOn:[SQueue mainQueue]] startWithNext:^(id next) + { + __strong TGEmbedCoubPlayerView *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + if ([next isKindOfClass:[UIImage class]]) + [strongSelf setCoverImage:next]; + }]]; + } + } + else if (alternateCachePathSignal != nil) + { + _videoPath = [[SVariable alloc] init]; + + [_disposables add:[alternateCachePathSignal startWithNext:^(NSString *path) + { + __strong TGEmbedCoubPlayerView *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + if ([[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:NULL]) + { + if (path.pathExtension.length == 0) + { + [[NSFileManager defaultManager] createSymbolicLinkAtPath:[path stringByAppendingString:@".mov"] withDestinationPath:[path lastPathComponent] error:nil]; + path = [path stringByAppendingString:@".mov"]; + } + + NSURL *url = [NSURL fileURLWithPath:path]; + [strongSelf->_videoPath set:[SSignal single:url]]; + + if (thumbnailSignal == nil) + { + [strongSelf->_disposables add:[[[TGMediaAssetImageSignals videoThumbnailForAVAsset:[AVURLAsset assetWithURL:url] size:CGSizeMake(480, 480) timestamp:CMTimeMake(1, 100)] deliverOn:[SQueue mainQueue]] startWithNext:^(id next) + { + __strong TGEmbedCoubPlayerView *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + if ([next isKindOfClass:[UIImage class]]) + [strongSelf setCoverImage:next]; + }]]; + } + } + else + { + [strongSelf->_videoPath set:[SSignal single:nil]]; + } + }]]; + } + + self.controlsView.watermarkImage = TGComponentsImageNamed(@"CoubWatermark"); + self.controlsView.watermarkOffset = CGPointMake(12.0f, 12.0f); + } + return self; +} + +- (void)dealloc +{ + [_disposables dispose]; +} + +- (bool)supportsPIP +{ + return false; +} + +- (void)_watermarkAction +{ + [super _watermarkAction]; + + if (self.onWatermarkAction != nil) + self.onWatermarkAction(); + + NSString *permalink = _permalink; + NSString *coubId = nil; + if ([_asset isKindOfClass:[CBCoubNew class]]) + coubId = ((CBCoubNew *)_asset).coubID; + + NSURL *appUrl = [[NSURL alloc] initWithString:[[NSString alloc] initWithFormat:@"coub://view/%@", coubId]]; + + if ([[LegacyComponentsGlobals provider] canOpenURL:appUrl]) + { + [[LegacyComponentsGlobals provider] openURL:appUrl]; + return; + } + + NSURL *webUrl = [NSURL URLWithString:[NSString stringWithFormat:@"https://coub.com/view/%@", permalink]]; + [[LegacyComponentsGlobals provider] openURL:webUrl]; +} + +- (void)setupWithEmbedSize:(CGSize)embedSize +{ + [super setupWithEmbedSize:embedSize]; + + _playerView = [[CBPlayerView alloc] initWithFrame:[self _webView].bounds]; + [[self _webView].superview insertSubview:_playerView aboveSubview:[self _webView]]; + + _coubPlayer = [[CBCoubPlayer alloc] initWithVideoLayer:(AVPlayerLayer *)_playerView.videoPlayerView.layer]; + _coubPlayer.withoutAudio = false; + _coubPlayer.delegate = self; + + [self _cleanWebView]; + + [self setDimmed:true animated:false shouldDelay:false]; + [self initializePlayer]; + + [self setLoadProgress:0.01f duration:0.01]; +} + +- (void)playVideo +{ + [_coubPlayer resume]; +} + +- (void)pauseVideo:(bool)manually +{ + [super pauseVideo:manually]; + [_coubPlayer pause]; +} + + +- (void)_onPageReady +{ + +} + +- (void)_didBeginPlayback +{ + [super _didBeginPlayback]; + + [self setDimmed:false animated:true shouldDelay:false]; +} + +- (TGEmbedPlayerControlsType)_controlsType +{ + return TGEmbedPlayerControlsTypeSimple; +} + +- (NSString *)_embedHTML +{ + NSError *error = nil; + NSString *path = TGComponentsPathForResource(@"DefaultPlayer", @"html"); + + NSString *embedHTMLTemplate = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:&error]; + if (error != nil) + { + TGLegacyLog(@"[CoubEmbedPlayer]: Received error rendering template: %@", error); + return nil; + } + + NSString *embedHTML = [NSString stringWithFormat:embedHTMLTemplate, @"about:blank"]; + return embedHTML; +} + +- (NSURL *)_baseURL +{ + return [NSURL URLWithString:@"https://coub.com/"]; +} + +#pragma mark - + +- (bool)_useFakeLoadingProgress +{ + return false; +} + ++ (SSignal *)webHeadersRequestSignal:(NSString *)url +{ + return [[SSignal alloc] initWithGenerator:^id(SSubscriber *subscriber) + { + TGEmbedCoubURLTaskAdapter *adapter = [[TGEmbedCoubURLTaskAdapter alloc] initWithURL:url]; + adapter.redirectUrl = ^(NSString *redirectUrl) + { + [subscriber putNext:redirectUrl]; + [subscriber putCompletion]; + }; + return [[SBlockDisposable alloc] initWithBlock:^ + { + [adapter invalidate]; + }]; + }]; +} + +- (void)initializePlayer +{ + NSString *url = [NSString stringWithFormat:@"http://coub.com/api/v2/coubs/%@", _permalink]; + + __weak TGEmbedCoubPlayerView *weakSelf = self; + SSignal *cachedSignal = [[SSignal alloc] initWithGenerator:^id(SSubscriber *subscriber) + { + __strong TGEmbedCoubPlayerView *strongSelf = weakSelf; + if (strongSelf == nil) + { + [subscriber putCompletion]; + return nil; + } + + NSDictionary *json = [TGEmbedCoubPlayerView coubJSONByPermalink:strongSelf->_permalink]; + if (json != nil) + { + [subscriber putNext:json]; + [subscriber putCompletion]; + } + else + { + [subscriber putError:nil]; + } + + return nil; + }]; + + SSignal *dataSignal = [[cachedSignal mapToSignal:^SSignal *(NSDictionary *json) + { + return [[SSignal single:@{ @"json": json, @"cached": @true }] delay:0.2 onQueue:[SQueue mainQueue]]; + }] catch:^SSignal *(__unused id error) + { + return [[[LegacyComponentsGlobals provider] jsonForHttpLocation:url] map:^id(NSDictionary *json) + { + return @{ @"json": json, @"cached": @false }; + }]; + }]; + + SSignal *locationSignal = [dataSignal mapToSignal:^SSignal *(NSDictionary *data) + { + NSDictionary *attributes = data[@"json"]; + NSString *remoteVideoLocation = nil; + NSDictionary *fileVersions = attributes[@"file_versions"]; + + if (fileVersions != nil) + remoteVideoLocation = fileVersions[@"iphone"][@"url"]; + if (!remoteVideoLocation || [remoteVideoLocation isKindOfClass:[NSNull class]]) + remoteVideoLocation = attributes[@"file"]; + if (!remoteVideoLocation || [remoteVideoLocation isKindOfClass:[NSNull class]]) + remoteVideoLocation = fileVersions[@"html5"][@"video"][@"med"][@"url"]; + + if ([remoteVideoLocation rangeOfString:@"getvideo?"].location != NSNotFound) + { + NSString *location = [remoteVideoLocation stringByReplacingOccurrencesOfString:@"//coub" withString:@"https://coub"]; + return [[TGEmbedCoubPlayerView webHeadersRequestSignal:location] map:^id(id result) { + NSMutableDictionary *updatedJson = [attributes mutableCopy]; + updatedJson[@"explicitVideoLocation"] = result; + return updatedJson; + }]; + } + else + { + return [SSignal single:attributes]; + } + }]; + + [_disposables add:[[locationSignal deliverOn:[SQueue mainQueue]] startWithNext:^(NSDictionary *data) + { + __strong TGEmbedCoubPlayerView *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + SSignal *signal = [SSignal single:nil]; + if (strongSelf->_videoPath != nil) + signal = strongSelf->_videoPath.signal; + + [strongSelf->_disposables add:[signal startWithNext:^(NSURL *videoPath) + { + __strong TGEmbedCoubPlayerView *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + CBCoubNew *coub = [CBCoubNew coubWithAttributes:data]; + coub.customLocalVideoFileURL = videoPath; + strongSelf->_asset = coub; + + if ([coub isKindOfClass:[CBCoubNew class]]) + { + CBCoubNew *coubNew = (CBCoubNew *)coub; + if (strongSelf.onMetadataLoaded != nil) + strongSelf.onMetadataLoaded(coubNew.title, coubNew.author.name); + } + + [strongSelf->_coubPlayer playAsset:strongSelf->_asset]; + + if (![data[@"cached"] boolValue]) + [TGEmbedCoubPlayerView setCoubJSON:data[@"json"] forPermalink:strongSelf->_permalink]; + }]]; + }]]; +} + +- (void)setVideoPath:(NSString *)videoPath { + [_videoPath set:[SSignal single:[NSURL fileURLWithPath:videoPath]]]; +} + +- (void)playerReadyToPlay:(CBCoubPlayer *)__unused player +{ + [self setLoadProgress:1.0f duration:0.2]; + [_playerView play]; + + if (self.onRealLoadProgress != nil) + self.onRealLoadProgress(1.0f, 0.2); +} + +- (void)playerDidStartPlaying:(CBCoubPlayer *)__unused player +{ + if (!_started) + { + _started = true; + [self _didBeginPlayback]; + + TGEmbedPlayerState *state = [TGEmbedPlayerState stateWithPlaying:true]; + [self updateState:state]; + } +} + +- (void)player:(CBCoubPlayer *)__unused player didReachProgressWhileDownloading:(float)progress +{ + [self setLoadProgress:progress duration:0.3]; + + if (self.onRealLoadProgress != nil) + self.onRealLoadProgress(progress, 0.3); +} + +- (void)playerDidPause:(CBCoubPlayer *)__unused player withUserAction:(BOOL)isUserAction +{ + if (!isUserAction) + return; + + TGEmbedPlayerState *state = [TGEmbedPlayerState stateWithPlaying:false]; + [self updateState:state]; +} + +- (void)playerDidResume:(CBCoubPlayer *)__unused player +{ + TGEmbedPlayerState *state = [TGEmbedPlayerState stateWithPlaying:true]; + [self updateState:state]; +} + +- (void)playerDidFail:(CBCoubPlayer *)__unused player error:(NSError *)error +{ + TGLegacyLog(@"[CoubPlayer] ERROR: %@", error.localizedDescription); +} + +- (void)playerDidStop:(CBCoubPlayer *)__unused player +{ + +} + +#pragma mark - + ++ (NSString *)_coubVideoIdFromText:(NSString *)text +{ + NSMutableArray *prefixes = [NSMutableArray arrayWithArray:@ + [ + @"http://coub.com/v/", + @"https://coub.com/v/", + @"http://coub.com/embed/", + @"https://coub.com/embed/", + @"http://coub.com/view/", + @"https://coub.com/view/" + ]]; + + NSString *prefix = nil; + for (NSString *p in prefixes) + { + if ([text hasPrefix:p]) + { + prefix = p; + break; + } + } + + if (prefix != nil) + { + NSString *suffix = [text substringFromIndex:prefix.length]; + + for (int i = 0; i < (int)suffix.length; i++) + { + unichar c = [suffix characterAtIndex:i]; + if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-' || c == '_' || c == '=' || c == '&' || c == '#')) + return nil; + } + + return suffix; + } + + return nil; +} + ++ (bool)_supportsWebPage:(TGWebPageMediaAttachment *)webPage +{ + NSString *url = webPage.embedUrl; + return ([url hasPrefix:@"http://coub.com/embed/"] || [url hasPrefix:@"https://coub.com/embed/"]); +} + ++ (PSLMDBKeyValueStore *)coubMetaStore +{ + static PSLMDBKeyValueStore *store = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^ + { + NSString *documentsPath = [[LegacyComponentsGlobals provider] dataStoragePath]; + store = [PSLMDBKeyValueStore storeWithPath:[documentsPath stringByAppendingPathComponent:@"misc/coubmetadata"] size:1 * 1024 * 1024]; + }); + return store; +} + ++ (NSDictionary *)coubJSONByPermalink:(NSString *)permalink +{ + if (permalink.length == 0) + return nil; + + __block NSData *jsonData = nil; + [[self coubMetaStore] readInTransaction:^(id reader) + { + NSMutableData *keyData = [[NSMutableData alloc] init]; + int8_t keyspace = 0; + [keyData appendBytes:&keyspace length:1]; + [keyData appendData:[permalink dataUsingEncoding:NSUTF8StringEncoding]]; + PSData key = {.data = (uint8_t *)keyData.bytes, .length = keyData.length}; + PSData value; + if ([reader readValueForRawKey:&key value:&value]) + jsonData = [[NSData alloc] initWithBytes:value.data length:value.length]; + }]; + + if (jsonData.length > 0) + { + @try + { + NSDictionary *json = [NSKeyedUnarchiver unarchiveObjectWithData:jsonData]; + return json; + } + @catch(NSException *) + { + } + } + + return nil; +} + ++ (void)setCoubJSON:(NSDictionary *)json forPermalink:(NSString *)permalink +{ + if (permalink.length == 0 || json.allKeys.count == 0) + return; + + NSData *data = [NSKeyedArchiver archivedDataWithRootObject:json]; + if (data.length == 0) + return; + + [[self coubMetaStore] readWriteInTransaction:^(id writer) + { + NSMutableData *keyData = [[NSMutableData alloc] init]; + int8_t keyspace = 0; + [keyData appendBytes:&keyspace length:1]; + [keyData appendData:[permalink dataUsingEncoding:NSUTF8StringEncoding]]; + PSData key = {.data = (uint8_t *)keyData.bytes, .length = keyData.length}; + PSData value = {.data = (uint8_t *)data.bytes, .length = data.length}; + [writer writeValueForRawKey:key.data keyLength:key.length value:value.data valueLength:value.length]; + }]; +} + +@end + + +@implementation TGEmbedCoubURLTaskAdapter + +- (instancetype)initWithURL:(NSString *)url +{ + self = [super init]; + if (self != nil) + { + NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration]; + _session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil]; + [[_session dataTaskWithURL:[NSURL URLWithString:url] completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {}] resume]; + } + return self; +} + +- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task willPerformHTTPRedirection:(NSHTTPURLResponse *)response newRequest:(NSURLRequest *)request completionHandler:(void (^)(NSURLRequest * _Nullable))completionHandler +{ + if (self.redirectUrl != nil) + self.redirectUrl(response.allHeaderFields[@"Location"]); +} + +- (void)invalidate +{ + [_session invalidateAndCancel]; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGEmbedInstagramPlayerView.h b/submodules/LegacyComponents/LegacyComponents/TGEmbedInstagramPlayerView.h new file mode 100644 index 0000000000..e103032699 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGEmbedInstagramPlayerView.h @@ -0,0 +1,5 @@ +#import "TGEmbedPlayerView.h" + +@interface TGEmbedInstagramPlayerView : TGEmbedPlayerView + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGEmbedInstagramPlayerView.m b/submodules/LegacyComponents/LegacyComponents/TGEmbedInstagramPlayerView.m new file mode 100644 index 0000000000..f416302265 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGEmbedInstagramPlayerView.m @@ -0,0 +1,113 @@ +#import "TGEmbedInstagramPlayerView.h" +#import "TGEmbedPlayerState.h" + +#import "LegacyComponentsInternal.h" + +NSString *const TGInstagramPlayerCallbackOnPlayback = @"onPlayback"; + +@interface TGEmbedInstagramPlayerView () +{ + NSString *_url; + bool _playing; + bool _started; +} +@end + +@implementation TGEmbedInstagramPlayerView + +- (instancetype)initWithWebPageAttachment:(TGWebPageMediaAttachment *)webPage thumbnailSignal:(SSignal *)thumbnailSignal alternateCachePathSignal:(SSignal *)alternateCachePathSignal +{ + self = [super initWithWebPageAttachment:webPage thumbnailSignal:thumbnailSignal alternateCachePathSignal:alternateCachePathSignal]; + if (self != nil) + { + _url = webPage.embedUrl; + } + return self; +} + +- (void)playVideo +{ + _playing = true; + [self _evaluateJS:@"play()" completion:nil]; + + TGEmbedPlayerState *state = [TGEmbedPlayerState stateWithPlaying:_playing]; + [self updateState:state]; +} + +- (void)pauseVideo:(bool)manually +{ + [super pauseVideo:manually]; + + _playing = false; + [self _evaluateJS:@"pause();" completion:nil]; + + TGEmbedPlayerState *state = [TGEmbedPlayerState stateWithPlaying:_playing]; + [self updateState:state]; +} + +- (TGEmbedPlayerControlsType)_controlsType +{ + return TGEmbedPlayerControlsTypeSimple; +} + +- (void)_onPageReady +{ + +} + +- (void)_didBeginPlayback +{ + [super _didBeginPlayback]; + [self setDimmed:false animated:true shouldDelay:false]; +} + +- (void)_notifyOfCallbackURL:(NSURL *)url +{ + NSString *action = url.host; + + NSString *query = url.query; + NSString *data; + if (query != nil) + data = [query componentsSeparatedByString:@"="][1]; + + if ([action isEqual:TGInstagramPlayerCallbackOnPlayback]) + { + if (!_started) + { + _started = true; + [self _didBeginPlayback]; + } + _playing = true; + TGEmbedPlayerState *state = [TGEmbedPlayerState stateWithPlaying:true]; + [self updateState:state]; + } +} + +- (NSString *)_embedHTML +{ + NSError *error = nil; + NSString *path = TGComponentsPathForResource(@"InstagramPlayer", @"html"); + + NSString *embedHTMLTemplate = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:&error]; + if (error != nil) + { + TGLegacyLog(@"[InstagramEmbedPlayer]: Received error rendering template: %@", error); + return nil; + } + + NSString *embedHTML = [NSString stringWithFormat:embedHTMLTemplate, _url]; + return embedHTML; +} + +- (NSURL *)_baseURL +{ + return [NSURL URLWithString:@"https://instagram.com/"]; +} + ++ (bool)_supportsWebPage:(TGWebPageMediaAttachment *)webPage +{ + NSURL *url = [NSURL URLWithString:webPage.embedUrl]; + return ([url.host containsString:@"cdninstagram"] && [url.pathExtension isEqualToString:@"mp4"]); +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGEmbedPIPButton.h b/submodules/LegacyComponents/LegacyComponents/TGEmbedPIPButton.h new file mode 100644 index 0000000000..d0bcb94075 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGEmbedPIPButton.h @@ -0,0 +1,9 @@ +#import + +@interface TGEmbedPIPButton : UIButton + +- (void)setIconImage:(UIImage *)iconImage; + +@end + +extern const CGSize TGEmbedPIPButtonSize; diff --git a/submodules/LegacyComponents/LegacyComponents/TGEmbedPIPButton.m b/submodules/LegacyComponents/LegacyComponents/TGEmbedPIPButton.m new file mode 100644 index 0000000000..482564985d --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGEmbedPIPButton.m @@ -0,0 +1,93 @@ +#import "TGEmbedPIPButton.h" + +const CGSize TGEmbedPIPButtonSize = { 42.0f, 42.0f }; + +@interface TGEmbedPIPButton () +{ + UIVisualEffectView *_backView; + UIView *_highlightView; + UIImageView *_iconView; + + bool _animateHighlight; +} +@end + +@implementation TGEmbedPIPButton + +- (instancetype)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + if (self != nil) + { + self.clipsToBounds = true; + self.exclusiveTouch = true; + + UIVisualEffect *blurEffect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleLight]; + _backView = [[UIVisualEffectView alloc] initWithEffect:blurEffect]; + _backView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + _backView.frame = self.bounds; + _backView.userInteractionEnabled = false; + [self addSubview:_backView]; + + _highlightView = [[UIView alloc] initWithFrame:self.bounds]; + _highlightView.alpha = 0.0f; + _highlightView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + _highlightView.backgroundColor = [UIColor whiteColor]; + _highlightView.userInteractionEnabled = false; + [self addSubview:_highlightView]; + + _iconView = [[UIImageView alloc] initWithFrame:self.bounds]; + _iconView.contentMode = UIViewContentModeCenter; + [self addSubview:_iconView]; + } + return self; +} + +- (void)setFrame:(CGRect)frame +{ + [super setFrame:frame]; + self.layer.cornerRadius = frame.size.width / 2.0f; +} + +- (void)setIconImage:(UIImage *)iconImage +{ + _iconView.image = iconImage; +} + +- (void)setHighlighted:(BOOL)highlighted +{ + [super setHighlighted:highlighted]; + + void (^changeBlock)(void) = ^ + { + _highlightView.alpha = highlighted ? 1.0f : 0.0f; + }; + + if (_animateHighlight) + [UIView animateWithDuration:0.2 animations:changeBlock]; + else + changeBlock(); +} + +- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event +{ + _animateHighlight = true; + [super touchesMoved:touches withEvent:event]; + _animateHighlight = false; +} + +- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event +{ + _animateHighlight = true; + [super touchesEnded:touches withEvent:event]; + _animateHighlight = false; +} + +- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event +{ + _animateHighlight = true; + [super touchesCancelled:touches withEvent:event]; + _animateHighlight = false; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGEmbedPIPPullArrowView.h b/submodules/LegacyComponents/LegacyComponents/TGEmbedPIPPullArrowView.h new file mode 100644 index 0000000000..b0adb16366 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGEmbedPIPPullArrowView.h @@ -0,0 +1,7 @@ +#import + +@interface TGEmbedPIPPullArrowView : UIView + +- (void)setAngled:(bool)angled animated:(bool)animated; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGEmbedPIPPullArrowView.m b/submodules/LegacyComponents/LegacyComponents/TGEmbedPIPPullArrowView.m new file mode 100644 index 0000000000..fbd13c3c03 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGEmbedPIPPullArrowView.m @@ -0,0 +1,68 @@ +#import "TGEmbedPIPPullArrowView.h" + +#import + +@interface TGEmbedPIPPullArrowView () +{ + UIImageView *_topPart; + UIImageView *_bottomPart; +} +@end + +@implementation TGEmbedPIPPullArrowView + +- (instancetype)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + if (self != nil) + { + self.layer.rasterizationScale = TGScreenScaling(); + self.layer.shouldRasterize = true; + if ([self.layer respondsToSelector:@selector(setAllowsEdgeAntialiasing:)]) + self.layer.allowsEdgeAntialiasing = true; + + static dispatch_once_t onceToken; + static UIImage *image; + dispatch_once(&onceToken, ^ + { + UIGraphicsBeginImageContextWithOptions(CGSizeMake(8, 23), false, 0.0f); + CGContextRef context = UIGraphicsGetCurrentContext(); + + CGContextSetFillColorWithColor(context, [UIColor blackColor].CGColor); + [[UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, 8, 23) cornerRadius:4.5f] fill]; + + image = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + }); + + _topPart = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 8, 38)]; + _topPart.contentMode = UIViewContentModeBottom; + _topPart.image = image; + [self addSubview:_topPart]; + + _bottomPart = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 8, 38)]; + _bottomPart.contentMode = UIViewContentModeTop; + _bottomPart.image = image; + _bottomPart.transform = CGAffineTransformMakeScale(1, -1); + [self addSubview:_bottomPart]; + } + return self; +} + +- (void)setAngled:(bool)angled animated:(bool)animated +{ + void (^changeBlock)(void) = ^ + { + CGFloat angle = angled ? 0.20944f : 0.0f; + + _topPart.transform = CGAffineTransformMakeRotation(angle); + _bottomPart.transform = CGAffineTransformMakeRotation(-angle); + }; + + if (animated) + [UIView animateWithDuration:0.25 animations:changeBlock]; + else + changeBlock(); +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGEmbedPIPScrubber.h b/submodules/LegacyComponents/LegacyComponents/TGEmbedPIPScrubber.h new file mode 100644 index 0000000000..ade95f7089 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGEmbedPIPScrubber.h @@ -0,0 +1,8 @@ +#import + +@interface TGEmbedPIPScrubber : UIView + +@property (nonatomic, assign) CGFloat playProgress; +@property (nonatomic, assign) CGFloat downloadProgress; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGEmbedPIPScrubber.m b/submodules/LegacyComponents/LegacyComponents/TGEmbedPIPScrubber.m new file mode 100644 index 0000000000..ccc862cea3 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGEmbedPIPScrubber.m @@ -0,0 +1,67 @@ +#import "TGEmbedPIPScrubber.h" + +#import "LegacyComponentsInternal.h" + +@interface TGEmbedPIPScrubber () +{ + UIVisualEffectView *_playProgressView; + UIVisualEffectView *_remainingProgressView; + UIView *_downloadProgressView; +} +@end + +@implementation TGEmbedPIPScrubber + +- (instancetype)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + if (self != nil) + { + self.userInteractionEnabled = false; + + UIVisualEffect *lightBlurEffect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleExtraLight]; + _playProgressView = [[UIVisualEffectView alloc] initWithEffect:lightBlurEffect]; + [self addSubview:_playProgressView]; + + _downloadProgressView = [[UIView alloc] init]; + _downloadProgressView.backgroundColor = UIColorRGBA(0x000000, 0.45f); + [self addSubview:_downloadProgressView]; + + UIVisualEffect *blurEffect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleLight]; + _remainingProgressView = [[UIVisualEffectView alloc] initWithEffect:blurEffect]; + [self addSubview:_remainingProgressView]; + } + return self; +} + +- (void)setPlayProgress:(CGFloat)playProgress +{ + if (isnan(playProgress)) + playProgress = 0.0f; + + _playProgress = playProgress; + [self setNeedsLayout]; +} + +- (void)setDownloadProgress:(CGFloat)downloadProgress +{ + _downloadProgress = downloadProgress; + [self setNeedsLayout]; +} + +- (void)layoutSubviews +{ + [super layoutSubviews]; + + CGFloat playedWidth = floor(self.frame.size.width * _playProgress); + if (isnan(playedWidth)) + playedWidth = 0.0f; + + _playProgressView.frame = CGRectMake(0, 0, playedWidth, self.frame.size.height); + _remainingProgressView.frame = CGRectMake(playedWidth, 0, self.frame.size.width - playedWidth, self.frame.size.height); + + CGFloat downloadedWidth = MAX(0.0f, playedWidth - floor(self.frame.size.width * _downloadProgress)); + _downloadProgressView.frame = CGRectMake(playedWidth, 0, downloadedWidth, self.frame.size.height); +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGEmbedPlayerControls.h b/submodules/LegacyComponents/LegacyComponents/TGEmbedPlayerControls.h new file mode 100644 index 0000000000..1e098c0979 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGEmbedPlayerControls.h @@ -0,0 +1,51 @@ +#import + +@class TGModernButton; +@class TGEmbedPlayerState; + +typedef enum { + TGEmbedPlayerControlsTypeNone, + TGEmbedPlayerControlsTypeSimple, + TGEmbedPlayerControlsTypeFull +} TGEmbedPlayerControlsType; + +typedef enum { + TGEmbedPlayerWatermarkPositionTopLeft, + TGEmbedPlayerWatermarkPositionBottomLeft, + TGEmbedPlayerWatermarkPositionBottomRight +} TGEmbedPlayerWatermarkPosition; + +@interface TGEmbedPlayerControls : UIView + +@property (nonatomic, copy) void (^panelVisibilityChange)(bool hidden); + +@property (nonatomic, copy) void (^playPressed)(void); +@property (nonatomic, copy) void (^pausePressed)(void); +@property (nonatomic, copy) void (^fullscreenPressed)(void); +@property (nonatomic, copy) void (^seekToPosition)(CGFloat position); +@property (nonatomic, copy) void (^pictureInPicturePressed)(void); + +@property (nonatomic, assign) TGEmbedPlayerWatermarkPosition watermarkPosition; +@property (nonatomic, strong) UIImage *watermarkImage; +@property (nonatomic, assign) bool watermarkPrerenderedOpacity; +@property (nonatomic, assign) CGPoint watermarkOffset; +@property (nonatomic, copy) void(^watermarkPressed)(void); + +- (instancetype)initWithFrame:(CGRect)frame type:(TGEmbedPlayerControlsType)type; + +- (void)setWatermarkHidden:(bool)hidden; +- (void)setDisabled; +- (void)hidePlayButton; +- (void)setPictureInPictureHidden:(bool)hidden; + +- (void)showLargePlayButton:(bool)force; + +- (void)setState:(TGEmbedPlayerState *)state; +- (void)notifyOfPlaybackStart; + +- (void)setHidden:(bool)hidden animated:(bool)animated; + +@property (nonatomic, assign) bool inhibitFullscreenButton; +- (void)setFullscreenButtonHidden:(bool)hidden animated:(bool)animated; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGEmbedPlayerControls.m b/submodules/LegacyComponents/LegacyComponents/TGEmbedPlayerControls.m new file mode 100644 index 0000000000..52dcf186d2 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGEmbedPlayerControls.m @@ -0,0 +1,689 @@ +#import "TGEmbedPlayerControls.h" + +#import "LegacyComponentsInternal.h" +#import "TGFont.h" + +#import + +#import +#import +#import + +#import "TGEmbedPlayerScrubber.h" + +#import "TGEmbedPlayerState.h" + +const CGFloat TGEmbedPlayerControlsPanelHeight = 32.0f; + +@interface TGEmbedPlayerControls () +{ + TGEmbedPlayerControlsType _type; + + UIButton *_screenAreaButton; + + UIView *_backgroundView; + UIView *_backgroundContentView; + TGModernButton *_playButton; + TGModernButton *_pauseButton; + + UILabel *_positionLabel; + UILabel *_remainingLabel; + TGEmbedPlayerScrubber *_scrubber; + TGModernButton *_pictureInPictureButton; + + UIView *_fullscreenButtonWrapper; + TGModernButton *_fullscreenButton; + + UIView *_largePlayButtonBack; + TGModernButton *_largePlayButton; + + UIButton *_watermarkView; + bool _watermarkDenyHiding; + + bool _disabled; + bool _controlsHidden; + bool _panelHidden; + bool _animatingPanel; + bool _playing; + bool _hasPlaybackButton; + bool _showingLargeButton; + + bool _wasPlayingBeforeScrubbing; + + NSTimer *_hidePanelTimer; +} +@end + +@implementation TGEmbedPlayerControls + +- (instancetype)initWithFrame:(CGRect)frame type:(TGEmbedPlayerControlsType)type +{ + self = [super initWithFrame:frame]; + if (self != nil) + { + _type = type; + + _panelHidden = true; + self.clipsToBounds = true; + + _screenAreaButton = [[UIButton alloc] initWithFrame:self.bounds]; + _screenAreaButton.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + _screenAreaButton.exclusiveTouch = true; + [_screenAreaButton addTarget:self action:@selector(screenAreaPressed) forControlEvents:UIControlEventTouchUpInside]; + [self addSubview:_screenAreaButton]; + + if (type == TGEmbedPlayerControlsTypeFull) + { + if (iosMajorVersion() >= 8) + { + UIVisualEffectView *effectView = [[UIVisualEffectView alloc] initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleLight]]; + _backgroundView = effectView; + _backgroundContentView = effectView.contentView; + + UIView *whiteView = [[UIView alloc] initWithFrame:effectView.bounds]; + whiteView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + whiteView.backgroundColor = UIColorRGBA(0xffffff, 0.3f); + [effectView.contentView addSubview:whiteView]; + } + else + { + _backgroundView = [[UIView alloc] initWithFrame:CGRectZero]; + _backgroundContentView = _backgroundView; + } + [self addSubview:_backgroundView]; + + _pauseButton = [[TGModernButton alloc] initWithFrame:CGRectMake(0, 0, 38, TGEmbedPlayerControlsPanelHeight)]; + _pauseButton.exclusiveTouch = true; + [_pauseButton setImage:TGComponentsImageNamed(@"EmbedVideoPauseIcon") forState:UIControlStateNormal]; + [_pauseButton addTarget:self action:@selector(pauseButtonPressed) forControlEvents:UIControlEventTouchUpInside]; + [_backgroundContentView addSubview:_pauseButton]; + + _playButton = [[TGModernButton alloc] initWithFrame:CGRectMake(0, 0, 38, TGEmbedPlayerControlsPanelHeight)]; + _playButton.exclusiveTouch = true; + [_playButton setImage:TGComponentsImageNamed(@"EmbedVideoPlayIcon") forState:UIControlStateNormal]; + [_playButton addTarget:self action:@selector(playButtonPressed) forControlEvents:UIControlEventTouchUpInside]; + [_backgroundContentView addSubview:_playButton]; + + _positionLabel = [[UILabel alloc] initWithFrame:CGRectMake(24.0f, 0, 56.0f, TGEmbedPlayerControlsPanelHeight)]; + _positionLabel.backgroundColor = [UIColor clearColor]; + _positionLabel.font = TGSystemFontOfSize(13.0f); + _positionLabel.text = @"0:00"; + _positionLabel.textAlignment = NSTextAlignmentCenter; + _positionLabel.textColor = UIColorRGB(0x302e2e); + _positionLabel.userInteractionEnabled = false; + [_backgroundContentView addSubview:_positionLabel]; + + _remainingLabel = [[UILabel alloc] initWithFrame:CGRectMake(frame.size.width - 56.0f, 0, 56, TGEmbedPlayerControlsPanelHeight)]; + _remainingLabel.backgroundColor = [UIColor clearColor]; + _remainingLabel.font = TGSystemFontOfSize(13.0f); + _remainingLabel.text = @"-0:00"; + _remainingLabel.textAlignment = NSTextAlignmentCenter; + _remainingLabel.textColor = UIColorRGB(0x302e2e); + _remainingLabel.userInteractionEnabled = false; + [_backgroundContentView addSubview:_remainingLabel]; + + _pictureInPictureButton = [[TGModernButton alloc] initWithFrame:CGRectMake(frame.size.width - 45.0f, 0, 45.0f, TGEmbedPlayerControlsPanelHeight)]; + _pictureInPictureButton.exclusiveTouch = true; + [_pictureInPictureButton setImage:TGComponentsImageNamed(@"EmbedVideoPIPIcon") forState:UIControlStateNormal]; + [_pictureInPictureButton addTarget:self action:@selector(pictureInPictureButtonPressed) forControlEvents:UIControlEventTouchUpInside]; + [_backgroundContentView addSubview:_pictureInPictureButton]; + + __weak TGEmbedPlayerControls *weakSelf = self; + _scrubber = [[TGEmbedPlayerScrubber alloc] initWithFrame:CGRectZero]; + _scrubber.onInteractionStart = ^ + { + __strong TGEmbedPlayerControls *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + if (strongSelf->_playing) + { + strongSelf->_wasPlayingBeforeScrubbing = true; + if (strongSelf.pausePressed != nil) + strongSelf.pausePressed(); + } + [strongSelf _invalidateTimer]; + }; + _scrubber.onSeek = ^(CGFloat position) + { + __strong TGEmbedPlayerControls *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + if (strongSelf.seekToPosition != nil) + strongSelf.seekToPosition(position); + }; + _scrubber.onInteractionEnd = ^ + { + __strong TGEmbedPlayerControls *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [strongSelf _startTimerIfNeeded]; + + if (strongSelf->_wasPlayingBeforeScrubbing) + { + strongSelf->_wasPlayingBeforeScrubbing = false; + if (strongSelf.playPressed != nil) + strongSelf.playPressed(); + } + }; + [_scrubber setTintColor:UIColorRGB(0x2f2e2e)]; + [_backgroundContentView addSubview:_scrubber]; + } + + if (type == TGEmbedPlayerControlsTypeSimple) + { + [self showLargePlayButton:false]; + } + + if (type == TGEmbedPlayerControlsTypeSimple || type == TGEmbedPlayerControlsTypeFull) + { + _fullscreenButtonWrapper = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 51.0f, 51.0f)]; + _fullscreenButtonWrapper.alpha = 0.0f; + _fullscreenButtonWrapper.userInteractionEnabled = false; + [self addSubview:_fullscreenButtonWrapper]; + + if (iosMajorVersion() >= 8) + { + UIVisualEffectView *effectView = [[UIVisualEffectView alloc] initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleLight]]; + effectView.contentView.backgroundColor = UIColorRGBA(0xffffff, 0.3f); + effectView.clipsToBounds = true; + effectView.frame = CGRectMake(12.0f, 12.0f, 27.0f, 27.0f); + effectView.layer.cornerRadius = 13.5f; + [_fullscreenButtonWrapper addSubview:effectView]; + } + + _fullscreenButton = [[TGModernButton alloc] initWithFrame:_fullscreenButtonWrapper.bounds]; + _fullscreenButton.exclusiveTouch = true; + [_fullscreenButton setImage:TGComponentsImageNamed(@"EmbedVideoFullScreenIcon") forState:UIControlStateNormal]; + [_fullscreenButton addTarget:self action:@selector(fullscreenButtonPressed) forControlEvents:UIControlEventTouchUpInside]; + [_fullscreenButtonWrapper addSubview:_fullscreenButton]; + } + + _watermarkView = [[UIButton alloc] init]; + _watermarkView.alpha = 0.6f; + _watermarkView.adjustsImageWhenHighlighted = false; + _watermarkView.exclusiveTouch = true; + _watermarkView.hitTestEdgeInsets = UIEdgeInsetsMake(-12.0f, -12.0f, -12.0f, -12.0f); + [_watermarkView addTarget:self action:@selector(watermarkButtonPressed) forControlEvents:UIControlEventTouchUpInside]; + [self addSubview:_watermarkView]; + + _watermarkDenyHiding = true; + } + return self; +} + +- (void)showLargePlayButton:(bool)force +{ + if (_largePlayButton != nil) + return; + + if (iosMajorVersion() >= 8) + { + UIBlurEffect *effect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleLight]; + + UIVisualEffectView *effectView = [[UIVisualEffectView alloc] initWithEffect:effect]; + effectView.alpha = 1.0f; + effectView.clipsToBounds = true; + effectView.frame = CGRectMake(0.0f, 0.0f, 72.0f, 72.0f); + effectView.layer.cornerRadius = 36.0f; + [self addSubview:effectView]; + + UIVisualEffectView *vibrancyView = [[UIVisualEffectView alloc] initWithEffect:[UIVibrancyEffect effectForBlurEffect:effect]]; + vibrancyView.contentView.backgroundColor = [UIColor colorWithWhite:1.0f alpha:0.6f]; + vibrancyView.frame = effectView.bounds; + [effectView.contentView addSubview:vibrancyView]; + + _largePlayButtonBack = effectView; + + if (!force) + _largePlayButtonBack.hidden = true; + + static dispatch_once_t onceToken; + static UIImage *largePlayIcon; + dispatch_once(&onceToken, ^ + { + UIGraphicsBeginImageContextWithOptions(CGSizeMake(72, 72), false, 0.0f); + CGContextRef ctx = UIGraphicsGetCurrentContext(); + + CGContextSetFillColorWithColor(ctx, [UIColor colorWithWhite:0.8f alpha:0.09f].CGColor); + CGContextFillRect(ctx, CGRectMake(0, 0, 72, 72)); + + CGContextBeginPath(ctx); + CGContextMoveToPoint(ctx, 25.0f, 18.0f); + CGContextAddLineToPoint(ctx, 58.0f, 36.5f); + CGContextAddLineToPoint(ctx, 25.0f, 55.0f); + CGContextClosePath(ctx); + + CGContextSetFillColorWithColor(ctx, [UIColor colorWithWhite:0.0f alpha:0.7f].CGColor); + CGContextFillPath(ctx); + + largePlayIcon = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + }); + + _largePlayButton = [[TGModernButton alloc] initWithFrame:CGRectMake(0, 0, 72, 72)]; + [_largePlayButton setImage:largePlayIcon forState:UIControlStateNormal]; + [_largePlayButton addTarget:self action:@selector(playButtonPressed) forControlEvents:UIControlEventTouchUpInside]; + [effectView.contentView addSubview:_largePlayButton]; + + _showingLargeButton = force; + } +} + +- (void)setHidden:(bool)hidden animated:(bool)__unused animated +{ + if (hidden == _controlsHidden) + return; + + _controlsHidden = hidden; + + _backgroundView.hidden = hidden; + _fullscreenButtonWrapper.hidden = hidden; + + if (_type == TGEmbedPlayerControlsTypeSimple) + _largePlayButtonBack.hidden = hidden || _playing; + + if (hidden) + [self setPanelHidden:true animated:false]; +} + +#pragma mark - + +- (void)watermarkButtonPressed +{ + if (self.watermarkPressed != nil) + self.watermarkPressed(); +} + +- (void)setWatermarkHidden:(bool)hidden +{ + _watermarkView.hidden = hidden; +} + +- (void)setWatermarkPrerenderedOpacity:(bool)watermarkPrerenderedOpacity +{ + _watermarkPrerenderedOpacity = watermarkPrerenderedOpacity; + if (watermarkPrerenderedOpacity && _watermarkView.alpha > FLT_EPSILON) + _watermarkView.alpha = 1.0f; +} + +- (void)setInternalWatermarkHidden:(bool)hidden animated:(bool)animated +{ + if (_type != TGEmbedPlayerControlsTypeFull) + return; + + CGFloat visibleAlpha = _watermarkPrerenderedOpacity ? 1.0f : 0.6f; + + if (animated) + { + [UIView animateWithDuration:0.25 animations:^ + { + _watermarkView.alpha = hidden ? 0.0f : visibleAlpha; + }]; + } + else + { + _watermarkView.alpha = hidden ? 0.0f : visibleAlpha; + } +} + +- (UIImage *)watermarkImage +{ + return [_watermarkView imageForState:UIControlStateNormal]; +} + +- (void)setWatermarkImage:(UIImage *)watermarkImage +{ + [_watermarkView setImage:watermarkImage forState:UIControlStateNormal]; + [_watermarkView sizeToFit]; + [self setNeedsLayout]; +} + +- (void)setWatermarkOffset:(CGPoint)watermarkOffset +{ + _watermarkOffset = watermarkOffset; + [self setNeedsLayout]; +} + +- (void)setWatermarkPosition:(TGEmbedPlayerWatermarkPosition)watermarkPosition +{ + _watermarkPosition = watermarkPosition; + [self setNeedsLayout]; +} + +#pragma mark - + +- (void)setState:(TGEmbedPlayerState *)state +{ + _playing = state.isPlaying; + + if (_type == TGEmbedPlayerControlsTypeFull) + { + _playButton.hidden = _playing; + _pauseButton.hidden = !_playing; + + NSInteger position = (NSInteger)state.position; + NSString *positionString = [[NSString alloc] initWithFormat:@"%d:%02d", (int)position / 60, (int)position % 60]; + _positionLabel.text = positionString; + + NSInteger remaining = (NSInteger)(state.duration - state.position); + NSString *remainingString = [[NSString alloc] initWithFormat:@"-%d:%02d", (int)remaining / 60, (int)remaining % 60]; + _remainingLabel.text = remainingString; + + CGFloat fractPosition = state.position / MAX(state.duration, 0.001); + if (state.duration <= 0.01 || isnan(state.downloadProgress)) + { + _remainingLabel.hidden = true; + _scrubber.hidden = true; + } + else + { + _remainingLabel.hidden = false; + _scrubber.hidden = false; + } + + _positionLabel.hidden = (state.position < 0.0); + + if (!_scrubber.hidden) + { + [_scrubber setDownloadProgress:state.downloadProgress]; + [_scrubber setPosition:fractPosition]; + } + + if (!_watermarkDenyHiding) + [self setInternalWatermarkHidden:_playing animated:true]; + + if (_playing) + { + _largePlayButtonBack.hidden = true; + _showingLargeButton = false; + } + + if (!_playing && !_animatingPanel && _panelHidden && !_showingLargeButton) + { + [self setPanelHidden:false animated:true]; + } + else + { + if (!_playing && _hidePanelTimer != nil) + [self _invalidateTimer]; + else + [self _startTimerIfNeeded]; + } + } + else if (_type == TGEmbedPlayerControlsTypeSimple) + { + _largePlayButtonBack.hidden = _controlsHidden || _playing; + } +} + +- (void)hidePanelEvent +{ + [self _invalidateTimer]; + [self setPanelHidden:true animated:true]; +} + +- (void)_startTimer +{ + _hidePanelTimer = [TGTimerTarget scheduledMainThreadTimerWithTarget:self action:@selector(hidePanelEvent) interval:3.0 repeat:false]; +} + +- (void)_startTimerIfNeeded +{ + if (_playing && !_panelHidden && _hidePanelTimer == nil) + [self _startTimer]; +} + +- (void)_invalidateTimer +{ + [_hidePanelTimer invalidate]; + _hidePanelTimer = nil; +} + +- (void)screenAreaPressed +{ + if (_type == TGEmbedPlayerControlsTypeFull) + { + if (_panelHidden) + { + [self setPanelHidden:false animated:true]; + } + else + { + if (_playing) + [self pauseButtonPressed]; + else + [self playButtonPressed]; + } + } + else if (_type == TGEmbedPlayerControlsTypeSimple) + { + if (_playing) + [self pauseButtonPressed]; + else + [self playButtonPressed]; + } +} + +- (void)playButtonPressed +{ + if (_type == TGEmbedPlayerControlsTypeFull) + { + _largePlayButtonBack.hidden = true; + } + + if (self.playPressed != nil) + self.playPressed(); +} + +- (void)pauseButtonPressed +{ + _watermarkDenyHiding = false; + + if (self.pausePressed != nil) + self.pausePressed(); +} + +- (void)fullscreenButtonPressed +{ + if (self.fullscreenPressed != nil) + self.fullscreenPressed(); +} + +- (void)pictureInPictureButtonPressed +{ + if (self.pictureInPicturePressed != nil) + self.pictureInPicturePressed(); +} + +- (void)setPictureInPictureHidden:(bool)hidden +{ + _pictureInPictureButton.hidden = hidden; + [self setNeedsLayout]; +} + +- (void)setPanelHidden:(bool)hidden animated:(bool)animated +{ + if (_panelHidden == hidden) + return; + + _panelHidden = hidden; + + if (self.panelVisibilityChange != nil) + self.panelVisibilityChange(hidden); + + [self setFullscreenButtonDimmed:hidden animated:true]; + + if (animated) + { + UIViewAnimationOptions options = kNilOptions; + if (!hidden && iosMajorVersion() >= 7) + options |= (7 << 16); + else if (hidden) + options |= UIViewAnimationOptionCurveEaseOut; + + _animatingPanel = true; + + NSTimeInterval duration = hidden ? 0.4 : 0.25; + [UIView animateWithDuration:duration delay:0.0 options:options animations:^ + { + if (hidden) + _backgroundView.frame = CGRectMake(0, self.frame.size.height, self.frame.size.width, TGEmbedPlayerControlsPanelHeight); + else + _backgroundView.frame = CGRectMake(0, self.frame.size.height - _backgroundView.frame.size.height, self.frame.size.width, TGEmbedPlayerControlsPanelHeight); + + [self _layoutWatermark]; + } completion:^(__unused BOOL finished) + { + _animatingPanel = false; + }]; + } + else + { + _animatingPanel = false; + if (hidden) + _backgroundView.frame = CGRectMake(0, self.frame.size.height, self.frame.size.width, TGEmbedPlayerControlsPanelHeight); + else + _backgroundView.frame = CGRectMake(0, self.frame.size.height - _backgroundView.frame.size.height, self.frame.size.width, TGEmbedPlayerControlsPanelHeight); + + [self _layoutWatermark]; + } +} + +- (void)setFullscreenButtonHidden:(bool)hidden animated:(bool)animated +{ + CGFloat visibleAlpha = self.inhibitFullscreenButton ? 0.65f : 1.0f; + if (animated) + { + _fullscreenButtonWrapper.userInteractionEnabled = !hidden; + [UIView animateWithDuration:0.25 animations:^ + { + _fullscreenButtonWrapper.alpha = hidden ? 0.0f : visibleAlpha; + }]; + } + else + { + _fullscreenButtonWrapper.userInteractionEnabled = !hidden; + _fullscreenButtonWrapper.alpha = hidden ? 0.0f : visibleAlpha; + } +} + +- (void)setFullscreenButtonDimmed:(bool)dimmed animated:(bool)animated +{ + if (self.inhibitFullscreenButton && dimmed && _fullscreenButtonWrapper.alpha < FLT_EPSILON) + return; + + if (animated) + { + NSTimeInterval duration = dimmed ? 0.5 : 0.25; + [UIView animateWithDuration:duration animations:^ + { + _fullscreenButtonWrapper.alpha = dimmed ? 0.65f : 1.0f; + }]; + } + else + { + _fullscreenButtonWrapper.alpha = dimmed ? 0.65f : 1.0f; + } +} + +- (void)setDisabled +{ + _disabled = true; + _screenAreaButton.userInteractionEnabled = false; +} + +- (void)hidePlayButton +{ + [_largePlayButtonBack removeFromSuperview]; +} + +- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event +{ + UIView *view = [super hitTest:point withEvent:event]; + if (!_disabled) + return view; + + if (view == _watermarkView) + return view; + + return nil; +} + +- (void)notifyOfPlaybackStart +{ + TGDispatchAfter(0.1, dispatch_get_main_queue(), ^ + { + if (!self.inhibitFullscreenButton) + [self setFullscreenButtonHidden:false animated:true]; + + TGDispatchAfter(2.5, dispatch_get_main_queue(), ^ + { + if (_panelHidden) + [self setFullscreenButtonDimmed:true animated:true]; + + if (_playing) + [self setInternalWatermarkHidden:true animated:true]; + + _watermarkDenyHiding = false; + }); + }); +} + +- (void)_layoutWatermark +{ + CGFloat visiblePanelHeight = _panelHidden ? 0.0f : TGEmbedPlayerControlsPanelHeight; + CGRect watermarkFrame = CGRectMake(0, 0, _watermarkView.frame.size.width, _watermarkView.frame.size.height); + switch (_watermarkPosition) + { + case TGEmbedPlayerWatermarkPositionTopLeft: + { + watermarkFrame.origin = _watermarkOffset; + } + break; + + case TGEmbedPlayerWatermarkPositionBottomLeft: + { + watermarkFrame.origin = CGPointMake(_watermarkOffset.x, self.frame.size.height - watermarkFrame.size.height - visiblePanelHeight + _watermarkOffset.y); + } + break; + + case TGEmbedPlayerWatermarkPositionBottomRight: + { + watermarkFrame.origin = CGPointMake(self.frame.size.width - watermarkFrame.size.width + _watermarkOffset.x, self.frame.size.height - watermarkFrame.size.height - visiblePanelHeight + _watermarkOffset.y); + } + break; + + default: + break; + } + _watermarkView.frame = watermarkFrame; +} + +- (void)layoutSubviews +{ + _fullscreenButtonWrapper.frame = CGRectMake(self.bounds.size.width - _fullscreenButtonWrapper.frame.size.width, 0, _fullscreenButtonWrapper.frame.size.width, _fullscreenButtonWrapper.frame.size.height); + + CGFloat rightOffset = _pictureInPictureButton.hidden ? 0.0f : 35.0f; + _remainingLabel.frame = CGRectMake(self.frame.size.width - _remainingLabel.frame.size.width - rightOffset, 0.0f, _remainingLabel.frame.size.width, _remainingLabel.frame.size.height); + + _pictureInPictureButton.frame = CGRectMake(self.frame.size.width - _pictureInPictureButton.frame.size.width, 0, _pictureInPictureButton.frame.size.width, _pictureInPictureButton.frame.size.height); + + _scrubber.frame = CGRectMake(CGRectGetMaxX(_positionLabel.frame), 14.5f, self.frame.size.width - CGRectGetMaxX(_positionLabel.frame) - _remainingLabel.frame.size.width - rightOffset, 3.0f); + + if (!_animatingPanel || _backgroundView.frame.size.width < FLT_EPSILON) + { + if (_panelHidden) + _backgroundView.frame = CGRectMake(0, self.frame.size.height, self.frame.size.width, TGEmbedPlayerControlsPanelHeight); + else + _backgroundView.frame = CGRectMake(0, self.frame.size.height - _backgroundView.frame.size.height, self.frame.size.width, TGEmbedPlayerControlsPanelHeight); + } + + [self _layoutWatermark]; + + _largePlayButtonBack.frame = CGRectMake(CGFloor((self.frame.size.width - _largePlayButtonBack.frame.size.width) / 2.0f), CGFloor((self.frame.size.height - _largePlayButtonBack.frame.size.height) / 2.0f), _largePlayButtonBack.frame.size.width, _largePlayButtonBack.frame.size.height); +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGEmbedPlayerScrubber.h b/submodules/LegacyComponents/LegacyComponents/TGEmbedPlayerScrubber.h new file mode 100644 index 0000000000..0d9487735a --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGEmbedPlayerScrubber.h @@ -0,0 +1,16 @@ +#import + +@interface TGEmbedPlayerScrubber : UIControl + +@property (nonatomic, copy) void (^onInteractionStart)(); +@property (nonatomic, copy) void (^onSeek)(CGFloat position); +@property (nonatomic, copy) void (^onInteractionEnd)(); + +@property (nonatomic, readonly) bool isTracking; + +- (void)setPosition:(CGFloat)position; +- (void)setDownloadProgress:(CGFloat)progress; + +- (void)setTintColor:(UIColor *)tintColor; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGEmbedPlayerScrubber.m b/submodules/LegacyComponents/LegacyComponents/TGEmbedPlayerScrubber.m new file mode 100644 index 0000000000..3837876bb9 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGEmbedPlayerScrubber.m @@ -0,0 +1,174 @@ +#import "TGEmbedPlayerScrubber.h" + +#import "LegacyComponentsInternal.h" +#import "TGImageUtils.h" + +#import + +const CGFloat TGEmbedPlayerKnobMargin = 8.0f; + +@interface TGEmbedPlayerScrubber () +{ + UIImageView *_backgroundView; + UIView *_downloadProgressView; + UIImageView *_playPositionView; + + UIControl *_knobView; + + CGFloat _position; + CGFloat _downloadProgress; + + CGFloat _knobDragPosition; + bool _tracking; + + UIPanGestureRecognizer *_panGestureRecognizer; +} +@end + +@implementation TGEmbedPlayerScrubber + +- (instancetype)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + if (self != nil) + { + self.hitTestEdgeInsets = UIEdgeInsetsMake(-20, -20, -20, -20); + + UIImage *hollowTrackImage = [TGComponentsImageNamed(@"EmbedVideoTrackHollow") resizableImageWithCapInsets:UIEdgeInsetsMake(0, 2, 0, 2)]; + _backgroundView = [[UIImageView alloc] initWithImage:hollowTrackImage]; + [self addSubview:_backgroundView]; + + _downloadProgressView = [[UIView alloc] initWithFrame:CGRectZero]; + _downloadProgressView.backgroundColor = [UIColor blackColor]; + [self addSubview:_downloadProgressView]; + + static UIImage *trackImage = nil; + static dispatch_once_t onceToken1; + dispatch_once(&onceToken1, ^ + { + UIGraphicsBeginImageContextWithOptions(CGSizeMake(3.0f, 3.0f), false, 0.0f); + CGContextRef context = UIGraphicsGetCurrentContext(); + CGContextSetFillColorWithColor(context, [UIColor whiteColor].CGColor); + CGContextFillEllipseInRect(context, CGRectMake(0, 0, 3.0f, 3.0f)); + trackImage = [UIGraphicsGetImageFromCurrentImageContext() resizableImageWithCapInsets:UIEdgeInsetsMake(0, 1, 0, 1)]; + UIGraphicsEndImageContext(); + }); + + _playPositionView = [[UIImageView alloc] initWithImage:trackImage]; + [self addSubview:_playPositionView]; + + static UIImage *knobViewImage = nil; + static dispatch_once_t onceToken2; + dispatch_once(&onceToken2, ^ + { + UIGraphicsBeginImageContextWithOptions(CGSizeMake(21.0f, 21.0f), false, 0.0f); + CGContextRef context = UIGraphicsGetCurrentContext(); + CGContextSetShadowWithColor(context, CGSizeMake(0, 1.0f), 2.0f, [UIColor colorWithWhite:0.0f alpha:0.5f].CGColor); + CGContextSetFillColorWithColor(context, [UIColor whiteColor].CGColor); + CGContextFillEllipseInRect(context, CGRectMake(3.0f, 3.0f, 15.0f, 15.0f)); + knobViewImage = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + }); + + _knobView = [[UIControl alloc] initWithFrame:CGRectMake(0, 0, 21.0f, 21.0f)]; + _knobView.hitTestEdgeInsets = UIEdgeInsetsMake(-10, -10, -10, -10); + [self addSubview:_knobView]; + + UIImageView *knobBackground = [[UIImageView alloc] initWithImage:knobViewImage]; + [_knobView addSubview:knobBackground]; + + _panGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePan:)]; + [_knobView addGestureRecognizer:_panGestureRecognizer]; + } + return self; +} + +- (void)handlePan:(UIPanGestureRecognizer *)gestureRecognizer +{ + CGPoint touchLocation = [gestureRecognizer locationInView:self]; + + switch (gestureRecognizer.state) + { + case UIGestureRecognizerStateBegan: + { + _tracking = true; + + if (self.onInteractionStart != nil) + self.onInteractionStart(); + } + case UIGestureRecognizerStateChanged: + { + _knobDragPosition = [self knobPositionForX:touchLocation.x]; + + [self setNeedsLayout]; + + if (self.onSeek != nil) + self.onSeek(_knobDragPosition); + } + break; + + case UIGestureRecognizerStateEnded: + case UIGestureRecognizerStateCancelled: + { + _tracking = false; + _position = _knobDragPosition; + [self setNeedsLayout]; + + if (self.onInteractionEnd != nil) + self.onInteractionEnd(); + } + break; + + default: + break; + } +} + +- (bool)isTracking +{ + return _knobView.highlighted; +} + +- (void)setPosition:(CGFloat)position +{ + _position = position; + [self setNeedsLayout]; +} + +- (void)setDownloadProgress:(CGFloat)progress +{ + _downloadProgress = progress; + [self setNeedsLayout]; +} + +- (void)setTintColor:(UIColor *)tintColor +{ + UIImage *tintedImage = [TGTintedImage(TGComponentsImageNamed(@"EmbedVideoTrackHollow"), tintColor) resizableImageWithCapInsets:UIEdgeInsetsMake(0, 2, 0, 2)]; + _backgroundView.image = tintedImage; + _downloadProgressView.backgroundColor = tintColor; +} + +- (CGFloat)knobPositionRange +{ + return MAX(0.0f, self.bounds.size.width - TGEmbedPlayerKnobMargin * 2); +} + +- (CGFloat)knobPositionForX:(CGFloat)x +{ + return MAX(0, MIN(1.0f, (x - TGEmbedPlayerKnobMargin) / [self knobPositionRange])); +} + +- (void)layoutSubviews +{ + _backgroundView.frame = self.bounds; + + CGFloat downloadProgressRange = MAX(0.0f, self.bounds.size.width - 2.0f); + _downloadProgressView.frame = CGRectMake(1.0f, 1.0f, TGRetinaFloor(_downloadProgress * downloadProgressRange), 1.0f); + + CGFloat position = _tracking ? _knobDragPosition : _position; + _knobView.center = CGPointMake(TGRetinaCeil(TGEmbedPlayerKnobMargin + position * [self knobPositionRange]), 1.5f); + + _playPositionView.frame = CGRectMake(0, 0, _knobView.center.x, 3.0f); +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGEmbedPlayerState.h b/submodules/LegacyComponents/LegacyComponents/TGEmbedPlayerState.h new file mode 100644 index 0000000000..8a67cb1660 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGEmbedPlayerState.h @@ -0,0 +1,9 @@ +#import +#import "TGPIPAblePlayerView.h" + +@interface TGEmbedPlayerState : NSObject + ++ (instancetype)stateWithPlaying:(bool)playing; ++ (instancetype)stateWithPlaying:(bool)playing duration:(NSTimeInterval)duration position:(NSTimeInterval)position downloadProgress:(CGFloat)downloadProgress buffering:(bool)buffering; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGEmbedPlayerState.m b/submodules/LegacyComponents/LegacyComponents/TGEmbedPlayerState.m new file mode 100644 index 0000000000..b861c4fea9 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGEmbedPlayerState.m @@ -0,0 +1,29 @@ +#import "TGEmbedPlayerState.h" + +@implementation TGEmbedPlayerState + +@synthesize playing = _playing; +@synthesize duration = _duration; +@synthesize position = _position; +@synthesize downloadProgress = _downloadProgress; +@synthesize buffering = _buffering; + ++ (instancetype)stateWithPlaying:(bool)playing +{ + TGEmbedPlayerState *state = [[TGEmbedPlayerState alloc] init]; + state->_playing = playing; + return state; +} + ++ (instancetype)stateWithPlaying:(bool)playing duration:(NSTimeInterval)duration position:(NSTimeInterval)position downloadProgress:(CGFloat)downloadProgress buffering:(bool)buffering +{ + TGEmbedPlayerState *state = [[TGEmbedPlayerState alloc] init]; + state->_playing = playing; + state->_duration = duration; + state->_position = position; + state->_downloadProgress = downloadProgress; + state->_buffering = buffering; + return state; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGEmbedPlayerView.h b/submodules/LegacyComponents/LegacyComponents/TGEmbedPlayerView.h new file mode 100644 index 0000000000..2178838cf5 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGEmbedPlayerView.h @@ -0,0 +1,105 @@ +#import + +#import + +#import +#import +#import +#import +#import + +@class TGEmbedPlayerView; + +@protocol TGEmbedPlayerWrapperView + +- (void)reattachPlayerView; +- (void)reattachPlayerView:(TGEmbedPlayerView *)playerView; + +@end + +@interface TGEmbedPlayerView : UIView +{ + TGWebPageMediaAttachment *_webPage; + + TGMessageImageViewOverlayView *_overlayView; + CGSize _embedSize; +} + +@property (nonatomic, readonly) TGEmbedPlayerState *state; +@property (nonatomic, readonly) TGEmbedPlayerControls *controlsView; +@property (nonatomic, readonly) UIView *dimWrapperView; + +@property (nonatomic, assign) bool disableWatermarkAction; +@property (nonatomic, assign) bool inhibitFullscreenButton; + +@property (nonatomic, assign) UIRectCorner roundCorners; + +@property (nonatomic, assign) bool disallowAutoplay; +@property (nonatomic, assign) bool disallowPIP; +@property (nonatomic, assign) bool disableControls; + +@property (nonatomic, assign) CGRect initialFrame; + +@property (nonatomic, copy) void (^onWatermarkAction)(void); + +@property (nonatomic, copy) void (^requestFullscreen)(NSTimeInterval duration); +@property (nonatomic, copy) void (^onMetadataLoaded)(NSString *title, NSString *subtitle); + +@property (nonatomic, copy) void (^onBeganLoading)(void); +@property (nonatomic, copy) void (^onBeganPlaying)(void); +@property (nonatomic, copy) void (^onRealLoadProgress)(CGFloat progress, NSTimeInterval duration); + +- (instancetype)initWithWebPageAttachment:(TGWebPageMediaAttachment *)webPage; +- (instancetype)initWithWebPageAttachment:(TGWebPageMediaAttachment *)webPage thumbnailSignal:(SSignal *)thumbnailSignal; +- (instancetype)initWithWebPageAttachment:(TGWebPageMediaAttachment *)webPage thumbnailSignal:(SSignal *)thumbnailSignal alternateCachePathSignal:(SSignal *)alternateCachePathSignal; +- (void)setupWithEmbedSize:(CGSize)embedSize; + +- (void)setDimmed:(bool)dimmed animated:(bool)animated shouldDelay:(bool)shouldDelay; +- (void)setCoverImage:(UIImage *)image; + +- (void)pauseVideo:(bool)manually; + +- (void)updateState:(TGEmbedPlayerState *)state; + +- (void)hideControls; + +- (void)enterFullscreen:(NSTimeInterval)duration; +- (void)enterPictureInPicture:(TGEmbedPIPCorner)corner; + +- (void)_onPageReady; +- (void)_didBeginPlayback; +- (void)_onPanelAppearance; +- (void)_watermarkAction; + +- (void)_openWebPage:(NSURL *)url; + +- (bool)_scaleViewToMaxSize; + +- (void)onLockInPlace; + +- (bool)_useFakeLoadingProgress; +- (void)setLoadProgress:(CGFloat)value duration:(NSTimeInterval)duration; +- (void)setDimmed:(bool)dimmed animated:(bool)animated; + +- (TGEmbedPlayerControlsType)_controlsType; +- (void)_evaluateJS:(NSString *)jsString completion:(void (^)(NSString *))completion; +- (NSURL *)_embedURL; +- (NSString *)_embedHTML; +- (NSURL *)_baseURL; +- (void)_notifyOfCallbackURL:(NSURL *)url; +- (void)_setupUserScripts:(WKUserContentController *)contentController; +- (bool)_applyViewportUserScript; +- (UIView *)_webView; +- (CGFloat)_compensationEdges; + +- (void)_cleanWebView; + +- (SSignal *)loadProgress; + ++ (bool)_supportsWebPage:(TGWebPageMediaAttachment *)webPage; ++ (bool)hasNativeSupportForX:(TGWebPageMediaAttachment *)webPage; + ++ (Class)playerViewClassForWebPage:(TGWebPageMediaAttachment *)webPage onlySpecial:(bool)onlySpecial; ++ (TGEmbedPlayerView *)makePlayerViewForWebPage:(TGWebPageMediaAttachment *)webPage thumbnailSignal:(SSignal *)signal; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGEmbedPlayerView.m b/submodules/LegacyComponents/LegacyComponents/TGEmbedPlayerView.m new file mode 100644 index 0000000000..9d4430164b --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGEmbedPlayerView.m @@ -0,0 +1,948 @@ +#import "TGEmbedPlayerView.h" + +#import "LegacyComponentsInternal.h" + +#import + +#import + +#import "TGEmbedPlayerState.h" + +#import "TGEmbedYoutubePlayerView.h" +#import "TGEmbedVimeoPlayerView.h" +#import "TGEmbedCoubPlayerView.h" +#import "TGEmbedVKPlayerView.h" +#import "TGEmbedVinePlayerView.h" +#import "TGEmbedInstagramPlayerView.h" +#import "TGEmbedSoundCloudPlayerView.h" +#import "TGEmbedTwitchPlayerView.h" +#import "TGEmbedVideoPlayerView.h" + +#import + +@interface TGEmbedPlayerView () +{ + CGFloat _embedScale; + + TGImageView *_coverView; + UIView *_dimView; + UILabel *_errorLabel; + + UIWebView *_uiWebView; + WKWebView *_wkWebView; + + UIView *_interactionView; + + CGSize _maxPlayerSize; + + bool _loading; + + dispatch_semaphore_t _sema; + SQueue *_jsQueue; + + SPipe *_statePipe; + + bool _pausedManually; + bool _shouldResumePIPPlayback; + + id _currentAudioSession; + + SVariable *_loadProgressValue; +} +@end + +@implementation TGEmbedPlayerView + +@synthesize requestPictureInPicture = _requestPictureInPicture; +@synthesize disallowPIP = _disallowPIP; +@synthesize initialFrame = _initialFrame; + +- (instancetype)initWithWebPageAttachment:(TGWebPageMediaAttachment *)webPage +{ + return [self initWithWebPageAttachment:webPage thumbnailSignal:nil]; +} + +- (instancetype)initWithWebPageAttachment:(TGWebPageMediaAttachment *)webPage thumbnailSignal:(SSignal *)thumbnailSignal +{ + return [self initWithWebPageAttachment:webPage thumbnailSignal:nil alternateCachePathSignal:nil]; +} + +- (instancetype)initWithWebPageAttachment:(TGWebPageMediaAttachment *)webPage thumbnailSignal:(SSignal *)thumbnailSignal alternateCachePathSignal:(SSignal *)__unused alternateCachePathSignal +{ + self = [super initWithFrame:CGRectZero]; + if (self != nil) + { + self.clipsToBounds = true; + + _statePipe = [[SPipe alloc] init]; + _loadProgressValue = [[SVariable alloc] init]; + + _webPage = webPage; + _state = [TGEmbedPlayerState stateWithPlaying:false duration:0.0 position:0.0 downloadProgress:0.0f buffering:false]; + + TGEmbedPlayerControlsType controlsType = [self _controlsType]; + if (controlsType != TGEmbedPlayerControlsTypeNone) + { + __weak TGEmbedPlayerView *weakSelf = self; + _controlsView = [[TGEmbedPlayerControls alloc] initWithFrame:CGRectZero type:controlsType]; + _controlsView.playPressed = ^ + { + __strong TGEmbedPlayerView *strongSelf = weakSelf; + if (strongSelf != nil) + [strongSelf playVideo]; + }; + _controlsView.pausePressed = ^ + { + __strong TGEmbedPlayerView *strongSelf = weakSelf; + if (strongSelf != nil) + [strongSelf pauseVideo]; + }; + _controlsView.seekToPosition = ^(CGFloat position) + { + __strong TGEmbedPlayerView *strongSelf = weakSelf; + if (strongSelf != nil) + [strongSelf seekToFractPosition:position]; + }; + _controlsView.fullscreenPressed = ^ + { + __strong TGEmbedPlayerView *strongSelf = weakSelf; + if (strongSelf != nil) + [strongSelf enterFullscreen:0.0]; + }; + _controlsView.pictureInPicturePressed = ^ + { + __strong TGEmbedPlayerView *strongSelf = weakSelf; + if (strongSelf != nil) + [strongSelf _pictureInPicturePressed]; + }; + _controlsView.watermarkPressed = ^ + { + __strong TGEmbedPlayerView *strongSelf = weakSelf; + if (strongSelf != nil && !strongSelf.disableWatermarkAction) + [strongSelf _watermarkAction]; + }; + _controlsView.panelVisibilityChange = ^(bool hidden) + { + if (hidden) + return; + + __strong TGEmbedPlayerView *strongSelf = weakSelf; + if (strongSelf != nil) + [strongSelf _onPanelAppearance]; + }; + [_controlsView setPictureInPictureHidden:![self supportsPIP]]; + [self addSubview:_controlsView]; + } + + CGSize imageSize = CGSizeZero; + if (webPage.photo != nil) + [webPage.photo.imageInfo closestImageUrlWithSize:CGSizeMake(1136, 1136) resultingSize:&imageSize]; + + CGFloat imageAspect = imageSize.width / imageSize.height; + CGSize fitSize = CGSizeMake(215.0f, 180.0f); + if (ABS(imageAspect - 1.0f) < FLT_EPSILON) + fitSize = CGSizeMake(215.0f, 215.0f); + + imageSize = TGScaleToFill(imageSize, fitSize); + + _dimWrapperView = [[UIView alloc] init]; + _dimWrapperView.backgroundColor = [UIColor blackColor]; + [self addSubview:_dimWrapperView]; + + SSignal *coverSignal = thumbnailSignal ?: [[LegacyComponentsGlobals provider] squarePhotoThumbnail:webPage.photo ofSize:imageSize threadPool:[[LegacyComponentsGlobals provider] sharedMediaImageProcessingThreadPool] memoryCache:[[LegacyComponentsGlobals provider] sharedMediaMemoryImageCache] pixelProcessingBlock:nil downloadLargeImage:false placeholder:nil]; + + _coverView = [[TGImageView alloc] init]; + _coverView.contentMode = UIViewContentModeScaleAspectFill; + [_coverView setSignal:coverSignal]; + [_dimWrapperView addSubview:_coverView]; + + _dimView = [[UIView alloc] init]; + _dimView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + _dimView.backgroundColor = UIColorRGBA(0x000000, 0.5f); + [_dimWrapperView addSubview:_dimView]; + + _overlayView = [[TGMessageImageViewOverlayView alloc] initWithFrame:CGRectMake(0.0f, 0.0f, 44.0f, 44.0f)]; + [_overlayView setRadius:44.0f]; + [_dimWrapperView addSubview:_overlayView]; + + _errorLabel = [[UILabel alloc] init]; + _errorLabel.backgroundColor = [UIColor clearColor]; + _errorLabel.font = TGSystemFontOfSize(16.0f); + _errorLabel.hidden = true; + _errorLabel.text = TGLocalized(@"Web.Error"); + _errorLabel.textColor = [UIColor whiteColor]; + [_errorLabel sizeToFit]; + [_dimWrapperView addSubview:_errorLabel]; + + if (iosMajorVersion() >= 11) + { + _coverView.accessibilityIgnoresInvertColors = true; + _dimView.accessibilityIgnoresInvertColors = true; + _overlayView.accessibilityIgnoresInvertColors = true; + _errorLabel.accessibilityIgnoresInvertColors = true; + } + + _interactionView = [[UIView alloc] initWithFrame:self.bounds]; + _interactionView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + _interactionView.hidden = true; + [self addSubview:_interactionView]; + + _jsQueue = [[SQueue alloc] init]; + _sema = dispatch_semaphore_create(0); + } + return self; +} + +- (void)dealloc +{ + WKWebView *wkWebView = _wkWebView; + [_jsQueue dispatchSync:^ + { + wkWebView.navigationDelegate = nil; + }]; + + _uiWebView.delegate = nil; + + [_currentAudioSession dispose]; + + [[LegacyComponentsGlobals provider] resumePictureInPicturePlayback]; + + TGDispatchAfter(0.1, dispatch_get_main_queue(), ^ + { + [[LegacyComponentsGlobals provider] maybeReleaseVolumeOverlay]; + }); +} + +- (void)setDisallowPIP:(bool)disallowPIP +{ + _disallowPIP = disallowPIP; + [_controlsView setPictureInPictureHidden:disallowPIP]; +} + +- (void)setDisallowAutoplay:(bool)disallowAutoplay +{ + _disallowAutoplay = disallowAutoplay; + _dimView.hidden = true; + + [_controlsView showLargePlayButton:true]; + [self insertSubview:_dimWrapperView belowSubview:_controlsView]; +} + +- (void)setDisableControls:(bool)disableControls { + _disableControls = disableControls; + if (disableControls) { + for (UIView *view in [_dimWrapperView.subviews copy]) { + if (view != _coverView) { + view.alpha = 0.0f; + view.hidden = true; + } + } + _controlsView.hidden = true; + _dimView.hidden = true; + } +} + +- (void)_setupAudioSessionIfNeeded +{ + if (_currentAudioSession != nil) + return; + + _currentAudioSession = [[LegacyComponentsGlobals provider] requestAudioSession:TGAudioSessionTypePlayEmbedVideo interrupted:^{}]; +} + +- (void)setupWithEmbedSize:(CGSize)embedSize +{ + if (!self.disallowAutoplay || iosMajorVersion() < 8) + [self _setupAudioSessionIfNeeded]; + + CGFloat horEdge = [self _compensationEdges]; + CGFloat verEdge = horEdge * embedSize.width / embedSize.height; + + _embedSize = CGSizeMake(embedSize.width + horEdge * 2.0f, embedSize.height + verEdge * 2.0f); + + CGSize screenSize = TGScreenSize(); + screenSize = CGSizeMake(screenSize.height, screenSize.width); + _maxPlayerSize = [self _scaleViewToMaxSize] ? TGScaleToSize(embedSize, screenSize) : _embedSize; + _embedScale = _embedSize.width / _maxPlayerSize.width; + + if (iosMajorVersion() >= 8) + [self setupWKWebView]; + else + [self setupUIWebView]; + + if (!self.disallowAutoplay) + { + _overlayView.hidden = false; + [self setLoadProgress:0.01f duration:0.01]; + [_loadProgressValue set:[SSignal single:@(0.01f)]]; + } +} + +- (CGFloat)_compensationEdges +{ + return 0.0f; +} + +- (SSignal *)loadProgress { + return [_loadProgressValue signal]; +} + +- (void)hideControls +{ + [_controlsView hidePlayButton]; +} + +- (void)switchToPictureInPicture +{ + [self _pictureInPicturePressed]; +} + +- (void)_requestSystemPictureInPictureMode +{ + [self _evaluateJS:@"injectCmd('switchToPIP');" completion:^(__unused NSString *result) + { + }]; +} + +- (void)pausePIPPlayback +{ + if (_pausedManually) + return; + + _shouldResumePIPPlayback = true; + [self pauseVideo:false]; +} + +- (void)resumePIPPlayback +{ + if (_shouldResumePIPPlayback) + [self playVideo]; + + _shouldResumePIPPlayback = false; +} + +- (bool)supportsPIP +{ + CGSize screenSize = TGScreenSize(); + return !self.disallowPIP && (int)screenSize.height != 480; +} + +- (void)setDimmed:(bool)dimmed animated:(bool)animated +{ + [self setDimmed:dimmed animated:animated shouldDelay:false]; +} + +- (void)setDimmed:(bool)dimmed animated:(bool)animated shouldDelay:(bool)shouldDelay +{ + bool useFakeProgress = [self _useFakeLoadingProgress]; + if (animated) + { + if (dimmed) + { + _overlayView.hidden = false; + if (useFakeProgress) { + [self setLoadProgress:0.88f duration:3.0]; + [_loadProgressValue set:[SSignal single:@(0.88f)]]; + } + + _dimWrapperView.hidden = false; + _dimWrapperView.alpha = 1.0f; + } + else + { + [self setLoadProgress:1.0f duration:0.2]; + [_loadProgressValue set:[SSignal single:@(1.0f)]]; + + NSTimeInterval delay = shouldDelay ? 0.4 : 0.0; + [UIView animateWithDuration:0.2 delay:delay options:UIViewAnimationOptionAllowAnimatedContent | UIViewAnimationOptionCurveLinear animations:^ + { + _dimWrapperView.alpha = 0.0f; + } completion:^(__unused BOOL finished) + { + _dimWrapperView.hidden = true; + _dimWrapperView.alpha = 1.0f; + }]; + } + } + else + { + _dimWrapperView.hidden = !dimmed; + + _overlayView.hidden = !dimmed; + if (dimmed && useFakeProgress) { + [self setLoadProgress:0.88f duration:3.0]; + [_loadProgressValue set:[SSignal single:@(0.88f)]]; + } + else + [_overlayView setNone]; + } +} + +- (void)setLoadProgress:(CGFloat)value duration:(NSTimeInterval)duration +{ + [_overlayView setProgressAnimated:value duration:duration cancelEnabled:false]; + [_loadProgressValue set:[SSignal single:@(value)]]; +} + +- (bool)_useFakeLoadingProgress +{ + return true; +} + +- (void)setCoverImage:(UIImage *)image +{ + [_coverView setSignal:[SSignal single:image]]; +} + +#pragma mark - + +- (void)beginLeavingFullscreen +{ + UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:self.bounds byRoundingCorners:self.roundCorners cornerRadii:CGSizeMake(14.5f, 14.5f)]; + + CAShapeLayer *maskLayer = [CAShapeLayer layer]; + maskLayer.frame = self.bounds; + maskLayer.path = maskPath.CGPath; + + self.layer.mask = maskLayer; +} + +- (void)finishedLeavingFullscreen +{ + self.layer.mask = nil; +} + +- (void)onLockInPlace +{ + [_controlsView setFullscreenButtonHidden:false animated:true]; +} + +- (void)setInhibitFullscreenButton:(bool)inhibitFullscreenButton +{ + _inhibitFullscreenButton = inhibitFullscreenButton; + _controlsView.inhibitFullscreenButton = inhibitFullscreenButton; +} + +#pragma mark - + +- (void)setupWKWebView +{ + WKUserContentController *contentController = [[WKUserContentController alloc] init]; + + if ([self _applyViewportUserScript]) + { + NSString *jScript = @"var meta = document.createElement('meta'); meta.setAttribute('name', 'viewport'); meta.setAttribute('content', 'width=device-width'); document.getElementsByTagName('head')[0].appendChild(meta);"; + WKUserScript *viewportScript = [[WKUserScript alloc] initWithSource:jScript injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:true]; + [contentController addUserScript:viewportScript]; + } + + [self _setupUserScripts:contentController]; + + WKWebViewConfiguration *conf = [[WKWebViewConfiguration alloc] init]; + conf.allowsInlineMediaPlayback = true; + conf.userContentController = contentController; + + if ([conf respondsToSelector:@selector(setRequiresUserActionForMediaPlayback:)]) + conf.requiresUserActionForMediaPlayback = false; + else if ([conf respondsToSelector:@selector(setMediaPlaybackRequiresUserAction:)]) + conf.mediaPlaybackRequiresUserAction = false; + + if ([conf respondsToSelector:@selector(setAllowsPictureInPictureMediaPlayback:)] && !TGIsPad()) + conf.allowsPictureInPictureMediaPlayback = false; + + _wkWebView = [[WKWebView alloc] initWithFrame:CGRectZero configuration:conf]; + _wkWebView.navigationDelegate = self; + _wkWebView.scrollView.scrollEnabled = false; + if (iosMajorVersion() >= 11) + _wkWebView.scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; + + NSString *embedHTML = [self _embedHTML]; + bool useURL = (embedHTML.length == 0); + [self commonSetupWithWebView:_wkWebView useURL:useURL completion:^(NSURLRequest *request) + { + if (useURL) + [_wkWebView loadRequest:request]; + else + [_wkWebView loadHTMLString:embedHTML baseURL:[self _baseURL]]; + }]; +} + +- (void)webView:(WKWebView *)__unused webView didStartProvisionalNavigation:(WKNavigation *)__unused navigation +{ + if (_loading) + return; + + _loading = true; + if (!self.disallowAutoplay) + [self setDimmed:true animated:false]; + + if (self.onBeganLoading != nil) + self.onBeganLoading(); +} + +- (void)webView:(WKWebView *)__unused webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler +{ + NSURL *url = navigationAction.request.URL; + if (![url.scheme isEqualToString:@"http"] && ![url.scheme isEqualToString:@"https"] && ![url.absoluteString isEqualToString:@"about:blank"]) + { + [self _notifyOfCallbackURL:url]; + decisionHandler(WKNavigationActionPolicyCancel); + } + else + { + if (navigationAction.targetFrame == nil) + { + [self _openWebPage:url]; + decisionHandler(WKNavigationActionPolicyCancel); + } + else + { + decisionHandler(WKNavigationActionPolicyAllow); + } + } +} + +- (void)webView:(WKWebView *)__unused webView didFinishNavigation:(WKNavigation *)__unused navigation +{ + if (!_loading) + return; + + _loading = false; + [self _onPageReady]; +} + +- (void)webView:(WKWebView *)__unused webView didFailNavigation:(WKNavigation *)__unused navigation withError:(NSError *)__unused error +{ + if ([error.domain isEqualToString:@"WebKitErrorDomain"] && error.code == 204) + return; + + if (!_loading) + return; + + _loading = false; + _overlayView.hidden = true; + [_overlayView setNone]; + _errorLabel.hidden = false; +} + +#pragma mark - + +- (void)setupUIWebView +{ + _uiWebView = [[UIWebView alloc] initWithFrame:CGRectZero]; + _uiWebView.mediaPlaybackRequiresUserAction = false; + _uiWebView.delegate = self; + _uiWebView.scrollView.scrollEnabled = false; + + NSString *embedHTML = [self _embedHTML]; + bool useURL = (embedHTML.length == 0); + [self commonSetupWithWebView:_uiWebView useURL:useURL completion:^(NSURLRequest *request) + { + if (useURL) + [_uiWebView loadRequest:request]; + else + [_uiWebView loadHTMLString:embedHTML baseURL:[self _baseURL]]; + }]; +} + +- (BOOL)webView:(UIWebView *)__unused webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)__unused navigationType +{ + NSURL *url = request.URL; + if (![url.scheme isEqualToString:@"http"] && ![url.scheme isEqualToString:@"https"] && ![url.absoluteString isEqualToString:@"about:blank"]) + { + [self _notifyOfCallbackURL:url]; + return false; + } + + return true; +} + +- (void)webViewDidStartLoad:(UIWebView *)__unused webView +{ + if (_loading) + return; + + _loading = true; + [self setDimmed:true animated:false]; + + if (self.onBeganLoading != nil) + self.onBeganLoading(); +} + +- (void)webViewDidFinishLoad:(UIWebView *)__unused webView +{ + if (!_loading) + return; + + _loading = false; + [self _onPageReady]; +} + +- (void)webView:(UIWebView *)__unused webView didFailLoadWithError:(NSError *)__unused error +{ + if (!_loading) + return; + + _loading = false; + _overlayView.hidden = true; + [_overlayView setNone]; + _errorLabel.hidden = false; +} + +- (void)commonSetupWithWebView:(UIView *)webView useURL:(bool)useURL completion:(void (^)(NSURLRequest *))completion +{ + CGFloat horEdge = [self _compensationEdges]; + CGFloat verEdge = horEdge * _embedSize.width / _embedSize.height; + + if (iosMajorVersion() >= 11) + webView.accessibilityIgnoresInvertColors = true; + + webView.backgroundColor = [UIColor blackColor]; + webView.frame = CGRectMake(0, 0, _maxPlayerSize.width, _maxPlayerSize.height); + webView.transform = CGAffineTransformMakeScale(_embedScale, _embedScale); + webView.center = CGPointMake((_embedSize.width - horEdge * 2.0f) / 2.0f, (_embedSize.height - verEdge * 2.0f) / 2.0f); + + if (_controlsView != nil && !_disallowAutoplay) + [self insertSubview:webView belowSubview:_controlsView]; + else + [self insertSubview:webView belowSubview:_dimWrapperView]; + + if (useURL) + { + NSURL *url = [self _embedURL]; + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; + NSString *referer = [[NSString alloc] initWithFormat:@"%@://%@", [url scheme], [url host]]; + [request setValue:referer forHTTPHeaderField:@"Referer"]; + + if (completion != nil) + completion(request); + } + else + { + if (completion != nil) + completion(nil); + } +} + +#pragma mark - + +- (void)_openWebPage:(NSURL *)url +{ + [[LegacyComponentsGlobals provider] openURLNative:url]; +} + +- (void)playVideo +{ + if (_disallowAutoplay) + _dimWrapperView.hidden = true; + + [self _setupAudioSessionIfNeeded]; +} + +- (void)pauseVideo +{ + [self pauseVideo:true]; +} + +- (void)pauseVideo:(bool)manually +{ + _pausedManually = manually; +} + +- (void)seekToPosition:(NSTimeInterval)__unused position +{ + +} + +- (void)seekToFractPosition:(CGFloat)position +{ + NSTimeInterval timePosition = self.state.duration * position; + [self seekToPosition:timePosition]; +} + +- (void)enterFullscreen:(NSTimeInterval)duration +{ + if (self.requestFullscreen != nil) + self.requestFullscreen(duration); +} + +- (void)enterPictureInPicture:(TGEmbedPIPCorner)corner +{ + if (self.requestPictureInPicture != nil) + self.requestPictureInPicture(corner); +} + +- (void)_pictureInPicturePressed +{ + [self enterPictureInPicture:TGEmbedPIPCornerNone]; +} + +- (SSignal *)stateSignal +{ + return _statePipe.signalProducer(); +} + +- (void)updateState:(TGEmbedPlayerState *)state +{ + _state = state; + [_controlsView setState:state]; + + _statePipe.sink(state); +} + +#pragma mark - + +- (void)_onPageReady +{ + [self setDimmed:false animated:true]; +} + +- (void)_didBeginPlayback +{ + [_controlsView notifyOfPlaybackStart]; + + [[LegacyComponentsGlobals provider] pausePictureInPicturePlayback]; + + if (self.onBeganPlaying != nil) + self.onBeganPlaying(); +} + +- (void)_onPanelAppearance +{ + +} + +- (void)_watermarkAction +{ + [self pauseVideo]; +} + +- (void)_prepareToEnterFullscreen +{ + [_controlsView setWatermarkHidden:true]; + [_controlsView setHidden:true animated:true]; + _interactionView.hidden = false; +} + +- (void)_prepareToLeaveFullscreen +{ + [_controlsView setWatermarkHidden:false]; + [_controlsView setHidden:false animated:true]; + _interactionView.hidden = true; +} + +#pragma mark - + +- (TGEmbedPlayerControlsType)_controlsType +{ + return TGEmbedPlayerControlsTypeNone; +} + +- (void)_evaluateJS:(NSString *)jsString completion:(void (^)(NSString *))completion +{ + if (_wkWebView != nil) + { + [_jsQueue dispatch:^ + { + void (^block)(void) = ^ + { + [_wkWebView evaluateJavaScript:jsString completionHandler:^(id result, __unused NSError *error) + { + dispatch_semaphore_signal(_sema); + TGDispatchOnMainThread(^ + { + if (completion != nil) + completion(result); + }); + }]; + }; + + if (iosMajorVersion() >= 11) + TGDispatchOnMainThread(block); + else + block(); + + dispatch_semaphore_wait(_sema, DISPATCH_TIME_FOREVER); + }]; + } + else if (_uiWebView != nil) + { + NSString *result = [_uiWebView stringByEvaluatingJavaScriptFromString:jsString]; + if (completion != nil) + completion(result); + } +} + +- (NSString *)_embedHTML +{ + NSString *path = TGComponentsPathForResource(@"DefaultPlayer", @"html"); + NSError *error = nil; + NSString *embedHTMLTemplate = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:&error]; + if (error != nil) + { + TGLegacyLog(@"[DefaultEmbedPlayer]: Received error rendering template: %@", error); + return nil; + } + + NSString *embedHTML = [NSString stringWithFormat:embedHTMLTemplate, [self _embedURL].absoluteString]; + return embedHTML; +} + +- (NSURL *)_embedURL +{ + return [NSURL URLWithString:_webPage.embedUrl]; +} + +- (NSURL *)_baseURL +{ + return [NSURL URLWithString:@"about:blank"]; +} + +- (void)_setupUserScripts:(WKUserContentController *)__unused contentController +{ + NSError *error = nil; + NSString *path = TGComponentsPathForResource(@"DefaultPlayerInject", @"js"); + NSString *scriptText = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:&error]; + if (error != nil) + TGLegacyLog(@"[DefaultEmbedPlayer]: Received error loading inject script: %@", error); + + WKUserScript *script = [[WKUserScript alloc] initWithSource:scriptText injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:false]; + [contentController addUserScript:script]; +} + +- (bool)_applyViewportUserScript +{ + return true; +} + +- (void)_notifyOfCallbackURL:(NSURL *)__unused url +{ + +} + +- (UIView *)_webView +{ + if (_wkWebView != nil) + return _wkWebView; + else if (_uiWebView != nil) + return _uiWebView; + + return nil; +} + +- (bool)_scaleViewToMaxSize +{ + return false; +} + +- (void)_cleanWebView +{ + _wkWebView.navigationDelegate = nil; + [_wkWebView removeFromSuperview]; + _wkWebView = nil; + + _uiWebView.delegate = nil; + [_uiWebView removeFromSuperview]; + _uiWebView = nil; +} + ++ (bool)_supportsWebPage:(TGWebPageMediaAttachment *)__unused webPage +{ + return true; +} + ++ (Class)playerViewClassForWebPage:(TGWebPageMediaAttachment *)webPage onlySpecial:(bool)onlySpecial +{ + static dispatch_once_t onceToken; + static NSArray *playerViewClasses; + dispatch_once(&onceToken, ^ + { + playerViewClasses = @ + [ + [TGEmbedYoutubePlayerView class], + [TGEmbedVimeoPlayerView class], + [TGEmbedCoubPlayerView class], + [TGEmbedVKPlayerView class], + [TGEmbedVinePlayerView class], + [TGEmbedInstagramPlayerView class], + [TGEmbedSoundCloudPlayerView class], + [TGEmbedTwitchPlayerView class], + [TGEmbedVideoPlayerView class] + ]; + }); + + if (iosMajorVersion() >= 8) + { + for (Class playerViewClass in playerViewClasses) + { + if ([playerViewClass _supportsWebPage:webPage]) + { + if (playerViewClass == [TGEmbedVideoPlayerView class] && onlySpecial) + return nil; + + return playerViewClass; + } + } + } + + if (onlySpecial) + return nil; + + return self; +} + +- (void)layoutSubviews +{ + _dimWrapperView.frame = self.bounds; + _coverView.frame = _dimWrapperView.bounds; + _overlayView.center = CGPointMake(CGRectGetMidX(_dimWrapperView.bounds), CGRectGetMidY(_dimWrapperView.bounds)); + _errorLabel.center = _overlayView.center; + _controlsView.frame = self.bounds; +} + ++ (TGEmbedPlayerView *)makePlayerViewForWebPage:(TGWebPageMediaAttachment *)webPage thumbnailSignal:(SSignal *)signal { + Class playerClass = [self playerViewClassForWebPage:webPage onlySpecial:false]; + if (playerClass != nil) { + return [[playerClass alloc] initWithWebPageAttachment:webPage thumbnailSignal:signal]; + } else { + return nil; + } +} + ++ (bool)hasNativeSupportForX:(TGWebPageMediaAttachment *)webPage { + static dispatch_once_t onceToken; + static NSArray *playerViewClasses; + dispatch_once(&onceToken, ^ + { + playerViewClasses = @ + [ + [TGEmbedYoutubePlayerView class], + /*[TGEmbedVimeoPlayerView class], + [TGEmbedCoubPlayerView class], + [TGEmbedVKPlayerView class], + [TGEmbedVinePlayerView class], + [TGEmbedInstagramPlayerView class], + [TGEmbedSoundCloudPlayerView class], + [TGEmbedTwitchPlayerView class], + [TGEmbedVideoPlayerView class]*/ + ]; + }); + + if (iosMajorVersion() >= 8) + { + for (Class playerViewClass in playerViewClasses) + { + if ([playerViewClass _supportsWebPage:webPage]) + { + if (playerViewClass == [TGEmbedVideoPlayerView class]) + return false; + + return true; + } + } + } + + return false; +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGEmbedSoundCloudPlayerView.h b/submodules/LegacyComponents/LegacyComponents/TGEmbedSoundCloudPlayerView.h new file mode 100644 index 0000000000..15d33b7e5c --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGEmbedSoundCloudPlayerView.h @@ -0,0 +1,7 @@ +#import "TGEmbedPlayerView.h" + +@interface TGEmbedSoundCloudPlayerView : TGEmbedPlayerView + ++ (NSString *)_soundCloudIdFromText:(NSString *)text; + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGEmbedSoundCloudPlayerView.m b/submodules/LegacyComponents/LegacyComponents/TGEmbedSoundCloudPlayerView.m new file mode 100644 index 0000000000..924bc693a8 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGEmbedSoundCloudPlayerView.m @@ -0,0 +1,64 @@ +#import "TGEmbedSoundCloudPlayerView.h" + +@implementation TGEmbedSoundCloudPlayerView + +- (NSURL *)_embedURL +{ + NSString *trackId = [TGEmbedSoundCloudPlayerView _soundCloudIdFromText:_webPage.embedUrl]; + + NSString *url = [NSString stringWithFormat:@"https://w.soundcloud.com/player/?url=https%%3A%%2F%%2Fapi.soundcloud.com%%2Ftracks%%2F%@&auto_play=true&show_artwork=true&visual=true&liking=false&download=false&sharing=false&buying=false&hide_related=true&show_comments=false&show_user=true&show_reposts=false", trackId]; + return [NSURL URLWithString:url]; +} + ++ (NSString *)_soundCloudIdFromText:(NSString *)text +{ + NSMutableArray *prefixes = [NSMutableArray arrayWithArray:@ + [ + @"http://w.soundcloud.com/player/?url=", + @"https://w.soundcloud.com/player/?url=" + ]]; + + NSString *prefix = nil; + for (NSString *p in prefixes) + { + if ([text hasPrefix:p]) + { + prefix = p; + break; + } + } + + if (prefix == nil) + return nil; + + NSString *suffix = [[text substringFromIndex:prefix.length] stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; + NSArray *components = [suffix componentsSeparatedByString:@"&"]; + if (components.count < 2) + return nil; + + + NSString *url = components.firstObject; + components = [url componentsSeparatedByString:@"/"]; + + if (components.count < 1) + return nil; + + NSString *identifier = components.lastObject; + + for (int i = 0; i < (int)identifier.length; i++) + { + unichar c = [identifier characterAtIndex:i]; + if (!(c >= '0' && c <= '9')) + return nil; + } + + return identifier; +} + ++ (bool)_supportsWebPage:(TGWebPageMediaAttachment *)webPage +{ + NSString *url = webPage.embedUrl; + return ([url hasPrefix:@"http://w.soundcloud.com/player/"] || [url hasPrefix:@"https://w.soundcloud.com/player/"]); +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGEmbedTwitchPlayerView.h b/submodules/LegacyComponents/LegacyComponents/TGEmbedTwitchPlayerView.h new file mode 100644 index 0000000000..12697a35e1 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGEmbedTwitchPlayerView.h @@ -0,0 +1,5 @@ +#import + +@interface TGEmbedTwitchPlayerView : TGEmbedPlayerView + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGEmbedTwitchPlayerView.m b/submodules/LegacyComponents/LegacyComponents/TGEmbedTwitchPlayerView.m new file mode 100644 index 0000000000..73ed01b515 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGEmbedTwitchPlayerView.m @@ -0,0 +1,107 @@ +#import "TGEmbedTwitchPlayerView.h" +#import "TGEmbedPlayerState.h" + +#import "LegacyComponentsInternal.h" + +@interface TGEmbedTwitchPlayerView () +{ + bool _started; +} +@end + +@implementation TGEmbedTwitchPlayerView + +- (void)playVideo +{ + if (!_started) + return; + + [self _evaluateJS:@"injectCmd('play')" completion:nil]; + + TGEmbedPlayerState *newState = [TGEmbedPlayerState stateWithPlaying:true duration:0.0 position:-1.0 downloadProgress:0.0 buffering:false]; + [self updateState:newState]; +} + +- (void)pauseVideo:(bool)manually +{ + [super pauseVideo:manually]; + [self _evaluateJS:@"injectCmd('play')" completion:nil]; + + TGEmbedPlayerState *newState = [TGEmbedPlayerState stateWithPlaying:false duration:0.0 position:-1.0 downloadProgress:0.0 buffering:false]; + [self updateState:newState]; +} + +- (void)_onPageReady +{ + TGDispatchAfter(0.5, dispatch_get_main_queue(), ^ + { + [super _onPageReady]; + }); + + TGEmbedPlayerState *newState = [TGEmbedPlayerState stateWithPlaying:false duration:0.0 position:-1.0 downloadProgress:0.0 buffering:false]; + [self updateState:newState]; +} + +- (TGEmbedPlayerControlsType)_controlsType +{ + return TGEmbedPlayerControlsTypeFull; +} + +- (void)_notifyOfCallbackURL:(NSURL *)url +{ + NSString *action = url.host; + + NSString *query = url.query; + NSString *data; + if (query != nil) + data = [query componentsSeparatedByString:@"="][1]; + + if ([action isEqualToString:@"onPlayback"]) + { + if (!_started) + { + _started = true; + [self _didBeginPlayback]; + } + + TGEmbedPlayerState *newState = [TGEmbedPlayerState stateWithPlaying:true duration:0.0 position:-1.0 downloadProgress:0.0 buffering:false]; + [self updateState:newState]; + } +} + +- (NSString *)_embedHTML +{ + NSError *error = nil; + NSString *path = TGComponentsPathForResource(@"TwitchPlayer", @"html"); + + NSString *embedHTMLTemplate = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:&error]; + if (error != nil) + { + TGLegacyLog(@"[TwitchEmbedPlayer]: Received error rendering template: %@", error); + return nil; + } + + NSString *embedHTML = [NSString stringWithFormat:embedHTMLTemplate, _webPage.embedUrl]; + return embedHTML; +} + +- (void)_setupUserScripts:(WKUserContentController *)contentController +{ + NSError *error = nil; + NSString *path = TGComponentsPathForResource(@"TwitchPlayerInject", @"js"); + NSString *scriptText = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:&error]; + if (error != nil) + TGLegacyLog(@"[TwitchEmbedPlayer]: Received error loading inject script: %@", error); + + WKUserScript *script = [[WKUserScript alloc] initWithSource:scriptText injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:false]; + [contentController addUserScript:script]; +} + ++ (bool)_supportsWebPage:(TGWebPageMediaAttachment *)webPage +{ + NSString *url = webPage.embedUrl; + return ([url hasPrefix:@"http://player.twitch.tv/"] || [url hasPrefix:@"https://player.twitch.tv/"]) + || ([url hasPrefix:@"http://clips.twitch.tv/"] || [url hasPrefix:@"https://clips.twitch.tv/"]); +} + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGEmbedVKPlayerView.h b/submodules/LegacyComponents/LegacyComponents/TGEmbedVKPlayerView.h new file mode 100644 index 0000000000..9e91fd7590 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGEmbedVKPlayerView.h @@ -0,0 +1,5 @@ +#import "TGEmbedPlayerView.h" + +@interface TGEmbedVKPlayerView : TGEmbedPlayerView + +@end diff --git a/submodules/LegacyComponents/LegacyComponents/TGEmbedVKPlayerView.m b/submodules/LegacyComponents/LegacyComponents/TGEmbedVKPlayerView.m new file mode 100644 index 0000000000..a7ebebe7d2 --- /dev/null +++ b/submodules/LegacyComponents/LegacyComponents/TGEmbedVKPlayerView.m @@ -0,0 +1,288 @@ +#import "TGEmbedVKPlayerView.h" + +#import + +#import "TGEmbedYoutubePlayerView.h" +#import "TGEmbedVimeoPlayerView.h" +#import "TGEmbedCoubPlayerView.h" +#import "TGEmbedVideoPlayerView.h" + +@interface TGEmbedVKPlayerView () +{ + NSString *_url; + + TGEmbedPlayerView *_subPlayerView; + SMetaDisposable *_disposable; +} +@end + +@implementation TGEmbedVKPlayerView + +- (instancetype)initWithWebPageAttachment:(TGWebPageMediaAttachment *)webPage thumbnailSignal:(SSignal *)thumbnailSignal alternateCachePathSignal:(SSignal *)alternateCachePathSignal +{ + self = [super initWithWebPageAttachment:webPage thumbnailSignal:thumbnailSignal alternateCachePathSignal:alternateCachePathSignal]; + if (self != nil) + { + _url = webPage.embedUrl; + } + return self; +} + +- (void)dealloc +{ + [_disposable dispose]; +} + +- (void)setFrame:(CGRect)frame +{ + [super setFrame:frame]; + + if (_subPlayerView != nil) + _subPlayerView.frame = self.bounds; +} + +- (void)setRequestFullscreen:(void (^)(NSTimeInterval))requestFullscreen +{ + [super setRequestFullscreen:requestFullscreen]; + + if (_subPlayerView != nil) + [_subPlayerView setRequestFullscreen:requestFullscreen]; +} + +- (void)setRequestPictureInPicture:(void (^)(TGEmbedPIPCorner))requestPictureInPicture +{ + [super setRequestPictureInPicture:requestPictureInPicture]; + + if (_subPlayerView != nil) + [_subPlayerView setRequestPictureInPicture:requestPictureInPicture]; +} + +- (void)_prepareToEnterFullscreen +{ + [super _prepareToEnterFullscreen]; + + if (_subPlayerView != nil) + [_subPlayerView _prepareToEnterFullscreen]; +} + +- (void)_prepareToLeaveFullscreen +{ + [super _prepareToLeaveFullscreen]; + + if (_subPlayerView != nil) + [_subPlayerView _prepareToLeaveFullscreen]; +} + +- (void)playVideo +{ + [super playVideo]; + + if (_subPlayerView != nil) + { + [_subPlayerView playVideo]; + return; + } +} + +- (void)pauseVideo:(bool)manually +{ + [super pauseVideo:manually]; + + if (_subPlayerView != nil) + { + [_subPlayerView pauseVideo:manually]; + return; + } +} + +- (void)seekToPosition:(NSTimeInterval)position +{ + if (_subPlayerView != nil) + { + [_subPlayerView seekToPosition:position]; + return; + } +} + +- (void)onLockInPlace +{ + [super onLockInPlace]; + + if (_subPlayerView != nil) + { + [_subPlayerView onLockInPlace]; + return; + } +} + +- (void)setupWithEmbedSize:(CGSize)embedSize +{ + [super setupWithEmbedSize:embedSize]; + + [self initializePlayer]; + + [self setLoadProgress:0.01f duration:0.01]; +} + +- (void)_requestSystemPictureInPictureMode +{ + if (_subPlayerView != nil) + [_subPlayerView _requestSystemPictureInPictureMode]; + else + [super _requestSystemPictureInPictureMode]; +} + +- (TGEmbedPlayerState *)state +{ + if (_subPlayerView != nil) + return [_subPlayerView state]; + else + return [super state]; +} + +- (SSignal *)stateSignal +{ + if (_subPlayerView != nil) + return [_subPlayerView stateSignal]; + else + return [super stateSignal]; +} + +- (void)initializePlayer +{ + __weak TGEmbedVKPlayerView *weakSelf = self; + SSignal *signal = [[[LegacyComponentsGlobals provider] dataForHttpLocation:_url] map:^NSString *(NSData *data) + { + return [[NSString alloc] initWithData:data encoding:NSWindowsCP1251StringEncoding]; + }]; + + _disposable = [[SMetaDisposable alloc] init]; + [_disposable setDisposable:[[signal deliverOn:[SQueue mainQueue]] startWithNext:^(NSString *next) + { + __strong TGEmbedVKPlayerView *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + NSRange ytRange = [next rangeOfString:@"youtube.com/embed/"]; + if (ytRange.location != NSNotFound) + { + NSString *videoId = [self _getVideoId:next location:ytRange.location + @"youtube.com/embed/".length stopChar:'?']; + if (videoId.length > 0) + { + TGWebPageMediaAttachment *webPage = [[TGWebPageMediaAttachment alloc] init]; + webPage.embedUrl = [NSString stringWithFormat:@"https://www.youtube.com/embed/%@", videoId]; + + [self _setupWithSubPlayerView:[[TGEmbedYoutubePlayerView alloc] initWithWebPageAttachment:webPage]]; + } + } + + NSRange vimeoRange = [next rangeOfString:@"vimeo.com/video/"]; + if (vimeoRange.location != NSNotFound) + { + NSString *videoId = [self _getVideoId:next location:vimeoRange.location + @"vimeo.com/video/".length stopChar:'?']; + if (videoId.length > 0) + { + TGWebPageMediaAttachment *webPage = [[TGWebPageMediaAttachment alloc] init]; + webPage.embedUrl = [NSString stringWithFormat:@"https://player.vimeo.com/video/%@", videoId]; + + [self _setupWithSubPlayerView:[[TGEmbedVimeoPlayerView alloc] initWithWebPageAttachment:webPage]]; + } + } + + NSRange coubRange = [next rangeOfString:@"coub.com/embed/"]; + if (coubRange.location != NSNotFound) + { + NSString *videoId = [self _getVideoId:next location:coubRange.location + @"coub.com/embed/".length stopChar:'"']; + if (videoId.length > 0) + { + TGWebPageMediaAttachment *webPage = [[TGWebPageMediaAttachment alloc] init]; + webPage.embedUrl = [NSString stringWithFormat:@"https://coub.com/embed/%@", videoId]; + + [self _setupWithSubPlayerView:[[TGEmbedCoubPlayerView alloc] initWithWebPageAttachment:webPage]]; + } + } + + NSRange vkRange = [next rangeOfString:@"