diff --git a/LegacyComponents.xcodeproj/project.pbxproj b/LegacyComponents.xcodeproj/project.pbxproj index 5f6cf7af02..bb295d1fd6 100644 --- a/LegacyComponents.xcodeproj/project.pbxproj +++ b/LegacyComponents.xcodeproj/project.pbxproj @@ -427,6 +427,643 @@ D0177B231F2641B10044446D /* PGCameraVolumeButtonHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = D0177B131F2641B10044446D /* PGCameraVolumeButtonHandler.m */; }; D0177B2F1F26430D0044446D /* TGCameraPreviewView.h in Headers */ = {isa = PBXBuildFile; fileRef = D0177B2D1F26430D0044446D /* TGCameraPreviewView.h */; settings = {ATTRIBUTES = (Public, ); }; }; D0177B301F26430D0044446D /* TGCameraPreviewView.m in Sources */ = {isa = PBXBuildFile; fileRef = D0177B2E1F26430D0044446D /* TGCameraPreviewView.m */; }; + D07BC6CF1F2A18B700ED97AA /* TGCameraMainPhoneView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC6C91F2A18B700ED97AA /* TGCameraMainPhoneView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BC6D01F2A18B700ED97AA /* TGCameraMainPhoneView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC6CA1F2A18B700ED97AA /* TGCameraMainPhoneView.m */; }; + D07BC6D11F2A18B700ED97AA /* TGCameraMainTabletView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC6CB1F2A18B700ED97AA /* TGCameraMainTabletView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BC6D21F2A18B700ED97AA /* TGCameraMainTabletView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC6CC1F2A18B700ED97AA /* TGCameraMainTabletView.m */; }; + D07BC6D31F2A18B700ED97AA /* TGCameraMainView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC6CD1F2A18B700ED97AA /* TGCameraMainView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BC6D41F2A18B700ED97AA /* TGCameraMainView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC6CE1F2A18B700ED97AA /* TGCameraMainView.m */; }; + D07BC6D71F2A18FE00ED97AA /* UIControl+HitTestEdgeInsets.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC6D51F2A18FE00ED97AA /* UIControl+HitTestEdgeInsets.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BC6D81F2A18FE00ED97AA /* UIControl+HitTestEdgeInsets.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC6D61F2A18FE00ED97AA /* UIControl+HitTestEdgeInsets.m */; }; + D07BC6EB1F2A19A700ED97AA /* TGCameraFlashActiveView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC6D91F2A19A700ED97AA /* TGCameraFlashActiveView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BC6EC1F2A19A700ED97AA /* TGCameraFlashActiveView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC6DA1F2A19A700ED97AA /* TGCameraFlashActiveView.m */; }; + D07BC6ED1F2A19A700ED97AA /* TGCameraFlashControl.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC6DB1F2A19A700ED97AA /* TGCameraFlashControl.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BC6EE1F2A19A700ED97AA /* TGCameraFlashControl.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC6DC1F2A19A700ED97AA /* TGCameraFlashControl.m */; }; + D07BC6EF1F2A19A700ED97AA /* TGCameraFlipButton.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC6DD1F2A19A700ED97AA /* TGCameraFlipButton.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BC6F01F2A19A700ED97AA /* TGCameraFlipButton.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC6DE1F2A19A700ED97AA /* TGCameraFlipButton.m */; }; + D07BC6F11F2A19A700ED97AA /* TGCameraInterfaceAssets.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC6DF1F2A19A700ED97AA /* TGCameraInterfaceAssets.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BC6F21F2A19A700ED97AA /* TGCameraInterfaceAssets.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC6E01F2A19A700ED97AA /* TGCameraInterfaceAssets.m */; }; + D07BC6F31F2A19A700ED97AA /* TGCameraModeControl.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC6E11F2A19A700ED97AA /* TGCameraModeControl.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BC6F41F2A19A700ED97AA /* TGCameraModeControl.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC6E21F2A19A700ED97AA /* TGCameraModeControl.m */; }; + D07BC6F51F2A19A700ED97AA /* TGCameraSegmentsView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC6E31F2A19A700ED97AA /* TGCameraSegmentsView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BC6F61F2A19A700ED97AA /* TGCameraSegmentsView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC6E41F2A19A700ED97AA /* TGCameraSegmentsView.m */; }; + D07BC6F71F2A19A700ED97AA /* TGCameraShutterButton.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC6E51F2A19A700ED97AA /* TGCameraShutterButton.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BC6F81F2A19A700ED97AA /* TGCameraShutterButton.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC6E61F2A19A700ED97AA /* TGCameraShutterButton.m */; }; + D07BC6F91F2A19A700ED97AA /* TGCameraTimeCodeView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC6E71F2A19A700ED97AA /* TGCameraTimeCodeView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BC6FA1F2A19A700ED97AA /* TGCameraTimeCodeView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC6E81F2A19A700ED97AA /* TGCameraTimeCodeView.m */; }; + D07BC6FB1F2A19A700ED97AA /* TGCameraZoomView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC6E91F2A19A700ED97AA /* TGCameraZoomView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BC6FC1F2A19A700ED97AA /* TGCameraZoomView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC6EA1F2A19A700ED97AA /* TGCameraZoomView.m */; }; + D07BC6FF1F2A1A7700ED97AA /* TGMenuView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC6FD1F2A1A7700ED97AA /* TGMenuView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BC7001F2A1A7700ED97AA /* TGMenuView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC6FE1F2A1A7700ED97AA /* TGMenuView.m */; }; + D07BC70F1F2A25AE00ED97AA /* TGCameraPhotoPreviewController.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC70D1F2A25AE00ED97AA /* TGCameraPhotoPreviewController.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BC7101F2A25AE00ED97AA /* TGCameraPhotoPreviewController.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC70E1F2A25AE00ED97AA /* TGCameraPhotoPreviewController.m */; }; + D07BC7131F2A269400ED97AA /* TGImageView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC7111F2A269400ED97AA /* TGImageView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BC7141F2A269400ED97AA /* TGImageView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC7121F2A269400ED97AA /* TGImageView.m */; }; + D07BC7171F2A29B700ED97AA /* TGPhotoEditorController.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC7151F2A29B700ED97AA /* TGPhotoEditorController.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BC7181F2A29B700ED97AA /* TGPhotoEditorController.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC7161F2A29B700ED97AA /* TGPhotoEditorController.m */; }; + D07BC71F1F2A29E400ED97AA /* TGPhotoToolbarView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC7191F2A29E400ED97AA /* TGPhotoToolbarView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BC7201F2A29E400ED97AA /* TGPhotoToolbarView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC71A1F2A29E400ED97AA /* TGPhotoToolbarView.m */; }; + D07BC7211F2A29E400ED97AA /* TGPhotoToolCell.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC71B1F2A29E400ED97AA /* TGPhotoToolCell.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BC7221F2A29E400ED97AA /* TGPhotoToolCell.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC71C1F2A29E400ED97AA /* TGPhotoToolCell.m */; }; + D07BC7231F2A29E400ED97AA /* TGPhotoToolsController.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC71D1F2A29E400ED97AA /* TGPhotoToolsController.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BC7241F2A29E400ED97AA /* TGPhotoToolsController.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC71E1F2A29E400ED97AA /* TGPhotoToolsController.m */; }; + D07BC7271F2A2A5300ED97AA /* UICollectionView+Utils.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC7251F2A2A5300ED97AA /* UICollectionView+Utils.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BC7281F2A2A5300ED97AA /* UICollectionView+Utils.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC7261F2A2A5300ED97AA /* UICollectionView+Utils.m */; }; + D07BC7341F2A2A7D00ED97AA /* PGPhotoEditor.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC7291F2A2A7D00ED97AA /* PGPhotoEditor.h */; }; + D07BC7351F2A2A7D00ED97AA /* PGPhotoEditor.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC72A1F2A2A7D00ED97AA /* PGPhotoEditor.m */; }; + D07BC7361F2A2A7D00ED97AA /* PGPhotoEditorItem.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC72B1F2A2A7D00ED97AA /* PGPhotoEditorItem.h */; }; + D07BC7371F2A2A7D00ED97AA /* PGPhotoEditorPicture.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC72C1F2A2A7D00ED97AA /* PGPhotoEditorPicture.h */; }; + D07BC7381F2A2A7D00ED97AA /* PGPhotoEditorPicture.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC72D1F2A2A7D00ED97AA /* PGPhotoEditorPicture.m */; }; + D07BC7391F2A2A7D00ED97AA /* PGPhotoEditorRawDataInput.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC72E1F2A2A7D00ED97AA /* PGPhotoEditorRawDataInput.h */; }; + D07BC73A1F2A2A7D00ED97AA /* PGPhotoEditorRawDataInput.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC72F1F2A2A7D00ED97AA /* PGPhotoEditorRawDataInput.m */; }; + D07BC73B1F2A2A7D00ED97AA /* PGPhotoEditorRawDataOutput.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC7301F2A2A7D00ED97AA /* PGPhotoEditorRawDataOutput.h */; }; + D07BC73C1F2A2A7D00ED97AA /* PGPhotoEditorRawDataOutput.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC7311F2A2A7D00ED97AA /* PGPhotoEditorRawDataOutput.m */; }; + D07BC73D1F2A2A7D00ED97AA /* PGPhotoEditorView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC7321F2A2A7D00ED97AA /* PGPhotoEditorView.h */; }; + D07BC73E1F2A2A7D00ED97AA /* PGPhotoEditorView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC7331F2A2A7D00ED97AA /* PGPhotoEditorView.m */; }; + D07BC7411F2A2AC500ED97AA /* TGPhotoEditorButton.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC73F1F2A2AC500ED97AA /* TGPhotoEditorButton.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BC7421F2A2AC500ED97AA /* TGPhotoEditorButton.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC7401F2A2AC500ED97AA /* TGPhotoEditorButton.m */; }; + D07BC7691F2A2B3700ED97AA /* TGPhotoEditorBlurAreaView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC7431F2A2B3700ED97AA /* TGPhotoEditorBlurAreaView.m */; }; + D07BC76A1F2A2B3700ED97AA /* TGPhotoEditorBlurToolView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC7441F2A2B3700ED97AA /* TGPhotoEditorBlurToolView.h */; }; + D07BC76B1F2A2B3700ED97AA /* TGPhotoEditorBlurToolView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC7451F2A2B3700ED97AA /* TGPhotoEditorBlurToolView.m */; }; + D07BC76C1F2A2B3700ED97AA /* TGPhotoEditorBlurTypeButton.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC7461F2A2B3700ED97AA /* TGPhotoEditorBlurTypeButton.h */; }; + D07BC76D1F2A2B3700ED97AA /* TGPhotoEditorBlurTypeButton.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC7471F2A2B3700ED97AA /* TGPhotoEditorBlurTypeButton.m */; }; + D07BC76E1F2A2B3700ED97AA /* TGPhotoEditorBlurView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC7481F2A2B3700ED97AA /* TGPhotoEditorBlurView.h */; }; + D07BC76F1F2A2B3700ED97AA /* TGPhotoEditorBlurView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC7491F2A2B3700ED97AA /* TGPhotoEditorBlurView.m */; }; + D07BC7701F2A2B3700ED97AA /* TGPhotoEditorCollectionView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC74A1F2A2B3700ED97AA /* TGPhotoEditorCollectionView.h */; }; + D07BC7711F2A2B3700ED97AA /* TGPhotoEditorCollectionView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC74B1F2A2B3700ED97AA /* TGPhotoEditorCollectionView.m */; }; + D07BC7721F2A2B3700ED97AA /* TGPhotoEditorCurvesHistogramView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC74C1F2A2B3700ED97AA /* TGPhotoEditorCurvesHistogramView.h */; }; + D07BC7731F2A2B3700ED97AA /* TGPhotoEditorCurvesHistogramView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC74D1F2A2B3700ED97AA /* TGPhotoEditorCurvesHistogramView.m */; }; + D07BC7741F2A2B3700ED97AA /* TGPhotoEditorCurvesToolView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC74E1F2A2B3700ED97AA /* TGPhotoEditorCurvesToolView.h */; }; + D07BC7751F2A2B3700ED97AA /* TGPhotoEditorCurvesToolView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC74F1F2A2B3700ED97AA /* TGPhotoEditorCurvesToolView.m */; }; + D07BC7761F2A2B3700ED97AA /* TGPhotoEditorGenericToolView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC7501F2A2B3700ED97AA /* TGPhotoEditorGenericToolView.h */; }; + D07BC7771F2A2B3700ED97AA /* TGPhotoEditorGenericToolView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC7511F2A2B3700ED97AA /* TGPhotoEditorGenericToolView.m */; }; + D07BC7781F2A2B3700ED97AA /* TGPhotoEditorHUDView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC7521F2A2B3700ED97AA /* TGPhotoEditorHUDView.h */; }; + D07BC7791F2A2B3700ED97AA /* TGPhotoEditorHUDView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC7531F2A2B3700ED97AA /* TGPhotoEditorHUDView.m */; }; + D07BC77A1F2A2B3700ED97AA /* TGPhotoEditorInterfaceAssets.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC7541F2A2B3700ED97AA /* TGPhotoEditorInterfaceAssets.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BC77B1F2A2B3700ED97AA /* TGPhotoEditorInterfaceAssets.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC7551F2A2B3700ED97AA /* TGPhotoEditorInterfaceAssets.m */; }; + D07BC77C1F2A2B3700ED97AA /* TGPhotoEditorItemController.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC7561F2A2B3700ED97AA /* TGPhotoEditorItemController.h */; }; + D07BC77D1F2A2B3700ED97AA /* TGPhotoEditorItemController.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC7571F2A2B3700ED97AA /* TGPhotoEditorItemController.m */; }; + D07BC77E1F2A2B3700ED97AA /* TGPhotoEditorLinearBlurView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC7581F2A2B3700ED97AA /* TGPhotoEditorLinearBlurView.h */; }; + D07BC77F1F2A2B3700ED97AA /* TGPhotoEditorLinearBlurView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC7591F2A2B3700ED97AA /* TGPhotoEditorLinearBlurView.m */; }; + D07BC7801F2A2B3700ED97AA /* TGPhotoEditorPreviewView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC75A1F2A2B3700ED97AA /* TGPhotoEditorPreviewView.h */; }; + D07BC7811F2A2B3700ED97AA /* TGPhotoEditorPreviewView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC75B1F2A2B3700ED97AA /* TGPhotoEditorPreviewView.m */; }; + D07BC7821F2A2B3700ED97AA /* TGPhotoEditorRadialBlurView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC75C1F2A2B3700ED97AA /* TGPhotoEditorRadialBlurView.h */; }; + D07BC7831F2A2B3700ED97AA /* TGPhotoEditorRadialBlurView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC75D1F2A2B3700ED97AA /* TGPhotoEditorRadialBlurView.m */; }; + D07BC7841F2A2B3700ED97AA /* TGPhotoEditorSliderView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC75E1F2A2B3700ED97AA /* TGPhotoEditorSliderView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BC7851F2A2B3700ED97AA /* TGPhotoEditorSliderView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC75F1F2A2B3700ED97AA /* TGPhotoEditorSliderView.m */; }; + D07BC7861F2A2B3700ED97AA /* TGPhotoEditorTabController.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC7601F2A2B3700ED97AA /* TGPhotoEditorTabController.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BC7871F2A2B3700ED97AA /* TGPhotoEditorTabController.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC7611F2A2B3700ED97AA /* TGPhotoEditorTabController.m */; }; + D07BC7881F2A2B3700ED97AA /* TGPhotoEditorTintSwatchView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC7621F2A2B3700ED97AA /* TGPhotoEditorTintSwatchView.h */; }; + D07BC7891F2A2B3700ED97AA /* TGPhotoEditorTintSwatchView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC7631F2A2B3700ED97AA /* TGPhotoEditorTintSwatchView.m */; }; + D07BC78A1F2A2B3700ED97AA /* TGPhotoEditorTintToolView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC7641F2A2B3700ED97AA /* TGPhotoEditorTintToolView.h */; }; + D07BC78B1F2A2B3700ED97AA /* TGPhotoEditorTintToolView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC7651F2A2B3700ED97AA /* TGPhotoEditorTintToolView.m */; }; + D07BC78C1F2A2B3700ED97AA /* TGPhotoEditorToolButtonsView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC7661F2A2B3700ED97AA /* TGPhotoEditorToolButtonsView.h */; }; + D07BC78D1F2A2B3700ED97AA /* TGPhotoEditorToolButtonsView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC7671F2A2B3700ED97AA /* TGPhotoEditorToolButtonsView.m */; }; + D07BC78E1F2A2B3700ED97AA /* TGPhotoEditorToolView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC7681F2A2B3700ED97AA /* TGPhotoEditorToolView.h */; }; + D07BC7901F2A2B5A00ED97AA /* TGPhotoEditorBlurAreaView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC78F1F2A2B5A00ED97AA /* TGPhotoEditorBlurAreaView.h */; }; + D07BC7A11F2A2B8900ED97AA /* GLProgram.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC7921F2A2B8900ED97AA /* GLProgram.h */; }; + D07BC7A21F2A2B8900ED97AA /* GLProgram.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC7931F2A2B8900ED97AA /* GLProgram.m */; }; + D07BC7A31F2A2B8900ED97AA /* GPUImage.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC7941F2A2B8900ED97AA /* GPUImage.h */; }; + D07BC7A41F2A2B8900ED97AA /* GPUImageContext.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC7951F2A2B8900ED97AA /* GPUImageContext.h */; }; + D07BC7A51F2A2B8900ED97AA /* GPUImageContext.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC7961F2A2B8900ED97AA /* GPUImageContext.m */; }; + D07BC7A61F2A2B8900ED97AA /* GPUImageFilter.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC7971F2A2B8900ED97AA /* GPUImageFilter.h */; }; + D07BC7A71F2A2B8900ED97AA /* GPUImageFilter.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC7981F2A2B8900ED97AA /* GPUImageFilter.m */; }; + D07BC7A81F2A2B8900ED97AA /* GPUImageFramebuffer.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC7991F2A2B8900ED97AA /* GPUImageFramebuffer.h */; }; + D07BC7A91F2A2B8900ED97AA /* GPUImageFramebuffer.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC79A1F2A2B8900ED97AA /* GPUImageFramebuffer.m */; }; + D07BC7AA1F2A2B8900ED97AA /* GPUImageFramebufferCache.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC79B1F2A2B8900ED97AA /* GPUImageFramebufferCache.h */; }; + D07BC7AB1F2A2B8900ED97AA /* GPUImageFramebufferCache.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC79C1F2A2B8900ED97AA /* GPUImageFramebufferCache.m */; }; + D07BC7AC1F2A2B8900ED97AA /* GPUImageOutput.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC79D1F2A2B8900ED97AA /* GPUImageOutput.h */; }; + D07BC7AD1F2A2B8900ED97AA /* GPUImageOutput.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC79E1F2A2B8900ED97AA /* GPUImageOutput.m */; }; + D07BC7AE1F2A2B8900ED97AA /* GPUImageTwoInputFilter.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC79F1F2A2B8900ED97AA /* GPUImageTwoInputFilter.h */; }; + D07BC7AF1F2A2B8900ED97AA /* GPUImageTwoInputFilter.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC7A01F2A2B8900ED97AA /* GPUImageTwoInputFilter.m */; }; + D07BC7B41F2A2BBE00ED97AA /* PGPhotoProcessPass.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC7B01F2A2BBE00ED97AA /* PGPhotoProcessPass.h */; }; + D07BC7B51F2A2BBE00ED97AA /* PGPhotoProcessPass.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC7B11F2A2BBE00ED97AA /* PGPhotoProcessPass.m */; }; + D07BC7B61F2A2BBE00ED97AA /* PGBlurTool.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC7B21F2A2BBE00ED97AA /* PGBlurTool.h */; }; + D07BC7B71F2A2BBE00ED97AA /* PGBlurTool.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC7B31F2A2BBE00ED97AA /* PGBlurTool.m */; }; + D07BC7BC1F2A2BDD00ED97AA /* PGPhotoTool.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC7B81F2A2BDD00ED97AA /* PGPhotoTool.h */; }; + D07BC7BD1F2A2BDD00ED97AA /* PGPhotoTool.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC7B91F2A2BDD00ED97AA /* PGPhotoTool.m */; }; + D07BC7BE1F2A2BDD00ED97AA /* PGPhotoToolComposer.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC7BA1F2A2BDD00ED97AA /* PGPhotoToolComposer.h */; }; + D07BC7BF1F2A2BDD00ED97AA /* PGPhotoToolComposer.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC7BB1F2A2BDD00ED97AA /* PGPhotoToolComposer.m */; }; + D07BC7F61F2A2C0B00ED97AA /* PGContrastTool.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC7C01F2A2C0A00ED97AA /* PGContrastTool.h */; }; + D07BC7F71F2A2C0B00ED97AA /* PGContrastTool.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC7C11F2A2C0A00ED97AA /* PGContrastTool.m */; }; + D07BC7F81F2A2C0B00ED97AA /* PGCurvesTool.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC7C21F2A2C0A00ED97AA /* PGCurvesTool.h */; }; + D07BC7F91F2A2C0B00ED97AA /* PGCurvesTool.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC7C31F2A2C0A00ED97AA /* PGCurvesTool.m */; }; + D07BC7FA1F2A2C0B00ED97AA /* PGEnhanceTool.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC7C41F2A2C0A00ED97AA /* PGEnhanceTool.h */; }; + D07BC7FB1F2A2C0B00ED97AA /* PGEnhanceTool.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC7C51F2A2C0A00ED97AA /* PGEnhanceTool.m */; }; + D07BC7FC1F2A2C0B00ED97AA /* PGExposureTool.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC7C61F2A2C0A00ED97AA /* PGExposureTool.h */; }; + D07BC7FD1F2A2C0B00ED97AA /* PGExposureTool.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC7C71F2A2C0A00ED97AA /* PGExposureTool.m */; }; + D07BC7FE1F2A2C0B00ED97AA /* PGFadeTool.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC7C81F2A2C0A00ED97AA /* PGFadeTool.h */; }; + D07BC7FF1F2A2C0B00ED97AA /* PGFadeTool.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC7C91F2A2C0A00ED97AA /* PGFadeTool.m */; }; + D07BC8001F2A2C0B00ED97AA /* PGGrainTool.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC7CA1F2A2C0B00ED97AA /* PGGrainTool.h */; }; + D07BC8011F2A2C0B00ED97AA /* PGGrainTool.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC7CB1F2A2C0B00ED97AA /* PGGrainTool.m */; }; + D07BC8021F2A2C0B00ED97AA /* PGHighlightsTool.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC7CC1F2A2C0B00ED97AA /* PGHighlightsTool.h */; }; + D07BC8031F2A2C0B00ED97AA /* PGHighlightsTool.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC7CD1F2A2C0B00ED97AA /* PGHighlightsTool.m */; }; + D07BC8041F2A2C0B00ED97AA /* PGPhotoBlurPass.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC7CE1F2A2C0B00ED97AA /* PGPhotoBlurPass.h */; }; + D07BC8051F2A2C0B00ED97AA /* PGPhotoBlurPass.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC7CF1F2A2C0B00ED97AA /* PGPhotoBlurPass.m */; }; + D07BC8061F2A2C0B00ED97AA /* PGPhotoCustomFilterPass.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC7D01F2A2C0B00ED97AA /* PGPhotoCustomFilterPass.h */; }; + D07BC8071F2A2C0B00ED97AA /* PGPhotoCustomFilterPass.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC7D11F2A2C0B00ED97AA /* PGPhotoCustomFilterPass.m */; }; + D07BC8081F2A2C0B00ED97AA /* PGPhotoEnhanceColorConversionFilter.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC7D21F2A2C0B00ED97AA /* PGPhotoEnhanceColorConversionFilter.h */; }; + D07BC8091F2A2C0B00ED97AA /* PGPhotoEnhanceColorConversionFilter.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC7D31F2A2C0B00ED97AA /* PGPhotoEnhanceColorConversionFilter.m */; }; + D07BC80A1F2A2C0B00ED97AA /* PGPhotoEnhanceInterpolationFilter.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC7D41F2A2C0B00ED97AA /* PGPhotoEnhanceInterpolationFilter.h */; }; + D07BC80B1F2A2C0B00ED97AA /* PGPhotoEnhanceInterpolationFilter.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC7D51F2A2C0B00ED97AA /* PGPhotoEnhanceInterpolationFilter.m */; }; + D07BC80C1F2A2C0B00ED97AA /* PGPhotoEnhanceLUTGenerator.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC7D61F2A2C0B00ED97AA /* PGPhotoEnhanceLUTGenerator.h */; }; + D07BC80D1F2A2C0B00ED97AA /* PGPhotoEnhanceLUTGenerator.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC7D71F2A2C0B00ED97AA /* PGPhotoEnhanceLUTGenerator.m */; }; + D07BC80E1F2A2C0B00ED97AA /* PGPhotoEnhancePass.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC7D81F2A2C0B00ED97AA /* PGPhotoEnhancePass.h */; }; + D07BC80F1F2A2C0B00ED97AA /* PGPhotoEnhancePass.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC7D91F2A2C0B00ED97AA /* PGPhotoEnhancePass.m */; }; + D07BC8101F2A2C0B00ED97AA /* PGPhotoFilter.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC7DA1F2A2C0B00ED97AA /* PGPhotoFilter.h */; }; + D07BC8111F2A2C0B00ED97AA /* PGPhotoFilter.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC7DB1F2A2C0B00ED97AA /* PGPhotoFilter.m */; }; + D07BC8121F2A2C0B00ED97AA /* PGPhotoFilterDefinition.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC7DC1F2A2C0B00ED97AA /* PGPhotoFilterDefinition.h */; }; + D07BC8131F2A2C0B00ED97AA /* PGPhotoFilterDefinition.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC7DD1F2A2C0B00ED97AA /* PGPhotoFilterDefinition.m */; }; + D07BC8141F2A2C0B00ED97AA /* PGPhotoFilterThumbnailManager.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC7DE1F2A2C0B00ED97AA /* PGPhotoFilterThumbnailManager.h */; }; + D07BC8151F2A2C0B00ED97AA /* PGPhotoFilterThumbnailManager.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC7DF1F2A2C0B00ED97AA /* PGPhotoFilterThumbnailManager.m */; }; + D07BC8161F2A2C0B00ED97AA /* PGPhotoGaussianBlurFilter.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC7E01F2A2C0B00ED97AA /* PGPhotoGaussianBlurFilter.h */; }; + D07BC8171F2A2C0B00ED97AA /* PGPhotoGaussianBlurFilter.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC7E11F2A2C0B00ED97AA /* PGPhotoGaussianBlurFilter.m */; }; + D07BC8181F2A2C0B00ED97AA /* PGPhotoHistogram.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC7E21F2A2C0B00ED97AA /* PGPhotoHistogram.h */; }; + D07BC8191F2A2C0B00ED97AA /* PGPhotoHistogram.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC7E31F2A2C0B00ED97AA /* PGPhotoHistogram.m */; }; + D07BC81A1F2A2C0B00ED97AA /* PGPhotoHistogramGenerator.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC7E41F2A2C0B00ED97AA /* PGPhotoHistogramGenerator.h */; }; + D07BC81B1F2A2C0B00ED97AA /* PGPhotoHistogramGenerator.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC7E51F2A2C0B00ED97AA /* PGPhotoHistogramGenerator.m */; }; + D07BC81C1F2A2C0B00ED97AA /* PGPhotoLookupFilterPass.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC7E61F2A2C0B00ED97AA /* PGPhotoLookupFilterPass.h */; }; + D07BC81D1F2A2C0B00ED97AA /* PGPhotoLookupFilterPass.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC7E71F2A2C0B00ED97AA /* PGPhotoLookupFilterPass.m */; }; + D07BC81E1F2A2C0B00ED97AA /* PGPhotoSharpenPass.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC7E81F2A2C0B00ED97AA /* PGPhotoSharpenPass.h */; }; + D07BC81F1F2A2C0B00ED97AA /* PGPhotoSharpenPass.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC7E91F2A2C0B00ED97AA /* PGPhotoSharpenPass.m */; }; + D07BC8201F2A2C0B00ED97AA /* PGSaturationTool.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC7EA1F2A2C0B00ED97AA /* PGSaturationTool.h */; }; + D07BC8211F2A2C0B00ED97AA /* PGSaturationTool.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC7EB1F2A2C0B00ED97AA /* PGSaturationTool.m */; }; + D07BC8221F2A2C0B00ED97AA /* PGShadowsTool.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC7EC1F2A2C0B00ED97AA /* PGShadowsTool.h */; }; + D07BC8231F2A2C0B00ED97AA /* PGShadowsTool.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC7ED1F2A2C0B00ED97AA /* PGShadowsTool.m */; }; + D07BC8241F2A2C0B00ED97AA /* PGSharpenTool.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC7EE1F2A2C0B00ED97AA /* PGSharpenTool.h */; }; + D07BC8251F2A2C0B00ED97AA /* PGSharpenTool.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC7EF1F2A2C0B00ED97AA /* PGSharpenTool.m */; }; + D07BC8261F2A2C0B00ED97AA /* PGTintTool.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC7F01F2A2C0B00ED97AA /* PGTintTool.h */; }; + D07BC8271F2A2C0B00ED97AA /* PGTintTool.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC7F11F2A2C0B00ED97AA /* PGTintTool.m */; }; + D07BC8281F2A2C0B00ED97AA /* PGVignetteTool.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC7F21F2A2C0B00ED97AA /* PGVignetteTool.h */; }; + D07BC8291F2A2C0B00ED97AA /* PGVignetteTool.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC7F31F2A2C0B00ED97AA /* PGVignetteTool.m */; }; + D07BC82A1F2A2C0B00ED97AA /* PGWarmthTool.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC7F41F2A2C0B00ED97AA /* PGWarmthTool.h */; }; + D07BC82B1F2A2C0B00ED97AA /* PGWarmthTool.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC7F51F2A2C0B00ED97AA /* PGWarmthTool.m */; }; + D07BC8351F2A2D0C00ED97AA /* TGSecretTimerMenu.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC82D1F2A2D0C00ED97AA /* TGSecretTimerMenu.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BC8361F2A2D0C00ED97AA /* TGSecretTimerMenu.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC82E1F2A2D0C00ED97AA /* TGSecretTimerMenu.m */; }; + D07BC8371F2A2D0C00ED97AA /* TGSecretTimerPickerItemView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC82F1F2A2D0C00ED97AA /* TGSecretTimerPickerItemView.h */; }; + D07BC8381F2A2D0C00ED97AA /* TGSecretTimerPickerItemView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC8301F2A2D0C00ED97AA /* TGSecretTimerPickerItemView.m */; }; + D07BC8391F2A2D0C00ED97AA /* TGSecretTimerValueController.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC8311F2A2D0C00ED97AA /* TGSecretTimerValueController.h */; }; + D07BC83A1F2A2D0C00ED97AA /* TGSecretTimerValueController.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC8321F2A2D0C00ED97AA /* TGSecretTimerValueController.m */; }; + D07BC83B1F2A2D0C00ED97AA /* TGSecretTimerValueControllerItemView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC8331F2A2D0C00ED97AA /* TGSecretTimerValueControllerItemView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BC83C1F2A2D0C00ED97AA /* TGSecretTimerValueControllerItemView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC8341F2A2D0C00ED97AA /* TGSecretTimerValueControllerItemView.m */; }; + D07BC8411F2A2D7900ED97AA /* TGPhotoCaptionController.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC83D1F2A2D7900ED97AA /* TGPhotoCaptionController.h */; }; + D07BC8421F2A2D7900ED97AA /* TGPhotoCaptionController.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC83E1F2A2D7900ED97AA /* TGPhotoCaptionController.m */; }; + D07BC8431F2A2D7900ED97AA /* TGPhotoCaptionInputMixin.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC83F1F2A2D7900ED97AA /* TGPhotoCaptionInputMixin.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BC8441F2A2D7A00ED97AA /* TGPhotoCaptionInputMixin.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC8401F2A2D7900ED97AA /* TGPhotoCaptionInputMixin.m */; }; + D07BC8481F2A2DA200ED97AA /* TGMenuSheetController.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC8461F2A2DA200ED97AA /* TGMenuSheetController.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BC8491F2A2DA200ED97AA /* TGMenuSheetController.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC8471F2A2DA200ED97AA /* TGMenuSheetController.m */; }; + D07BC8561F2A2DBD00ED97AA /* TGMenuSheetButtonItemView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC84A1F2A2DBD00ED97AA /* TGMenuSheetButtonItemView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BC8571F2A2DBD00ED97AA /* TGMenuSheetButtonItemView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC84B1F2A2DBD00ED97AA /* TGMenuSheetButtonItemView.m */; }; + D07BC8581F2A2DBD00ED97AA /* TGMenuSheetCollectionView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC84C1F2A2DBD00ED97AA /* TGMenuSheetCollectionView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BC8591F2A2DBD00ED97AA /* TGMenuSheetCollectionView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC84D1F2A2DBD00ED97AA /* TGMenuSheetCollectionView.m */; }; + D07BC85A1F2A2DBD00ED97AA /* TGMenuSheetDimView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC84E1F2A2DBD00ED97AA /* TGMenuSheetDimView.h */; }; + D07BC85B1F2A2DBD00ED97AA /* TGMenuSheetDimView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC84F1F2A2DBD00ED97AA /* TGMenuSheetDimView.m */; }; + D07BC85C1F2A2DBD00ED97AA /* TGMenuSheetItemView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC8501F2A2DBD00ED97AA /* TGMenuSheetItemView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BC85D1F2A2DBD00ED97AA /* TGMenuSheetItemView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC8511F2A2DBD00ED97AA /* TGMenuSheetItemView.m */; }; + D07BC85E1F2A2DBD00ED97AA /* TGMenuSheetTitleItemView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC8521F2A2DBD00ED97AA /* TGMenuSheetTitleItemView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BC85F1F2A2DBD00ED97AA /* TGMenuSheetTitleItemView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC8531F2A2DBD00ED97AA /* TGMenuSheetTitleItemView.m */; }; + D07BC8601F2A2DBD00ED97AA /* TGMenuSheetView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC8541F2A2DBD00ED97AA /* TGMenuSheetView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BC8611F2A2DBD00ED97AA /* TGMenuSheetView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC8551F2A2DBD00ED97AA /* TGMenuSheetView.m */; }; + D07BC8651F2A2F1300ED97AA /* TGMediaPickerCaptionInputPanel.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC8631F2A2F1300ED97AA /* TGMediaPickerCaptionInputPanel.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BC8661F2A2F1300ED97AA /* TGMediaPickerCaptionInputPanel.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC8641F2A2F1300ED97AA /* TGMediaPickerCaptionInputPanel.m */; }; + D07BC86B1F2A2F3800ED97AA /* HPGrowingTextView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC8681F2A2F3800ED97AA /* HPGrowingTextView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BC86C1F2A2F3800ED97AA /* HPGrowingTextView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC8691F2A2F3800ED97AA /* HPGrowingTextView.m */; }; + D07BC86D1F2A2F3800ED97AA /* HPTextViewInternal.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC86A1F2A2F3800ED97AA /* HPTextViewInternal.m */; }; + D07BC86F1F2A2F5300ED97AA /* HPTextViewInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC86E1F2A2F5300ED97AA /* HPTextViewInternal.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BC8721F2A2F6500ED97AA /* TGInputTextTag.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC8701F2A2F6500ED97AA /* TGInputTextTag.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BC8731F2A2F6500ED97AA /* TGInputTextTag.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC8711F2A2F6500ED97AA /* TGInputTextTag.m */; }; + D07BC8761F2A2F7B00ED97AA /* TGWeakDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC8741F2A2F7B00ED97AA /* TGWeakDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BC8771F2A2F7B00ED97AA /* TGWeakDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC8751F2A2F7B00ED97AA /* TGWeakDelegate.m */; }; + D07BC87D1F2A365000ED97AA /* TGProgressSpinnerView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC8791F2A365000ED97AA /* TGProgressSpinnerView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BC87E1F2A365000ED97AA /* TGProgressSpinnerView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC87A1F2A365000ED97AA /* TGProgressSpinnerView.m */; }; + D07BC87F1F2A365000ED97AA /* TGProgressWindow.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC87B1F2A365000ED97AA /* TGProgressWindow.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BC8801F2A365000ED97AA /* TGProgressWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC87C1F2A365000ED97AA /* TGProgressWindow.m */; }; + D07BC8831F2A367500ED97AA /* TGActivityIndicatorView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC8811F2A367500ED97AA /* TGActivityIndicatorView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BC8841F2A367500ED97AA /* TGActivityIndicatorView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC8821F2A367500ED97AA /* TGActivityIndicatorView.m */; }; + D07BC8931F2A375800ED97AA /* TGPhotoCropAreaView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC8851F2A375800ED97AA /* TGPhotoCropAreaView.h */; }; + D07BC8941F2A375800ED97AA /* TGPhotoCropAreaView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC8861F2A375800ED97AA /* TGPhotoCropAreaView.m */; }; + D07BC8951F2A375800ED97AA /* TGPhotoCropControl.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC8871F2A375800ED97AA /* TGPhotoCropControl.h */; }; + D07BC8961F2A375800ED97AA /* TGPhotoCropControl.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC8881F2A375800ED97AA /* TGPhotoCropControl.m */; }; + D07BC8971F2A375800ED97AA /* TGPhotoCropController.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC8891F2A375800ED97AA /* TGPhotoCropController.h */; }; + D07BC8981F2A375800ED97AA /* TGPhotoCropController.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC88A1F2A375800ED97AA /* TGPhotoCropController.m */; }; + D07BC8991F2A375800ED97AA /* TGPhotoCropGridView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC88B1F2A375800ED97AA /* TGPhotoCropGridView.h */; }; + D07BC89A1F2A375800ED97AA /* TGPhotoCropGridView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC88C1F2A375800ED97AA /* TGPhotoCropGridView.m */; }; + D07BC89B1F2A375800ED97AA /* TGPhotoCropRotationView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC88D1F2A375800ED97AA /* TGPhotoCropRotationView.h */; }; + D07BC89C1F2A375800ED97AA /* TGPhotoCropRotationView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC88E1F2A375800ED97AA /* TGPhotoCropRotationView.m */; }; + D07BC89D1F2A375800ED97AA /* TGPhotoCropScrollView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC88F1F2A375800ED97AA /* TGPhotoCropScrollView.h */; }; + D07BC89E1F2A375800ED97AA /* TGPhotoCropScrollView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC8901F2A375800ED97AA /* TGPhotoCropScrollView.m */; }; + D07BC89F1F2A375800ED97AA /* TGPhotoCropView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC8911F2A375800ED97AA /* TGPhotoCropView.h */; }; + D07BC8A01F2A375800ED97AA /* TGPhotoCropView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC8921F2A375800ED97AA /* TGPhotoCropView.m */; }; + D07BC8A51F2A37A500ED97AA /* TGPhotoAvatarCropController.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC8A11F2A37A500ED97AA /* TGPhotoAvatarCropController.h */; }; + D07BC8A61F2A37A500ED97AA /* TGPhotoAvatarCropController.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC8A21F2A37A500ED97AA /* TGPhotoAvatarCropController.m */; }; + D07BC8A71F2A37A500ED97AA /* TGPhotoAvatarCropView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC8A31F2A37A500ED97AA /* TGPhotoAvatarCropView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BC8A81F2A37A500ED97AA /* TGPhotoAvatarCropView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC8A41F2A37A500ED97AA /* TGPhotoAvatarCropView.m */; }; + D07BC8BF1F2A37EC00ED97AA /* TGPhotoPaintActionsView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC8A91F2A37EC00ED97AA /* TGPhotoPaintActionsView.h */; }; + D07BC8C01F2A37EC00ED97AA /* TGPhotoPaintActionsView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC8AA1F2A37EC00ED97AA /* TGPhotoPaintActionsView.m */; }; + D07BC8C11F2A37EC00ED97AA /* TGPhotoPaintColorPicker.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC8AB1F2A37EC00ED97AA /* TGPhotoPaintColorPicker.h */; }; + D07BC8C21F2A37EC00ED97AA /* TGPhotoPaintColorPicker.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC8AC1F2A37EC00ED97AA /* TGPhotoPaintColorPicker.m */; }; + D07BC8C31F2A37EC00ED97AA /* TGPhotoPaintController.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC8AD1F2A37EC00ED97AA /* TGPhotoPaintController.h */; }; + D07BC8C41F2A37EC00ED97AA /* TGPhotoPaintController.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC8AE1F2A37EC00ED97AA /* TGPhotoPaintController.m */; }; + D07BC8C51F2A37EC00ED97AA /* TGPhotoPaintEntityView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC8AF1F2A37EC00ED97AA /* TGPhotoPaintEntityView.h */; }; + D07BC8C61F2A37EC00ED97AA /* TGPhotoPaintEntityView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC8B01F2A37EC00ED97AA /* TGPhotoPaintEntityView.m */; }; + D07BC8C71F2A37EC00ED97AA /* TGPhotoPaintFont.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC8B11F2A37EC00ED97AA /* TGPhotoPaintFont.h */; }; + D07BC8C81F2A37EC00ED97AA /* TGPhotoPaintFont.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC8B21F2A37EC00ED97AA /* TGPhotoPaintFont.m */; }; + D07BC8C91F2A37EC00ED97AA /* TGPhotoPaintScrollView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC8B31F2A37EC00ED97AA /* TGPhotoPaintScrollView.h */; }; + D07BC8CA1F2A37EC00ED97AA /* TGPhotoPaintScrollView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC8B41F2A37EC00ED97AA /* TGPhotoPaintScrollView.m */; }; + D07BC8CB1F2A37EC00ED97AA /* TGPhotoPaintSelectionContainerView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC8B51F2A37EC00ED97AA /* TGPhotoPaintSelectionContainerView.h */; }; + D07BC8CC1F2A37EC00ED97AA /* TGPhotoPaintSelectionContainerView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC8B61F2A37EC00ED97AA /* TGPhotoPaintSelectionContainerView.m */; }; + D07BC8CD1F2A37EC00ED97AA /* TGPhotoPaintSettingsView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC8B71F2A37EC00ED97AA /* TGPhotoPaintSettingsView.h */; }; + D07BC8CE1F2A37EC00ED97AA /* TGPhotoPaintSettingsView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC8B81F2A37EC00ED97AA /* TGPhotoPaintSettingsView.m */; }; + D07BC8CF1F2A37EC00ED97AA /* TGPhotoPaintSettingsWrapperView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC8B91F2A37EC00ED97AA /* TGPhotoPaintSettingsWrapperView.h */; }; + D07BC8D01F2A37EC00ED97AA /* TGPhotoPaintSettingsWrapperView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC8BA1F2A37EC00ED97AA /* TGPhotoPaintSettingsWrapperView.m */; }; + D07BC8D11F2A37EC00ED97AA /* TGPhotoPaintSparseView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC8BB1F2A37EC00ED97AA /* TGPhotoPaintSparseView.h */; }; + D07BC8D21F2A37EC00ED97AA /* TGPhotoPaintSparseView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC8BC1F2A37EC00ED97AA /* TGPhotoPaintSparseView.m */; }; + D07BC8D31F2A37EC00ED97AA /* TGPhotoPaintTextEntity.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC8BD1F2A37EC00ED97AA /* TGPhotoPaintTextEntity.h */; }; + D07BC8D41F2A37EC00ED97AA /* TGPhotoPaintTextEntity.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC8BE1F2A37EC00ED97AA /* TGPhotoPaintTextEntity.m */; }; + D07BC8FF1F2A380D00ED97AA /* TGPaintBrush.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC8D51F2A380D00ED97AA /* TGPaintBrush.h */; }; + D07BC9001F2A380D00ED97AA /* TGPaintBrush.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC8D61F2A380D00ED97AA /* TGPaintBrush.m */; }; + D07BC9011F2A380D00ED97AA /* TGPaintBrushPreview.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC8D71F2A380D00ED97AA /* TGPaintBrushPreview.h */; }; + D07BC9021F2A380D00ED97AA /* TGPaintBrushPreview.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC8D81F2A380D00ED97AA /* TGPaintBrushPreview.m */; }; + D07BC9031F2A380D00ED97AA /* TGPaintBuffers.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC8D91F2A380D00ED97AA /* TGPaintBuffers.h */; }; + D07BC9041F2A380D00ED97AA /* TGPaintBuffers.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC8DA1F2A380D00ED97AA /* TGPaintBuffers.m */; }; + D07BC9051F2A380D00ED97AA /* TGPaintCanvas.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC8DB1F2A380D00ED97AA /* TGPaintCanvas.h */; }; + D07BC9061F2A380D00ED97AA /* TGPaintCanvas.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC8DC1F2A380D00ED97AA /* TGPaintCanvas.m */; }; + D07BC9071F2A380D00ED97AA /* TGPaintEllipticalBrush.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC8DD1F2A380D00ED97AA /* TGPaintEllipticalBrush.h */; }; + D07BC9081F2A380D00ED97AA /* TGPaintEllipticalBrush.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC8DE1F2A380D00ED97AA /* TGPaintEllipticalBrush.m */; }; + D07BC9091F2A380D00ED97AA /* TGPaintFaceDebugView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC8DF1F2A380D00ED97AA /* TGPaintFaceDebugView.h */; }; + D07BC90A1F2A380D00ED97AA /* TGPaintFaceDebugView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC8E01F2A380D00ED97AA /* TGPaintFaceDebugView.m */; }; + D07BC90B1F2A380D00ED97AA /* TGPaintFaceDetector.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC8E11F2A380D00ED97AA /* TGPaintFaceDetector.h */; }; + D07BC90C1F2A380D00ED97AA /* TGPaintFaceDetector.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC8E21F2A380D00ED97AA /* TGPaintFaceDetector.m */; }; + D07BC90D1F2A380D00ED97AA /* TGPainting.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC8E31F2A380D00ED97AA /* TGPainting.h */; }; + D07BC90E1F2A380D00ED97AA /* TGPainting.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC8E41F2A380D00ED97AA /* TGPainting.m */; }; + D07BC90F1F2A380D00ED97AA /* TGPaintingWrapperView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC8E51F2A380D00ED97AA /* TGPaintingWrapperView.h */; }; + D07BC9101F2A380D00ED97AA /* TGPaintingWrapperView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC8E61F2A380D00ED97AA /* TGPaintingWrapperView.m */; }; + D07BC9111F2A380D00ED97AA /* TGPaintInput.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC8E71F2A380D00ED97AA /* TGPaintInput.h */; }; + D07BC9121F2A380D00ED97AA /* TGPaintInput.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC8E81F2A380D00ED97AA /* TGPaintInput.m */; }; + D07BC9131F2A380D00ED97AA /* TGPaintNeonBrush.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC8E91F2A380D00ED97AA /* TGPaintNeonBrush.h */; }; + D07BC9141F2A380D00ED97AA /* TGPaintNeonBrush.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC8EA1F2A380D00ED97AA /* TGPaintNeonBrush.m */; }; + D07BC9151F2A380D00ED97AA /* TGPaintPanGestureRecognizer.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC8EB1F2A380D00ED97AA /* TGPaintPanGestureRecognizer.h */; }; + D07BC9161F2A380D00ED97AA /* TGPaintPanGestureRecognizer.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC8EC1F2A380D00ED97AA /* TGPaintPanGestureRecognizer.m */; }; + D07BC9171F2A380D00ED97AA /* TGPaintPath.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC8ED1F2A380D00ED97AA /* TGPaintPath.h */; }; + D07BC9181F2A380D00ED97AA /* TGPaintPath.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC8EE1F2A380D00ED97AA /* TGPaintPath.m */; }; + D07BC9191F2A380D00ED97AA /* TGPaintRadialBrush.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC8EF1F2A380D00ED97AA /* TGPaintRadialBrush.h */; }; + D07BC91A1F2A380D00ED97AA /* TGPaintRadialBrush.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC8F01F2A380D00ED97AA /* TGPaintRadialBrush.m */; }; + D07BC91B1F2A380D00ED97AA /* TGPaintRender.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC8F11F2A380D00ED97AA /* TGPaintRender.h */; }; + D07BC91C1F2A380D00ED97AA /* TGPaintRender.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC8F21F2A380D00ED97AA /* TGPaintRender.m */; }; + D07BC91D1F2A380D00ED97AA /* TGPaintShader.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC8F31F2A380D00ED97AA /* TGPaintShader.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BC91E1F2A380D00ED97AA /* TGPaintShader.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC8F41F2A380D00ED97AA /* TGPaintShader.m */; }; + D07BC91F1F2A380D00ED97AA /* TGPaintShaderSet.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC8F51F2A380D00ED97AA /* TGPaintShaderSet.h */; }; + D07BC9201F2A380D00ED97AA /* TGPaintShaderSet.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC8F61F2A380D00ED97AA /* TGPaintShaderSet.m */; }; + D07BC9211F2A380D00ED97AA /* TGPaintSlice.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC8F71F2A380D00ED97AA /* TGPaintSlice.h */; }; + D07BC9221F2A380D00ED97AA /* TGPaintSlice.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC8F81F2A380D00ED97AA /* TGPaintSlice.m */; }; + D07BC9231F2A380D00ED97AA /* TGPaintState.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC8F91F2A380D00ED97AA /* TGPaintState.h */; }; + D07BC9241F2A380D00ED97AA /* TGPaintState.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC8FA1F2A380D00ED97AA /* TGPaintState.m */; }; + D07BC9251F2A380D00ED97AA /* TGPaintSwatch.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC8FB1F2A380D00ED97AA /* TGPaintSwatch.h */; }; + D07BC9261F2A380D00ED97AA /* TGPaintSwatch.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC8FC1F2A380D00ED97AA /* TGPaintSwatch.m */; }; + D07BC9271F2A380D00ED97AA /* TGPaintTexture.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC8FD1F2A380D00ED97AA /* TGPaintTexture.h */; }; + D07BC9281F2A380D00ED97AA /* TGPaintTexture.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC8FE1F2A380D00ED97AA /* TGPaintTexture.m */; }; + D07BC92B1F2A3A3F00ED97AA /* matrix.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC9291F2A3A3F00ED97AA /* matrix.h */; }; + D07BC92C1F2A3A3F00ED97AA /* matrix.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC92A1F2A3A3F00ED97AA /* matrix.m */; }; + D07BC92F1F2A3BA600ED97AA /* TGPhotoEntitiesContainerView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC92D1F2A3BA600ED97AA /* TGPhotoEntitiesContainerView.h */; }; + D07BC9301F2A3BA600ED97AA /* TGPhotoEntitiesContainerView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC92E1F2A3BA600ED97AA /* TGPhotoEntitiesContainerView.m */; }; + D07BC9351F2A3BD100ED97AA /* TGPhotoTextEntityView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC9311F2A3BD100ED97AA /* TGPhotoTextEntityView.h */; }; + D07BC9361F2A3BD100ED97AA /* TGPhotoTextEntityView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC9321F2A3BD100ED97AA /* TGPhotoTextEntityView.m */; }; + D07BC9371F2A3BD100ED97AA /* TGPhotoStickerEntityView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC9331F2A3BD100ED97AA /* TGPhotoStickerEntityView.h */; }; + D07BC9381F2A3BD100ED97AA /* TGPhotoStickerEntityView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC9341F2A3BD100ED97AA /* TGPhotoStickerEntityView.m */; }; + D07BC93B1F2A3C1F00ED97AA /* TGPhotoQualityController.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC9391F2A3C1F00ED97AA /* TGPhotoQualityController.h */; }; + D07BC93C1F2A3C1F00ED97AA /* TGPhotoQualityController.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC93A1F2A3C1F00ED97AA /* TGPhotoQualityController.m */; }; + D07BC93F1F2A3DB900ED97AA /* TGMessageImageViewOverlayView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC93D1F2A3DB900ED97AA /* TGMessageImageViewOverlayView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BC9401F2A3DB900ED97AA /* TGMessageImageViewOverlayView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC93E1F2A3DB900ED97AA /* TGMessageImageViewOverlayView.m */; }; + D07BC9431F2A3E4400ED97AA /* TGSuggestionContext.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC9411F2A3E4400ED97AA /* TGSuggestionContext.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BC9441F2A3E4400ED97AA /* TGSuggestionContext.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC9421F2A3E4400ED97AA /* TGSuggestionContext.m */; }; + D07BC94E1F2A3EA900ED97AA /* TGHashtagPanelCell.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC9461F2A3EA900ED97AA /* TGHashtagPanelCell.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BC94F1F2A3EA900ED97AA /* TGHashtagPanelCell.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC9471F2A3EA900ED97AA /* TGHashtagPanelCell.m */; }; + D07BC9501F2A3EA900ED97AA /* TGMentionPanelCell.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC9481F2A3EA900ED97AA /* TGMentionPanelCell.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BC9511F2A3EA900ED97AA /* TGMentionPanelCell.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC9491F2A3EA900ED97AA /* TGMentionPanelCell.m */; }; + D07BC9521F2A3EA900ED97AA /* TGModernConversationHashtagsAssociatedPanel.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC94A1F2A3EA900ED97AA /* TGModernConversationHashtagsAssociatedPanel.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BC9531F2A3EA900ED97AA /* TGModernConversationHashtagsAssociatedPanel.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC94B1F2A3EA900ED97AA /* TGModernConversationHashtagsAssociatedPanel.m */; }; + D07BC9541F2A3EA900ED97AA /* TGModernConversationMentionsAssociatedPanel.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC94C1F2A3EA900ED97AA /* TGModernConversationMentionsAssociatedPanel.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BC9551F2A3EA900ED97AA /* TGModernConversationMentionsAssociatedPanel.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC94D1F2A3EA900ED97AA /* TGModernConversationMentionsAssociatedPanel.m */; }; + D07BC9581F2A3EBF00ED97AA /* TGModernConversationAssociatedInputPanel.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC9561F2A3EBF00ED97AA /* TGModernConversationAssociatedInputPanel.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BC9591F2A3EBF00ED97AA /* TGModernConversationAssociatedInputPanel.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC9571F2A3EBF00ED97AA /* TGModernConversationAssociatedInputPanel.m */; }; + D07BC95C1F2A3EF000ED97AA /* TGLetteredAvatarView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC95A1F2A3EF000ED97AA /* TGLetteredAvatarView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BC95D1F2A3EF000ED97AA /* TGLetteredAvatarView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC95B1F2A3EF000ED97AA /* TGLetteredAvatarView.m */; }; + D07BC9601F2A3F0A00ED97AA /* TGGradientLabel.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC95E1F2A3F0A00ED97AA /* TGGradientLabel.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BC9611F2A3F0A00ED97AA /* TGGradientLabel.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC95F1F2A3F0A00ED97AA /* TGGradientLabel.m */; }; + D07BC9641F2A3F4000ED97AA /* TGRemoteImageView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC9621F2A3F4000ED97AA /* TGRemoteImageView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BC9651F2A3F4000ED97AA /* TGRemoteImageView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC9631F2A3F4000ED97AA /* TGRemoteImageView.m */; }; + D07BC9681F2A3F5C00ED97AA /* TGCache.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC9661F2A3F5C00ED97AA /* TGCache.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BC9691F2A3F5C00ED97AA /* TGCache.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC9671F2A3F5C00ED97AA /* TGCache.m */; }; + D07BC96C1F2A43E300ED97AA /* TGPhotoStickersView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC96A1F2A43E300ED97AA /* TGPhotoStickersView.h */; }; + D07BC96D1F2A43E300ED97AA /* TGPhotoStickersView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC96B1F2A43E300ED97AA /* TGPhotoStickersView.m */; }; + D07BC9721F2A467D00ED97AA /* TGPhotoTextSettingsView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC96E1F2A467D00ED97AA /* TGPhotoTextSettingsView.h */; }; + D07BC9731F2A467D00ED97AA /* TGPhotoTextSettingsView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC96F1F2A467D00ED97AA /* TGPhotoTextSettingsView.m */; }; + D07BC9741F2A467D00ED97AA /* TGPhotoBrushSettingsView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC9701F2A467D00ED97AA /* TGPhotoBrushSettingsView.h */; }; + D07BC9751F2A467D00ED97AA /* TGPhotoBrushSettingsView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC9711F2A467D00ED97AA /* TGPhotoBrushSettingsView.m */; }; + D07BC9791F2A471000ED97AA /* TGStickerKeyboardTabPanel.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC9771F2A471000ED97AA /* TGStickerKeyboardTabPanel.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BC97A1F2A471000ED97AA /* TGStickerKeyboardTabPanel.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC9781F2A471000ED97AA /* TGStickerKeyboardTabPanel.m */; }; + D07BC9831F2A472900ED97AA /* TGPhotoStickersCollectionLayout.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC97B1F2A472900ED97AA /* TGPhotoStickersCollectionLayout.h */; }; + D07BC9841F2A472900ED97AA /* TGPhotoStickersCollectionLayout.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC97C1F2A472900ED97AA /* TGPhotoStickersCollectionLayout.m */; }; + D07BC9851F2A472900ED97AA /* TGPhotoStickersCollectionView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC97D1F2A472900ED97AA /* TGPhotoStickersCollectionView.h */; }; + D07BC9861F2A472900ED97AA /* TGPhotoStickersCollectionView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC97E1F2A472900ED97AA /* TGPhotoStickersCollectionView.m */; }; + D07BC9871F2A472900ED97AA /* TGPhotoStickersSectionHeader.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC97F1F2A472900ED97AA /* TGPhotoStickersSectionHeader.h */; }; + D07BC9881F2A472900ED97AA /* TGPhotoStickersSectionHeader.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC9801F2A472900ED97AA /* TGPhotoStickersSectionHeader.m */; }; + D07BC9891F2A472900ED97AA /* TGPhotoStickersSectionHeaderView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC9811F2A472900ED97AA /* TGPhotoStickersSectionHeaderView.h */; }; + D07BC98A1F2A472A00ED97AA /* TGPhotoStickersSectionHeaderView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC9821F2A472900ED97AA /* TGPhotoStickersSectionHeaderView.m */; }; + D07BC9911F2A480800ED97AA /* TGStickerKeyboardTabCell.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC98F1F2A480800ED97AA /* TGStickerKeyboardTabCell.h */; }; + D07BC9921F2A480800ED97AA /* TGStickerKeyboardTabCell.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC9901F2A480800ED97AA /* TGStickerKeyboardTabCell.m */; }; + D07BC9951F2A481C00ED97AA /* TGStickerKeyboardTabSettingsCell.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC9931F2A481C00ED97AA /* TGStickerKeyboardTabSettingsCell.h */; }; + D07BC9961F2A481C00ED97AA /* TGStickerKeyboardTabSettingsCell.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC9941F2A481C00ED97AA /* TGStickerKeyboardTabSettingsCell.m */; }; + D07BC9991F2A489C00ED97AA /* TGStickerPack.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC9971F2A489C00ED97AA /* TGStickerPack.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BC99A1F2A489C00ED97AA /* TGStickerPack.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC9981F2A489C00ED97AA /* TGStickerPack.m */; }; + D07BC99D1F2A494000ED97AA /* TGStickerCollectionViewCell.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC99B1F2A494000ED97AA /* TGStickerCollectionViewCell.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BC99E1F2A494000ED97AA /* TGStickerCollectionViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC99C1F2A494000ED97AA /* TGStickerCollectionViewCell.m */; }; + D07BC9A21F2A49C200ED97AA /* TGItemPreviewController.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC9A01F2A49C200ED97AA /* TGItemPreviewController.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BC9A31F2A49C200ED97AA /* TGItemPreviewController.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC9A11F2A49C200ED97AA /* TGItemPreviewController.m */; }; + D07BC9A61F2A49E300ED97AA /* TGItemPreviewView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC9A41F2A49E300ED97AA /* TGItemPreviewView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BC9A71F2A49E300ED97AA /* TGItemPreviewView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC9A51F2A49E300ED97AA /* TGItemPreviewView.m */; }; + D07BC9AA1F2A4A0700ED97AA /* TGStickerItemPreviewView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC9A81F2A4A0700ED97AA /* TGStickerItemPreviewView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BC9AB1F2A4A0700ED97AA /* TGStickerItemPreviewView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC9A91F2A4A0700ED97AA /* TGStickerItemPreviewView.m */; }; + D07BC9AE1F2A4A5100ED97AA /* TGItemMenuSheetPreviewView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC9AC1F2A4A5100ED97AA /* TGItemMenuSheetPreviewView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BC9AF1F2A4A5100ED97AA /* TGItemMenuSheetPreviewView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC9AD1F2A4A5100ED97AA /* TGItemMenuSheetPreviewView.m */; }; + D07BC9B21F2A4B6600ED97AA /* TGStickerAssociation.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC9B01F2A4B6600ED97AA /* TGStickerAssociation.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BC9B31F2A4B6600ED97AA /* TGStickerAssociation.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC9B11F2A4B6600ED97AA /* TGStickerAssociation.m */; }; + D07BC9B61F2A700900ED97AA /* TGPhotoMaskPosition.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC9B41F2A700900ED97AA /* TGPhotoMaskPosition.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BC9B71F2A700900ED97AA /* TGPhotoMaskPosition.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC9B51F2A700900ED97AA /* TGPhotoMaskPosition.m */; }; + D07BC9BA1F2A705D00ED97AA /* TGPhotoFilterCell.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC9B81F2A705D00ED97AA /* TGPhotoFilterCell.h */; }; + D07BC9BB1F2A705D00ED97AA /* TGPhotoFilterCell.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC9B91F2A705D00ED97AA /* TGPhotoFilterCell.m */; }; + D07BC9BE1F2A722400ED97AA /* TGHistogramView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC9BC1F2A722400ED97AA /* TGHistogramView.h */; }; + D07BC9BF1F2A722400ED97AA /* TGHistogramView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC9BD1F2A722400ED97AA /* TGHistogramView.m */; }; + D07BC9F11F2A9A2B00ED97AA /* TGMediaPickerCell.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC9C31F2A9A2B00ED97AA /* TGMediaPickerCell.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BC9F21F2A9A2B00ED97AA /* TGMediaPickerCell.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC9C41F2A9A2B00ED97AA /* TGMediaPickerCell.m */; }; + D07BC9F31F2A9A2B00ED97AA /* TGMediaPickerController.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC9C51F2A9A2B00ED97AA /* TGMediaPickerController.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BC9F41F2A9A2B00ED97AA /* TGMediaPickerController.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC9C61F2A9A2B00ED97AA /* TGMediaPickerController.m */; }; + D07BC9F51F2A9A2B00ED97AA /* TGMediaPickerGalleryGifItem.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC9C71F2A9A2B00ED97AA /* TGMediaPickerGalleryGifItem.h */; }; + D07BC9F61F2A9A2B00ED97AA /* TGMediaPickerGalleryGifItem.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC9C81F2A9A2B00ED97AA /* TGMediaPickerGalleryGifItem.m */; }; + D07BC9F71F2A9A2B00ED97AA /* TGMediaPickerGalleryGifItemView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC9C91F2A9A2B00ED97AA /* TGMediaPickerGalleryGifItemView.h */; }; + D07BC9F81F2A9A2B00ED97AA /* TGMediaPickerGalleryGifItemView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC9CA1F2A9A2B00ED97AA /* TGMediaPickerGalleryGifItemView.m */; }; + D07BC9F91F2A9A2B00ED97AA /* TGMediaPickerGalleryInterfaceView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC9CB1F2A9A2B00ED97AA /* TGMediaPickerGalleryInterfaceView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BC9FA1F2A9A2B00ED97AA /* TGMediaPickerGalleryInterfaceView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC9CC1F2A9A2B00ED97AA /* TGMediaPickerGalleryInterfaceView.m */; }; + D07BC9FB1F2A9A2B00ED97AA /* TGMediaPickerGalleryItem.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC9CD1F2A9A2B00ED97AA /* TGMediaPickerGalleryItem.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BC9FC1F2A9A2B00ED97AA /* TGMediaPickerGalleryItem.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC9CE1F2A9A2B00ED97AA /* TGMediaPickerGalleryItem.m */; }; + D07BC9FD1F2A9A2B00ED97AA /* TGMediaPickerGalleryModel.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC9CF1F2A9A2B00ED97AA /* TGMediaPickerGalleryModel.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BC9FE1F2A9A2B00ED97AA /* TGMediaPickerGalleryModel.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC9D01F2A9A2B00ED97AA /* TGMediaPickerGalleryModel.m */; }; + D07BC9FF1F2A9A2B00ED97AA /* TGMediaPickerGalleryPhotoItem.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC9D11F2A9A2B00ED97AA /* TGMediaPickerGalleryPhotoItem.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BCA001F2A9A2B00ED97AA /* TGMediaPickerGalleryPhotoItem.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC9D21F2A9A2B00ED97AA /* TGMediaPickerGalleryPhotoItem.m */; }; + D07BCA011F2A9A2B00ED97AA /* TGMediaPickerGalleryPhotoItemView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC9D31F2A9A2B00ED97AA /* TGMediaPickerGalleryPhotoItemView.h */; }; + D07BCA021F2A9A2B00ED97AA /* TGMediaPickerGalleryPhotoItemView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC9D41F2A9A2B00ED97AA /* TGMediaPickerGalleryPhotoItemView.m */; }; + D07BCA031F2A9A2B00ED97AA /* TGMediaPickerGallerySelectedItemsModel.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC9D51F2A9A2B00ED97AA /* TGMediaPickerGallerySelectedItemsModel.h */; }; + D07BCA041F2A9A2B00ED97AA /* TGMediaPickerGallerySelectedItemsModel.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC9D61F2A9A2B00ED97AA /* TGMediaPickerGallerySelectedItemsModel.m */; }; + D07BCA051F2A9A2B00ED97AA /* TGMediaPickerGalleryVideoItem.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC9D71F2A9A2B00ED97AA /* TGMediaPickerGalleryVideoItem.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BCA061F2A9A2B00ED97AA /* TGMediaPickerGalleryVideoItem.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC9D81F2A9A2B00ED97AA /* TGMediaPickerGalleryVideoItem.m */; }; + D07BCA071F2A9A2B00ED97AA /* TGMediaPickerGalleryVideoItemView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC9D91F2A9A2B00ED97AA /* TGMediaPickerGalleryVideoItemView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BCA081F2A9A2B00ED97AA /* TGMediaPickerGalleryVideoItemView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC9DA1F2A9A2B00ED97AA /* TGMediaPickerGalleryVideoItemView.m */; }; + D07BCA091F2A9A2B00ED97AA /* TGMediaPickerGalleryVideoScrubber.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC9DB1F2A9A2B00ED97AA /* TGMediaPickerGalleryVideoScrubber.h */; }; + D07BCA0A1F2A9A2B00ED97AA /* TGMediaPickerGalleryVideoScrubber.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC9DC1F2A9A2B00ED97AA /* TGMediaPickerGalleryVideoScrubber.m */; }; + D07BCA0B1F2A9A2B00ED97AA /* TGMediaPickerGalleryVideoScrubberThumbnailView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC9DD1F2A9A2B00ED97AA /* TGMediaPickerGalleryVideoScrubberThumbnailView.h */; }; + D07BCA0C1F2A9A2B00ED97AA /* TGMediaPickerGalleryVideoScrubberThumbnailView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC9DE1F2A9A2B00ED97AA /* TGMediaPickerGalleryVideoScrubberThumbnailView.m */; }; + D07BCA0D1F2A9A2B00ED97AA /* TGMediaPickerGalleryVideoTrimView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC9DF1F2A9A2B00ED97AA /* TGMediaPickerGalleryVideoTrimView.h */; }; + D07BCA0E1F2A9A2B00ED97AA /* TGMediaPickerGalleryVideoTrimView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC9E01F2A9A2B00ED97AA /* TGMediaPickerGalleryVideoTrimView.m */; }; + D07BCA0F1F2A9A2B00ED97AA /* TGMediaPickerLayoutMetrics.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC9E11F2A9A2B00ED97AA /* TGMediaPickerLayoutMetrics.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BCA101F2A9A2B00ED97AA /* TGMediaPickerLayoutMetrics.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC9E21F2A9A2B00ED97AA /* TGMediaPickerLayoutMetrics.m */; }; + D07BCA111F2A9A2B00ED97AA /* TGMediaPickerModernGalleryMixin.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC9E31F2A9A2B00ED97AA /* TGMediaPickerModernGalleryMixin.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BCA121F2A9A2B00ED97AA /* TGMediaPickerModernGalleryMixin.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC9E41F2A9A2B00ED97AA /* TGMediaPickerModernGalleryMixin.m */; }; + D07BCA131F2A9A2B00ED97AA /* TGMediaPickerPhotoCounterButton.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC9E51F2A9A2B00ED97AA /* TGMediaPickerPhotoCounterButton.h */; }; + D07BCA141F2A9A2B00ED97AA /* TGMediaPickerPhotoCounterButton.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC9E61F2A9A2B00ED97AA /* TGMediaPickerPhotoCounterButton.m */; }; + D07BCA151F2A9A2B00ED97AA /* TGMediaPickerPhotoStripCell.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC9E71F2A9A2B00ED97AA /* TGMediaPickerPhotoStripCell.h */; }; + D07BCA161F2A9A2B00ED97AA /* TGMediaPickerPhotoStripCell.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC9E81F2A9A2B00ED97AA /* TGMediaPickerPhotoStripCell.m */; }; + D07BCA171F2A9A2B00ED97AA /* TGMediaPickerPhotoStripView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC9E91F2A9A2B00ED97AA /* TGMediaPickerPhotoStripView.h */; }; + D07BCA181F2A9A2B00ED97AA /* TGMediaPickerPhotoStripView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC9EA1F2A9A2B00ED97AA /* TGMediaPickerPhotoStripView.m */; }; + D07BCA191F2A9A2B00ED97AA /* TGMediaPickerScrubberHeaderView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC9EB1F2A9A2B00ED97AA /* TGMediaPickerScrubberHeaderView.h */; }; + D07BCA1A1F2A9A2B00ED97AA /* TGMediaPickerScrubberHeaderView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC9EC1F2A9A2B00ED97AA /* TGMediaPickerScrubberHeaderView.m */; }; + D07BCA1B1F2A9A2B00ED97AA /* TGMediaPickerSelectionGestureRecognizer.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC9ED1F2A9A2B00ED97AA /* TGMediaPickerSelectionGestureRecognizer.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BCA1C1F2A9A2B00ED97AA /* TGMediaPickerSelectionGestureRecognizer.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC9EE1F2A9A2B00ED97AA /* TGMediaPickerSelectionGestureRecognizer.m */; }; + D07BCA1D1F2A9A2B00ED97AA /* TGMediaPickerToolbarView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BC9EF1F2A9A2B00ED97AA /* TGMediaPickerToolbarView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BCA1E1F2A9A2B00ED97AA /* TGMediaPickerToolbarView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BC9F01F2A9A2B00ED97AA /* TGMediaPickerToolbarView.m */; }; + D07BCA211F2A9A5300ED97AA /* TGCheckButtonView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCA1F1F2A9A5300ED97AA /* TGCheckButtonView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BCA221F2A9A5300ED97AA /* TGCheckButtonView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCA201F2A9A5300ED97AA /* TGCheckButtonView.m */; }; + D07BCA291F2A9A9600ED97AA /* TGModernGalleryImageItem.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCA231F2A9A9600ED97AA /* TGModernGalleryImageItem.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BCA2A1F2A9A9600ED97AA /* TGModernGalleryImageItem.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCA241F2A9A9600ED97AA /* TGModernGalleryImageItem.m */; }; + D07BCA2B1F2A9A9600ED97AA /* TGModernGalleryImageItemImageView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCA251F2A9A9600ED97AA /* TGModernGalleryImageItemImageView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BCA2C1F2A9A9600ED97AA /* TGModernGalleryImageItemImageView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCA261F2A9A9600ED97AA /* TGModernGalleryImageItemImageView.m */; }; + D07BCA2D1F2A9A9600ED97AA /* TGModernGalleryImageItemView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCA271F2A9A9600ED97AA /* TGModernGalleryImageItemView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BCA2E1F2A9A9600ED97AA /* TGModernGalleryImageItemView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCA281F2A9A9600ED97AA /* TGModernGalleryImageItemView.m */; }; + D07BCA301F2A9AE700ED97AA /* TGModernGallerySelectableItem.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCA2F1F2A9AE700ED97AA /* TGModernGallerySelectableItem.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BCA331F2A9B0400ED97AA /* TGModernGalleryEditableItem.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCA311F2A9B0400ED97AA /* TGModernGalleryEditableItem.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BCA341F2A9B0400ED97AA /* TGModernGalleryEditableItemView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCA321F2A9B0400ED97AA /* TGModernGalleryEditableItemView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BCA371F2A9B6A00ED97AA /* TGMediaAsset+TGMediaEditableItem.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCA351F2A9B6A00ED97AA /* TGMediaAsset+TGMediaEditableItem.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BCA381F2A9B6A00ED97AA /* TGMediaAsset+TGMediaEditableItem.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCA361F2A9B6A00ED97AA /* TGMediaAsset+TGMediaEditableItem.m */; }; + D07BCA3B1F2A9BE600ED97AA /* TGModernGalleryVideoContentView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCA391F2A9BE600ED97AA /* TGModernGalleryVideoContentView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BCA3C1F2A9BE600ED97AA /* TGModernGalleryVideoContentView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCA3A1F2A9BE600ED97AA /* TGModernGalleryVideoContentView.m */; }; + D07BCA421F2A9C6600ED97AA /* TGModernMediaListItem.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCA3D1F2A9C6600ED97AA /* TGModernMediaListItem.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BCA431F2A9C6600ED97AA /* TGModernMediaListItemContentView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCA3E1F2A9C6600ED97AA /* TGModernMediaListItemContentView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BCA441F2A9C6600ED97AA /* TGModernMediaListItemContentView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCA3F1F2A9C6600ED97AA /* TGModernMediaListItemContentView.m */; }; + D07BCA451F2A9C6600ED97AA /* TGModernMediaListItemView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCA401F2A9C6600ED97AA /* TGModernMediaListItemView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BCA461F2A9C6600ED97AA /* TGModernMediaListItemView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCA411F2A9C6600ED97AA /* TGModernMediaListItemView.m */; }; + D07BCA481F2A9CE300ED97AA /* TGModernMediaListSelectableItem.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCA471F2A9CE300ED97AA /* TGModernMediaListSelectableItem.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BCA4B1F2A9DAB00ED97AA /* TGModernAnimatedImagePlayer.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCA491F2A9DAB00ED97AA /* TGModernAnimatedImagePlayer.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BCA4C1F2A9DAB00ED97AA /* TGModernAnimatedImagePlayer.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCA4A1F2A9DAB00ED97AA /* TGModernAnimatedImagePlayer.m */; }; + D07BCA4F1F2A9DDD00ED97AA /* FLAnimatedImage.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCA4D1F2A9DDD00ED97AA /* FLAnimatedImage.h */; }; + D07BCA501F2A9DDD00ED97AA /* FLAnimatedImage.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCA4E1F2A9DDD00ED97AA /* FLAnimatedImage.m */; }; + D07BCA551F2A9E1600ED97AA /* TGDraggableCollectionView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCA511F2A9E1600ED97AA /* TGDraggableCollectionView.h */; }; + D07BCA561F2A9E1600ED97AA /* TGDraggableCollectionView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCA521F2A9E1600ED97AA /* TGDraggableCollectionView.m */; }; + D07BCA571F2A9E1600ED97AA /* TGDraggableCollectionViewFlowLayout.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCA531F2A9E1600ED97AA /* TGDraggableCollectionViewFlowLayout.h */; }; + D07BCA581F2A9E1600ED97AA /* TGDraggableCollectionViewFlowLayout.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCA541F2A9E1600ED97AA /* TGDraggableCollectionViewFlowLayout.m */; }; + D07BCA6B1F2B3CE700ED97AA /* TGCameraController.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCA671F2B3CE700ED97AA /* TGCameraController.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BCA6C1F2B3CE700ED97AA /* TGCameraController.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCA681F2B3CE700ED97AA /* TGCameraController.m */; }; + D07BCA6D1F2B3CE700ED97AA /* TGCameraFocusCrosshairsControl.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCA691F2B3CE700ED97AA /* TGCameraFocusCrosshairsControl.h */; }; + D07BCA6E1F2B3CE700ED97AA /* TGCameraFocusCrosshairsControl.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCA6A1F2B3CE700ED97AA /* TGCameraFocusCrosshairsControl.m */; }; + D07BCA881F2B443700ED97AA /* TGMediaAssetsController.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCA701F2B443700ED97AA /* TGMediaAssetsController.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BCA891F2B443700ED97AA /* TGMediaAssetsController.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCA711F2B443700ED97AA /* TGMediaAssetsController.m */; }; + D07BCA8A1F2B443700ED97AA /* TGMediaAssetsGifCell.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCA721F2B443700ED97AA /* TGMediaAssetsGifCell.h */; }; + D07BCA8B1F2B443700ED97AA /* TGMediaAssetsGifCell.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCA731F2B443700ED97AA /* TGMediaAssetsGifCell.m */; }; + D07BCA8C1F2B443700ED97AA /* TGMediaAssetsMomentsCollectionLayout.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCA741F2B443700ED97AA /* TGMediaAssetsMomentsCollectionLayout.h */; }; + D07BCA8D1F2B443700ED97AA /* TGMediaAssetsMomentsCollectionLayout.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCA751F2B443700ED97AA /* TGMediaAssetsMomentsCollectionLayout.m */; }; + D07BCA8E1F2B443700ED97AA /* TGMediaAssetsMomentsCollectionView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCA761F2B443700ED97AA /* TGMediaAssetsMomentsCollectionView.h */; }; + D07BCA8F1F2B443700ED97AA /* TGMediaAssetsMomentsCollectionView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCA771F2B443700ED97AA /* TGMediaAssetsMomentsCollectionView.m */; }; + D07BCA901F2B443700ED97AA /* TGMediaAssetsMomentsController.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCA781F2B443700ED97AA /* TGMediaAssetsMomentsController.h */; }; + D07BCA911F2B443700ED97AA /* TGMediaAssetsMomentsController.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCA791F2B443700ED97AA /* TGMediaAssetsMomentsController.m */; }; + D07BCA921F2B443700ED97AA /* TGMediaAssetsMomentsSectionHeader.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCA7A1F2B443700ED97AA /* TGMediaAssetsMomentsSectionHeader.h */; }; + D07BCA931F2B443700ED97AA /* TGMediaAssetsMomentsSectionHeader.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCA7B1F2B443700ED97AA /* TGMediaAssetsMomentsSectionHeader.m */; }; + D07BCA941F2B443700ED97AA /* TGMediaAssetsMomentsSectionHeaderView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCA7C1F2B443700ED97AA /* TGMediaAssetsMomentsSectionHeaderView.h */; }; + D07BCA951F2B443700ED97AA /* TGMediaAssetsMomentsSectionHeaderView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCA7D1F2B443700ED97AA /* TGMediaAssetsMomentsSectionHeaderView.m */; }; + D07BCA961F2B443700ED97AA /* TGMediaAssetsPhotoCell.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCA7E1F2B443700ED97AA /* TGMediaAssetsPhotoCell.h */; }; + D07BCA971F2B443700ED97AA /* TGMediaAssetsPhotoCell.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCA7F1F2B443700ED97AA /* TGMediaAssetsPhotoCell.m */; }; + D07BCA981F2B443700ED97AA /* TGMediaAssetsPickerController.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCA801F2B443700ED97AA /* TGMediaAssetsPickerController.h */; }; + D07BCA991F2B443700ED97AA /* TGMediaAssetsPickerController.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCA811F2B443700ED97AA /* TGMediaAssetsPickerController.m */; }; + D07BCA9A1F2B443700ED97AA /* TGMediaAssetsTipView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCA821F2B443700ED97AA /* TGMediaAssetsTipView.h */; }; + D07BCA9B1F2B443700ED97AA /* TGMediaAssetsTipView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCA831F2B443700ED97AA /* TGMediaAssetsTipView.m */; }; + D07BCA9C1F2B443700ED97AA /* TGMediaAssetsUtils.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCA841F2B443700ED97AA /* TGMediaAssetsUtils.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BCA9D1F2B443700ED97AA /* TGMediaAssetsUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCA851F2B443700ED97AA /* TGMediaAssetsUtils.m */; }; + D07BCA9E1F2B443700ED97AA /* TGMediaAssetsVideoCell.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCA861F2B443700ED97AA /* TGMediaAssetsVideoCell.h */; }; + D07BCA9F1F2B443700ED97AA /* TGMediaAssetsVideoCell.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCA871F2B443700ED97AA /* TGMediaAssetsVideoCell.m */; }; + D07BCAA41F2B445E00ED97AA /* TGMediaGroupCell.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCAA01F2B445E00ED97AA /* TGMediaGroupCell.h */; }; + D07BCAA51F2B445E00ED97AA /* TGMediaGroupCell.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCAA11F2B445E00ED97AA /* TGMediaGroupCell.m */; }; + D07BCAA61F2B445E00ED97AA /* TGMediaGroupsController.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCAA21F2B445E00ED97AA /* TGMediaGroupsController.h */; }; + D07BCAA71F2B445E00ED97AA /* TGMediaGroupsController.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCAA31F2B445E00ED97AA /* TGMediaGroupsController.m */; }; + D07BCAAA1F2B44C100ED97AA /* TGModernBarButton.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCAA81F2B44C100ED97AA /* TGModernBarButton.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BCAAB1F2B44C100ED97AA /* TGModernBarButton.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCAA91F2B44C100ED97AA /* TGModernBarButton.m */; }; + D07BCAAE1F2B45DA00ED97AA /* TGFileUtils.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCAAC1F2B45DA00ED97AA /* TGFileUtils.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BCAAF1F2B45DA00ED97AA /* TGFileUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCAAD1F2B45DA00ED97AA /* TGFileUtils.m */; }; + D07BCAB21F2B460B00ED97AA /* TGGifConverter.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCAB01F2B460B00ED97AA /* TGGifConverter.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BCAB31F2B460B00ED97AA /* TGGifConverter.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCAB11F2B460B00ED97AA /* TGGifConverter.m */; }; + D07BCAB71F2B4DE200ED97AA /* TGAttachmentCarouselItemView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCAB51F2B4DE200ED97AA /* TGAttachmentCarouselItemView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BCAB81F2B4DE200ED97AA /* TGAttachmentCarouselItemView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCAB61F2B4DE200ED97AA /* TGAttachmentCarouselItemView.m */; }; + D07BCABB1F2B4E2600ED97AA /* TGTransitionLayout.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCAB91F2B4E2600ED97AA /* TGTransitionLayout.h */; }; + D07BCABC1F2B4E2600ED97AA /* TGTransitionLayout.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCABA1F2B4E2600ED97AA /* TGTransitionLayout.m */; }; + D07BCABF1F2B4E3900ED97AA /* UICollectionView+TGTransitioning.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCABD1F2B4E3900ED97AA /* UICollectionView+TGTransitioning.h */; }; + D07BCAC01F2B4E3900ED97AA /* UICollectionView+TGTransitioning.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCABE1F2B4E3900ED97AA /* UICollectionView+TGTransitioning.m */; }; + D07BCAC61F2B4E6200ED97AA /* TGAttachmentCameraCell.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCAC11F2B4E6200ED97AA /* TGAttachmentCameraCell.h */; }; + D07BCAC71F2B4E6200ED97AA /* TGAttachmentCameraCell.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCAC21F2B4E6200ED97AA /* TGAttachmentCameraCell.m */; }; + D07BCAC81F2B4E6200ED97AA /* TGAttachmentCameraView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCAC31F2B4E6200ED97AA /* TGAttachmentCameraView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BCAC91F2B4E6200ED97AA /* TGAttachmentCameraView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCAC41F2B4E6200ED97AA /* TGAttachmentCameraView.m */; }; + D07BCACA1F2B4E6200ED97AA /* TGMediaAvatarMenuMixin.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCAC51F2B4E6200ED97AA /* TGMediaAvatarMenuMixin.m */; }; + D07BCACC1F2B4E7300ED97AA /* TGMediaAvatarMenuMixin.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCACB1F2B4E7300ED97AA /* TGMediaAvatarMenuMixin.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BCACF1F2B4E9000ED97AA /* TGAttachmentMenuCell.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCACD1F2B4E9000ED97AA /* TGAttachmentMenuCell.h */; }; + D07BCAD01F2B4E9000ED97AA /* TGAttachmentMenuCell.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCACE1F2B4E9000ED97AA /* TGAttachmentMenuCell.m */; }; + D07BCAD91F2B4F2800ED97AA /* TGOverlayFormsheetController.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCAD51F2B4F2800ED97AA /* TGOverlayFormsheetController.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BCADA1F2B4F2800ED97AA /* TGOverlayFormsheetController.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCAD61F2B4F2800ED97AA /* TGOverlayFormsheetController.m */; }; + D07BCADB1F2B4F2800ED97AA /* TGOverlayFormsheetWindow.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCAD71F2B4F2800ED97AA /* TGOverlayFormsheetWindow.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BCADC1F2B4F2800ED97AA /* TGOverlayFormsheetWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCAD81F2B4F2800ED97AA /* TGOverlayFormsheetWindow.m */; }; + D07BCADF1F2B4F5E00ED97AA /* TGLegacyCameraController.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCADD1F2B4F5E00ED97AA /* TGLegacyCameraController.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BCAE01F2B4F5E00ED97AA /* TGLegacyCameraController.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCADE1F2B4F5E00ED97AA /* TGLegacyCameraController.m */; }; + D07BCAE31F2B502F00ED97AA /* TGImagePickerController.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCAE11F2B502F00ED97AA /* TGImagePickerController.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BCAE41F2B502F00ED97AA /* TGImagePickerController.mm in Sources */ = {isa = PBXBuildFile; fileRef = D07BCAE21F2B502F00ED97AA /* TGImagePickerController.mm */; }; + D07BCAEB1F2B507600ED97AA /* TGAttachmentGifCell.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCAE51F2B507600ED97AA /* TGAttachmentGifCell.h */; }; + D07BCAEC1F2B507600ED97AA /* TGAttachmentGifCell.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCAE61F2B507600ED97AA /* TGAttachmentGifCell.m */; }; + D07BCAED1F2B507600ED97AA /* TGAttachmentVideoCell.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCAE71F2B507600ED97AA /* TGAttachmentVideoCell.h */; }; + D07BCAEE1F2B507600ED97AA /* TGAttachmentVideoCell.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCAE81F2B507600ED97AA /* TGAttachmentVideoCell.m */; }; + D07BCAEF1F2B507600ED97AA /* TGAttachmentPhotoCell.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCAE91F2B507600ED97AA /* TGAttachmentPhotoCell.h */; }; + D07BCAF01F2B507600ED97AA /* TGAttachmentPhotoCell.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCAEA1F2B507600ED97AA /* TGAttachmentPhotoCell.m */; }; + D07BCAF31F2B509200ED97AA /* TGAttachmentAssetCell.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCAF11F2B509200ED97AA /* TGAttachmentAssetCell.h */; }; + D07BCAF41F2B509200ED97AA /* TGAttachmentAssetCell.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCAF21F2B509200ED97AA /* TGAttachmentAssetCell.m */; }; + D07BCAF71F2B50AA00ED97AA /* TGMediaAvatarEditorTransition.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCAF51F2B50AA00ED97AA /* TGMediaAvatarEditorTransition.h */; }; + D07BCAF81F2B50AA00ED97AA /* TGMediaAvatarEditorTransition.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCAF61F2B50AA00ED97AA /* TGMediaAvatarEditorTransition.m */; }; + D07BCAFB1F2B517900ED97AA /* TGLegacyMediaPickerTipView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCAF91F2B517900ED97AA /* TGLegacyMediaPickerTipView.h */; }; + D07BCAFC1F2B517900ED97AA /* TGLegacyMediaPickerTipView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCAFA1F2B517900ED97AA /* TGLegacyMediaPickerTipView.m */; }; + D07BCB021F2B546400ED97AA /* LegacyComponentsContext.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCB011F2B546400ED97AA /* LegacyComponentsContext.m */; }; + D07BCB151F2B646A00ED97AA /* TGPasswordEntryView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCB041F2B646A00ED97AA /* TGPasswordEntryView.h */; }; + D07BCB161F2B646A00ED97AA /* TGPasswordEntryView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCB051F2B646A00ED97AA /* TGPasswordEntryView.m */; }; + D07BCB171F2B646A00ED97AA /* TGPasscodeEntryController.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCB061F2B646A00ED97AA /* TGPasscodeEntryController.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BCB181F2B646A00ED97AA /* TGPasscodeEntryController.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCB071F2B646A00ED97AA /* TGPasscodeEntryController.m */; }; + D07BCB191F2B646A00ED97AA /* TGPasscodeEntryKeyboardView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCB081F2B646A00ED97AA /* TGPasscodeEntryKeyboardView.h */; }; + D07BCB1A1F2B646A00ED97AA /* TGPasscodeEntryKeyboardView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCB091F2B646A00ED97AA /* TGPasscodeEntryKeyboardView.m */; }; + D07BCB1B1F2B646A00ED97AA /* TGPasscodePinDotView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCB0A1F2B646A00ED97AA /* TGPasscodePinDotView.h */; }; + D07BCB1C1F2B646A00ED97AA /* TGPasscodePinDotView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCB0B1F2B646A00ED97AA /* TGPasscodePinDotView.m */; }; + D07BCB1D1F2B646A00ED97AA /* TGPasscodePinView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCB0C1F2B646A00ED97AA /* TGPasscodePinView.h */; }; + D07BCB1E1F2B646A00ED97AA /* TGPasscodePinView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCB0D1F2B646A00ED97AA /* TGPasscodePinView.m */; }; + D07BCB1F1F2B646A00ED97AA /* TGPasscodeButtonView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCB0E1F2B646A00ED97AA /* TGPasscodeButtonView.h */; }; + D07BCB201F2B646A00ED97AA /* TGPasscodeButtonView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCB0F1F2B646A00ED97AA /* TGPasscodeButtonView.m */; }; + D07BCB211F2B646A00ED97AA /* TGPasscodeBackground.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCB101F2B646A00ED97AA /* TGPasscodeBackground.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BCB221F2B646A00ED97AA /* TGTextField.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCB111F2B646A00ED97AA /* TGTextField.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BCB231F2B646A00ED97AA /* TGTextField.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCB121F2B646A00ED97AA /* TGTextField.m */; }; + D07BCB241F2B646A00ED97AA /* TGDefaultPasscodeBackground.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCB131F2B646A00ED97AA /* TGDefaultPasscodeBackground.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BCB251F2B646A00ED97AA /* TGDefaultPasscodeBackground.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCB141F2B646A00ED97AA /* TGDefaultPasscodeBackground.m */; }; + D07BCB281F2B652C00ED97AA /* TGImageBasedPasscodeBackground.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCB261F2B652C00ED97AA /* TGImageBasedPasscodeBackground.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BCB291F2B652C00ED97AA /* TGImageBasedPasscodeBackground.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCB271F2B652C00ED97AA /* TGImageBasedPasscodeBackground.m */; }; + D07BCB351F2B65F100ED97AA /* TGBuiltinWallpaperInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCB2B1F2B65F100ED97AA /* TGBuiltinWallpaperInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BCB361F2B65F100ED97AA /* TGBuiltinWallpaperInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCB2C1F2B65F100ED97AA /* TGBuiltinWallpaperInfo.m */; }; + D07BCB371F2B65F100ED97AA /* TGColorWallpaperInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCB2D1F2B65F100ED97AA /* TGColorWallpaperInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BCB381F2B65F100ED97AA /* TGColorWallpaperInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCB2E1F2B65F100ED97AA /* TGColorWallpaperInfo.m */; }; + D07BCB391F2B65F100ED97AA /* TGCustomImageWallpaperInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCB2F1F2B65F100ED97AA /* TGCustomImageWallpaperInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BCB3A1F2B65F100ED97AA /* TGCustomImageWallpaperInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCB301F2B65F100ED97AA /* TGCustomImageWallpaperInfo.m */; }; + D07BCB3B1F2B65F100ED97AA /* TGRemoteWallpaperInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCB311F2B65F100ED97AA /* TGRemoteWallpaperInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BCB3C1F2B65F100ED97AA /* TGRemoteWallpaperInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCB321F2B65F100ED97AA /* TGRemoteWallpaperInfo.m */; }; + D07BCB3D1F2B65F100ED97AA /* TGWallpaperInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCB331F2B65F100ED97AA /* TGWallpaperInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BCB3E1F2B65F100ED97AA /* TGWallpaperInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCB341F2B65F100ED97AA /* TGWallpaperInfo.m */; }; + D07BCB5B1F2B6A5600ED97AA /* TGPIPAblePlayerView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCB401F2B6A5600ED97AA /* TGPIPAblePlayerView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BCB5C1F2B6A5600ED97AA /* TGEmbedInstagramPlayerView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCB411F2B6A5600ED97AA /* TGEmbedInstagramPlayerView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BCB5D1F2B6A5600ED97AA /* TGEmbedInstagramPlayerView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCB421F2B6A5600ED97AA /* TGEmbedInstagramPlayerView.m */; }; + D07BCB5E1F2B6A5600ED97AA /* TGEmbedPIPButton.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCB431F2B6A5600ED97AA /* TGEmbedPIPButton.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BCB5F1F2B6A5600ED97AA /* TGEmbedPIPButton.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCB441F2B6A5600ED97AA /* TGEmbedPIPButton.m */; }; + D07BCB601F2B6A5600ED97AA /* TGEmbedPIPPullArrowView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCB451F2B6A5600ED97AA /* TGEmbedPIPPullArrowView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BCB611F2B6A5600ED97AA /* TGEmbedPIPPullArrowView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCB461F2B6A5600ED97AA /* TGEmbedPIPPullArrowView.m */; }; + D07BCB621F2B6A5600ED97AA /* TGEmbedPlayerControls.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCB471F2B6A5600ED97AA /* TGEmbedPlayerControls.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BCB631F2B6A5600ED97AA /* TGEmbedPlayerControls.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCB481F2B6A5600ED97AA /* TGEmbedPlayerControls.m */; }; + D07BCB641F2B6A5600ED97AA /* TGEmbedPlayerScrubber.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCB491F2B6A5600ED97AA /* TGEmbedPlayerScrubber.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BCB651F2B6A5600ED97AA /* TGEmbedPlayerScrubber.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCB4A1F2B6A5600ED97AA /* TGEmbedPlayerScrubber.m */; }; + D07BCB661F2B6A5600ED97AA /* TGEmbedPlayerState.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCB4B1F2B6A5600ED97AA /* TGEmbedPlayerState.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BCB671F2B6A5600ED97AA /* TGEmbedPlayerState.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCB4C1F2B6A5600ED97AA /* TGEmbedPlayerState.m */; }; + D07BCB681F2B6A5600ED97AA /* TGEmbedPlayerView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCB4D1F2B6A5600ED97AA /* TGEmbedPlayerView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BCB691F2B6A5600ED97AA /* TGEmbedPlayerView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCB4E1F2B6A5600ED97AA /* TGEmbedPlayerView.m */; }; + D07BCB6A1F2B6A5600ED97AA /* TGEmbedSoundCloudPlayerView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCB4F1F2B6A5600ED97AA /* TGEmbedSoundCloudPlayerView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BCB6B1F2B6A5600ED97AA /* TGEmbedSoundCloudPlayerView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCB501F2B6A5600ED97AA /* TGEmbedSoundCloudPlayerView.m */; }; + D07BCB6C1F2B6A5600ED97AA /* TGEmbedVideoPlayerView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCB511F2B6A5600ED97AA /* TGEmbedVideoPlayerView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BCB6D1F2B6A5600ED97AA /* TGEmbedVideoPlayerView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCB521F2B6A5600ED97AA /* TGEmbedVideoPlayerView.m */; }; + D07BCB6E1F2B6A5600ED97AA /* TGEmbedVimeoPlayerView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCB531F2B6A5600ED97AA /* TGEmbedVimeoPlayerView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BCB6F1F2B6A5600ED97AA /* TGEmbedVimeoPlayerView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCB541F2B6A5600ED97AA /* TGEmbedVimeoPlayerView.m */; }; + D07BCB701F2B6A5600ED97AA /* TGEmbedVinePlayerView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCB551F2B6A5600ED97AA /* TGEmbedVinePlayerView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BCB711F2B6A5600ED97AA /* TGEmbedVinePlayerView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCB561F2B6A5600ED97AA /* TGEmbedVinePlayerView.m */; }; + D07BCB721F2B6A5600ED97AA /* TGEmbedVKPlayerView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCB571F2B6A5600ED97AA /* TGEmbedVKPlayerView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BCB731F2B6A5600ED97AA /* TGEmbedVKPlayerView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCB581F2B6A5600ED97AA /* TGEmbedVKPlayerView.m */; }; + D07BCB741F2B6A5600ED97AA /* TGEmbedYoutubePlayerView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCB591F2B6A5600ED97AA /* TGEmbedYoutubePlayerView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BCB751F2B6A5600ED97AA /* TGEmbedYoutubePlayerView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCB5A1F2B6A5600ED97AA /* TGEmbedYoutubePlayerView.m */; }; + D07BCB781F2B6DB900ED97AA /* TGEmbedCoubPlayerView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCB761F2B6DB900ED97AA /* TGEmbedCoubPlayerView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BCB791F2B6DB900ED97AA /* TGEmbedCoubPlayerView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCB771F2B6DB900ED97AA /* TGEmbedCoubPlayerView.m */; }; + D07BCBA61F2B6F6300ED97AA /* AVAsset+CBExtension.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCB7B1F2B6F6300ED97AA /* AVAsset+CBExtension.h */; }; + D07BCBA71F2B6F6300ED97AA /* AVAsset+CBExtension.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCB7C1F2B6F6300ED97AA /* AVAsset+CBExtension.m */; }; + D07BCBA81F2B6F6300ED97AA /* CBAssetDownloadManager.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCB7D1F2B6F6300ED97AA /* CBAssetDownloadManager.h */; }; + D07BCBA91F2B6F6300ED97AA /* CBAssetDownloadManager.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCB7E1F2B6F6300ED97AA /* CBAssetDownloadManager.m */; }; + D07BCBAA1F2B6F6300ED97AA /* CBChunkDownloadOperation.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCB7F1F2B6F6300ED97AA /* CBChunkDownloadOperation.h */; }; + D07BCBAB1F2B6F6300ED97AA /* CBChunkDownloadOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCB801F2B6F6300ED97AA /* CBChunkDownloadOperation.m */; }; + D07BCBAC1F2B6F6300ED97AA /* CBConstance.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCB811F2B6F6300ED97AA /* CBConstance.h */; }; + D07BCBAD1F2B6F6300ED97AA /* CBConstance.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCB821F2B6F6300ED97AA /* CBConstance.m */; }; + D07BCBAE1F2B6F6300ED97AA /* CBCoubAsset.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCB831F2B6F6300ED97AA /* CBCoubAsset.h */; }; + D07BCBAF1F2B6F6300ED97AA /* CBCoubAudioSource.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCB841F2B6F6300ED97AA /* CBCoubAudioSource.h */; }; + D07BCBB01F2B6F6300ED97AA /* CBCoubAudioSource.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCB851F2B6F6300ED97AA /* CBCoubAudioSource.m */; }; + D07BCBB11F2B6F6300ED97AA /* CBCoubAuthorVO.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCB861F2B6F6300ED97AA /* CBCoubAuthorVO.h */; }; + D07BCBB21F2B6F6300ED97AA /* CBCoubAuthorVO.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCB871F2B6F6300ED97AA /* CBCoubAuthorVO.m */; }; + D07BCBB31F2B6F6300ED97AA /* CBCoubDownloadOperation.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCB881F2B6F6300ED97AA /* CBCoubDownloadOperation.h */; }; + D07BCBB41F2B6F6300ED97AA /* CBCoubDownloadOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCB891F2B6F6300ED97AA /* CBCoubDownloadOperation.m */; }; + D07BCBB51F2B6F6300ED97AA /* CBCoubLoopCompositionMaker.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCB8A1F2B6F6300ED97AA /* CBCoubLoopCompositionMaker.h */; }; + D07BCBB61F2B6F6300ED97AA /* CBCoubLoopCompositionMaker.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCB8B1F2B6F6300ED97AA /* CBCoubLoopCompositionMaker.m */; }; + D07BCBB71F2B6F6300ED97AA /* CBCoubNew.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCB8C1F2B6F6300ED97AA /* CBCoubNew.h */; }; + D07BCBB81F2B6F6300ED97AA /* CBCoubNew.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCB8D1F2B6F6300ED97AA /* CBCoubNew.m */; }; + D07BCBB91F2B6F6300ED97AA /* CBCoubPlayer.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCB8E1F2B6F6300ED97AA /* CBCoubPlayer.h */; }; + D07BCBBA1F2B6F6300ED97AA /* CBCoubPlayer.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCB8F1F2B6F6300ED97AA /* CBCoubPlayer.m */; }; + D07BCBBB1F2B6F6300ED97AA /* CBCoubPlayerContance.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCB901F2B6F6300ED97AA /* CBCoubPlayerContance.h */; }; + D07BCBBC1F2B6F6300ED97AA /* CBCoubPlayerContance.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCB911F2B6F6300ED97AA /* CBCoubPlayerContance.m */; }; + D07BCBBD1F2B6F6300ED97AA /* CBCoubVideoSource.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCB921F2B6F6300ED97AA /* CBCoubVideoSource.h */; }; + D07BCBBE1F2B6F6300ED97AA /* CBCoubVideoSource.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCB931F2B6F6300ED97AA /* CBCoubVideoSource.m */; }; + D07BCBBF1F2B6F6300ED97AA /* CBDownloadOperation.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCB941F2B6F6300ED97AA /* CBDownloadOperation.h */; }; + D07BCBC01F2B6F6300ED97AA /* CBDownloadOperationDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCB951F2B6F6300ED97AA /* CBDownloadOperationDelegate.h */; }; + D07BCBC11F2B6F6300ED97AA /* CBGenericDownloadOperation.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCB961F2B6F6300ED97AA /* CBGenericDownloadOperation.h */; }; + D07BCBC21F2B6F6300ED97AA /* CBGenericDownloadOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCB971F2B6F6300ED97AA /* CBGenericDownloadOperation.m */; }; + D07BCBC31F2B6F6300ED97AA /* CBJSONCoubMapper.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCB981F2B6F6300ED97AA /* CBJSONCoubMapper.h */; }; + D07BCBC41F2B6F6300ED97AA /* CBJSONCoubMapper.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCB991F2B6F6300ED97AA /* CBJSONCoubMapper.m */; }; + D07BCBC51F2B6F6300ED97AA /* CBLibrary.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCB9A1F2B6F6300ED97AA /* CBLibrary.h */; }; + D07BCBC61F2B6F6300ED97AA /* CBLibrary.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCB9B1F2B6F6300ED97AA /* CBLibrary.m */; }; + D07BCBC71F2B6F6300ED97AA /* CBPlayerLayerView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCB9C1F2B6F6300ED97AA /* CBPlayerLayerView.h */; }; + D07BCBC81F2B6F6300ED97AA /* CBPlayerLayerView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCB9D1F2B6F6300ED97AA /* CBPlayerLayerView.m */; }; + D07BCBC91F2B6F6300ED97AA /* CBPlayerView.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCB9E1F2B6F6300ED97AA /* CBPlayerView.h */; }; + D07BCBCA1F2B6F6300ED97AA /* CBPlayerView.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCB9F1F2B6F6300ED97AA /* CBPlayerView.m */; }; + D07BCBCB1F2B6F6300ED97AA /* CBTagNew.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCBA01F2B6F6300ED97AA /* CBTagNew.h */; }; + D07BCBCC1F2B6F6300ED97AA /* CBTagNew.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCBA11F2B6F6300ED97AA /* CBTagNew.m */; }; + D07BCBCD1F2B6F6300ED97AA /* CBVideoPlayer.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCBA21F2B6F6300ED97AA /* CBVideoPlayer.h */; }; + D07BCBCE1F2B6F6300ED97AA /* CBVideoPlayer.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCBA31F2B6F6300ED97AA /* CBVideoPlayer.m */; }; + D07BCBCF1F2B6F6300ED97AA /* NSDictionary+CBExtensions.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCBA41F2B6F6300ED97AA /* NSDictionary+CBExtensions.h */; }; + D07BCBD01F2B6F6300ED97AA /* NSDictionary+CBExtensions.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCBA51F2B6F6300ED97AA /* NSDictionary+CBExtensions.m */; }; + D07BCBD31F2B6FFE00ED97AA /* LegacyHTTPRequestOperation.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCBD11F2B6FFE00ED97AA /* LegacyHTTPRequestOperation.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BCBD91F2B72BD00ED97AA /* NSMutableArray+STKAudioPlayer.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCBD51F2B72BD00ED97AA /* NSMutableArray+STKAudioPlayer.h */; }; + D07BCBDA1F2B72BD00ED97AA /* NSMutableArray+STKAudioPlayer.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCBD61F2B72BD00ED97AA /* NSMutableArray+STKAudioPlayer.m */; }; + D07BCBDB1F2B72BD00ED97AA /* STKAudioPlayer.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCBD71F2B72BD00ED97AA /* STKAudioPlayer.h */; }; + D07BCBDC1F2B72BD00ED97AA /* STKAudioPlayer.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCBD81F2B72BD00ED97AA /* STKAudioPlayer.m */; }; + D07BCBEB1F2B72DC00ED97AA /* STKAutoRecoveringHTTPDataSource.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCBDD1F2B72DC00ED97AA /* STKAutoRecoveringHTTPDataSource.h */; }; + D07BCBEC1F2B72DC00ED97AA /* STKAutoRecoveringHTTPDataSource.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCBDE1F2B72DC00ED97AA /* STKAutoRecoveringHTTPDataSource.m */; }; + D07BCBED1F2B72DC00ED97AA /* STKCoreFoundationDataSource.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCBDF1F2B72DC00ED97AA /* STKCoreFoundationDataSource.h */; }; + D07BCBEE1F2B72DC00ED97AA /* STKCoreFoundationDataSource.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCBE01F2B72DC00ED97AA /* STKCoreFoundationDataSource.m */; }; + D07BCBEF1F2B72DC00ED97AA /* STKDataSource.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCBE11F2B72DC00ED97AA /* STKDataSource.h */; }; + D07BCBF01F2B72DC00ED97AA /* STKDataSource.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCBE21F2B72DC00ED97AA /* STKDataSource.m */; }; + D07BCBF11F2B72DC00ED97AA /* STKDataSourceWrapper.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCBE31F2B72DC00ED97AA /* STKDataSourceWrapper.h */; }; + D07BCBF21F2B72DC00ED97AA /* STKDataSourceWrapper.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCBE41F2B72DC00ED97AA /* STKDataSourceWrapper.m */; }; + D07BCBF31F2B72DC00ED97AA /* STKHTTPDataSource.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCBE51F2B72DC00ED97AA /* STKHTTPDataSource.h */; }; + D07BCBF41F2B72DC00ED97AA /* STKHTTPDataSource.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCBE61F2B72DC00ED97AA /* STKHTTPDataSource.m */; }; + D07BCBF51F2B72DC00ED97AA /* STKLocalFileDataSource.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCBE71F2B72DC00ED97AA /* STKLocalFileDataSource.h */; }; + D07BCBF61F2B72DC00ED97AA /* STKLocalFileDataSource.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCBE81F2B72DC00ED97AA /* STKLocalFileDataSource.m */; }; + D07BCBF71F2B72DC00ED97AA /* STKQueueEntry.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCBE91F2B72DC00ED97AA /* STKQueueEntry.h */; }; + D07BCBF81F2B72DC00ED97AA /* STKQueueEntry.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCBEA1F2B72DC00ED97AA /* STKQueueEntry.m */; }; + D07BCBFB1F2B757700ED97AA /* TGEmbedPIPScrubber.h in Headers */ = {isa = PBXBuildFile; fileRef = D07BCBF91F2B757700ED97AA /* TGEmbedPIPScrubber.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D07BCBFC1F2B757700ED97AA /* TGEmbedPIPScrubber.m in Sources */ = {isa = PBXBuildFile; fileRef = D07BCBFA1F2B757700ED97AA /* TGEmbedPIPScrubber.m */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -852,6 +1489,643 @@ D0177B131F2641B10044446D /* PGCameraVolumeButtonHandler.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PGCameraVolumeButtonHandler.m; sourceTree = ""; }; D0177B2D1F26430D0044446D /* TGCameraPreviewView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGCameraPreviewView.h; sourceTree = ""; }; D0177B2E1F26430D0044446D /* TGCameraPreviewView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGCameraPreviewView.m; sourceTree = ""; }; + D07BC6C91F2A18B700ED97AA /* TGCameraMainPhoneView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGCameraMainPhoneView.h; sourceTree = ""; }; + D07BC6CA1F2A18B700ED97AA /* TGCameraMainPhoneView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGCameraMainPhoneView.m; sourceTree = ""; }; + D07BC6CB1F2A18B700ED97AA /* TGCameraMainTabletView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGCameraMainTabletView.h; sourceTree = ""; }; + D07BC6CC1F2A18B700ED97AA /* TGCameraMainTabletView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGCameraMainTabletView.m; sourceTree = ""; }; + D07BC6CD1F2A18B700ED97AA /* TGCameraMainView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGCameraMainView.h; sourceTree = ""; }; + D07BC6CE1F2A18B700ED97AA /* TGCameraMainView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGCameraMainView.m; sourceTree = ""; }; + D07BC6D51F2A18FE00ED97AA /* UIControl+HitTestEdgeInsets.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIControl+HitTestEdgeInsets.h"; sourceTree = ""; }; + D07BC6D61F2A18FE00ED97AA /* UIControl+HitTestEdgeInsets.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIControl+HitTestEdgeInsets.m"; sourceTree = ""; }; + D07BC6D91F2A19A700ED97AA /* TGCameraFlashActiveView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGCameraFlashActiveView.h; sourceTree = ""; }; + D07BC6DA1F2A19A700ED97AA /* TGCameraFlashActiveView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGCameraFlashActiveView.m; sourceTree = ""; }; + D07BC6DB1F2A19A700ED97AA /* TGCameraFlashControl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGCameraFlashControl.h; sourceTree = ""; }; + D07BC6DC1F2A19A700ED97AA /* TGCameraFlashControl.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGCameraFlashControl.m; sourceTree = ""; }; + D07BC6DD1F2A19A700ED97AA /* TGCameraFlipButton.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGCameraFlipButton.h; sourceTree = ""; }; + D07BC6DE1F2A19A700ED97AA /* TGCameraFlipButton.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGCameraFlipButton.m; sourceTree = ""; }; + D07BC6DF1F2A19A700ED97AA /* TGCameraInterfaceAssets.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGCameraInterfaceAssets.h; sourceTree = ""; }; + D07BC6E01F2A19A700ED97AA /* TGCameraInterfaceAssets.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGCameraInterfaceAssets.m; sourceTree = ""; }; + D07BC6E11F2A19A700ED97AA /* TGCameraModeControl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGCameraModeControl.h; sourceTree = ""; }; + D07BC6E21F2A19A700ED97AA /* TGCameraModeControl.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGCameraModeControl.m; sourceTree = ""; }; + D07BC6E31F2A19A700ED97AA /* TGCameraSegmentsView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGCameraSegmentsView.h; sourceTree = ""; }; + D07BC6E41F2A19A700ED97AA /* TGCameraSegmentsView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGCameraSegmentsView.m; sourceTree = ""; }; + D07BC6E51F2A19A700ED97AA /* TGCameraShutterButton.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGCameraShutterButton.h; sourceTree = ""; }; + D07BC6E61F2A19A700ED97AA /* TGCameraShutterButton.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGCameraShutterButton.m; sourceTree = ""; }; + D07BC6E71F2A19A700ED97AA /* TGCameraTimeCodeView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGCameraTimeCodeView.h; sourceTree = ""; }; + D07BC6E81F2A19A700ED97AA /* TGCameraTimeCodeView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGCameraTimeCodeView.m; sourceTree = ""; }; + D07BC6E91F2A19A700ED97AA /* TGCameraZoomView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGCameraZoomView.h; sourceTree = ""; }; + D07BC6EA1F2A19A700ED97AA /* TGCameraZoomView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGCameraZoomView.m; sourceTree = ""; }; + D07BC6FD1F2A1A7700ED97AA /* TGMenuView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGMenuView.h; sourceTree = ""; }; + D07BC6FE1F2A1A7700ED97AA /* TGMenuView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGMenuView.m; sourceTree = ""; }; + D07BC70D1F2A25AE00ED97AA /* TGCameraPhotoPreviewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGCameraPhotoPreviewController.h; sourceTree = ""; }; + D07BC70E1F2A25AE00ED97AA /* TGCameraPhotoPreviewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGCameraPhotoPreviewController.m; sourceTree = ""; }; + D07BC7111F2A269400ED97AA /* TGImageView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGImageView.h; sourceTree = ""; }; + D07BC7121F2A269400ED97AA /* TGImageView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGImageView.m; sourceTree = ""; }; + D07BC7151F2A29B700ED97AA /* TGPhotoEditorController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPhotoEditorController.h; sourceTree = ""; }; + D07BC7161F2A29B700ED97AA /* TGPhotoEditorController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPhotoEditorController.m; sourceTree = ""; }; + D07BC7191F2A29E400ED97AA /* TGPhotoToolbarView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPhotoToolbarView.h; sourceTree = ""; }; + D07BC71A1F2A29E400ED97AA /* TGPhotoToolbarView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPhotoToolbarView.m; sourceTree = ""; }; + D07BC71B1F2A29E400ED97AA /* TGPhotoToolCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPhotoToolCell.h; sourceTree = ""; }; + D07BC71C1F2A29E400ED97AA /* TGPhotoToolCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPhotoToolCell.m; sourceTree = ""; }; + D07BC71D1F2A29E400ED97AA /* TGPhotoToolsController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPhotoToolsController.h; sourceTree = ""; }; + D07BC71E1F2A29E400ED97AA /* TGPhotoToolsController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPhotoToolsController.m; sourceTree = ""; }; + D07BC7251F2A2A5300ED97AA /* UICollectionView+Utils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UICollectionView+Utils.h"; sourceTree = ""; }; + D07BC7261F2A2A5300ED97AA /* UICollectionView+Utils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UICollectionView+Utils.m"; sourceTree = ""; }; + D07BC7291F2A2A7D00ED97AA /* PGPhotoEditor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PGPhotoEditor.h; sourceTree = ""; }; + D07BC72A1F2A2A7D00ED97AA /* PGPhotoEditor.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PGPhotoEditor.m; sourceTree = ""; }; + D07BC72B1F2A2A7D00ED97AA /* PGPhotoEditorItem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PGPhotoEditorItem.h; sourceTree = ""; }; + D07BC72C1F2A2A7D00ED97AA /* PGPhotoEditorPicture.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PGPhotoEditorPicture.h; sourceTree = ""; }; + D07BC72D1F2A2A7D00ED97AA /* PGPhotoEditorPicture.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PGPhotoEditorPicture.m; sourceTree = ""; }; + D07BC72E1F2A2A7D00ED97AA /* PGPhotoEditorRawDataInput.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PGPhotoEditorRawDataInput.h; sourceTree = ""; }; + D07BC72F1F2A2A7D00ED97AA /* PGPhotoEditorRawDataInput.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PGPhotoEditorRawDataInput.m; sourceTree = ""; }; + D07BC7301F2A2A7D00ED97AA /* PGPhotoEditorRawDataOutput.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PGPhotoEditorRawDataOutput.h; sourceTree = ""; }; + D07BC7311F2A2A7D00ED97AA /* PGPhotoEditorRawDataOutput.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PGPhotoEditorRawDataOutput.m; sourceTree = ""; }; + D07BC7321F2A2A7D00ED97AA /* PGPhotoEditorView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PGPhotoEditorView.h; sourceTree = ""; }; + D07BC7331F2A2A7D00ED97AA /* PGPhotoEditorView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PGPhotoEditorView.m; sourceTree = ""; }; + D07BC73F1F2A2AC500ED97AA /* TGPhotoEditorButton.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPhotoEditorButton.h; sourceTree = ""; }; + D07BC7401F2A2AC500ED97AA /* TGPhotoEditorButton.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPhotoEditorButton.m; sourceTree = ""; }; + D07BC7431F2A2B3700ED97AA /* TGPhotoEditorBlurAreaView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPhotoEditorBlurAreaView.m; sourceTree = ""; }; + D07BC7441F2A2B3700ED97AA /* TGPhotoEditorBlurToolView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPhotoEditorBlurToolView.h; sourceTree = ""; }; + D07BC7451F2A2B3700ED97AA /* TGPhotoEditorBlurToolView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPhotoEditorBlurToolView.m; sourceTree = ""; }; + D07BC7461F2A2B3700ED97AA /* TGPhotoEditorBlurTypeButton.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPhotoEditorBlurTypeButton.h; sourceTree = ""; }; + D07BC7471F2A2B3700ED97AA /* TGPhotoEditorBlurTypeButton.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPhotoEditorBlurTypeButton.m; sourceTree = ""; }; + D07BC7481F2A2B3700ED97AA /* TGPhotoEditorBlurView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPhotoEditorBlurView.h; sourceTree = ""; }; + D07BC7491F2A2B3700ED97AA /* TGPhotoEditorBlurView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPhotoEditorBlurView.m; sourceTree = ""; }; + D07BC74A1F2A2B3700ED97AA /* TGPhotoEditorCollectionView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPhotoEditorCollectionView.h; sourceTree = ""; }; + D07BC74B1F2A2B3700ED97AA /* TGPhotoEditorCollectionView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPhotoEditorCollectionView.m; sourceTree = ""; }; + D07BC74C1F2A2B3700ED97AA /* TGPhotoEditorCurvesHistogramView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPhotoEditorCurvesHistogramView.h; sourceTree = ""; }; + D07BC74D1F2A2B3700ED97AA /* TGPhotoEditorCurvesHistogramView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPhotoEditorCurvesHistogramView.m; sourceTree = ""; }; + D07BC74E1F2A2B3700ED97AA /* TGPhotoEditorCurvesToolView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPhotoEditorCurvesToolView.h; sourceTree = ""; }; + D07BC74F1F2A2B3700ED97AA /* TGPhotoEditorCurvesToolView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPhotoEditorCurvesToolView.m; sourceTree = ""; }; + D07BC7501F2A2B3700ED97AA /* TGPhotoEditorGenericToolView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPhotoEditorGenericToolView.h; sourceTree = ""; }; + D07BC7511F2A2B3700ED97AA /* TGPhotoEditorGenericToolView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPhotoEditorGenericToolView.m; sourceTree = ""; }; + D07BC7521F2A2B3700ED97AA /* TGPhotoEditorHUDView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPhotoEditorHUDView.h; sourceTree = ""; }; + D07BC7531F2A2B3700ED97AA /* TGPhotoEditorHUDView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPhotoEditorHUDView.m; sourceTree = ""; }; + D07BC7541F2A2B3700ED97AA /* TGPhotoEditorInterfaceAssets.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPhotoEditorInterfaceAssets.h; sourceTree = ""; }; + D07BC7551F2A2B3700ED97AA /* TGPhotoEditorInterfaceAssets.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPhotoEditorInterfaceAssets.m; sourceTree = ""; }; + D07BC7561F2A2B3700ED97AA /* TGPhotoEditorItemController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPhotoEditorItemController.h; sourceTree = ""; }; + D07BC7571F2A2B3700ED97AA /* TGPhotoEditorItemController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPhotoEditorItemController.m; sourceTree = ""; }; + D07BC7581F2A2B3700ED97AA /* TGPhotoEditorLinearBlurView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPhotoEditorLinearBlurView.h; sourceTree = ""; }; + D07BC7591F2A2B3700ED97AA /* TGPhotoEditorLinearBlurView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPhotoEditorLinearBlurView.m; sourceTree = ""; }; + D07BC75A1F2A2B3700ED97AA /* TGPhotoEditorPreviewView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPhotoEditorPreviewView.h; sourceTree = ""; }; + D07BC75B1F2A2B3700ED97AA /* TGPhotoEditorPreviewView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPhotoEditorPreviewView.m; sourceTree = ""; }; + D07BC75C1F2A2B3700ED97AA /* TGPhotoEditorRadialBlurView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPhotoEditorRadialBlurView.h; sourceTree = ""; }; + D07BC75D1F2A2B3700ED97AA /* TGPhotoEditorRadialBlurView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPhotoEditorRadialBlurView.m; sourceTree = ""; }; + D07BC75E1F2A2B3700ED97AA /* TGPhotoEditorSliderView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPhotoEditorSliderView.h; sourceTree = ""; }; + D07BC75F1F2A2B3700ED97AA /* TGPhotoEditorSliderView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPhotoEditorSliderView.m; sourceTree = ""; }; + D07BC7601F2A2B3700ED97AA /* TGPhotoEditorTabController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPhotoEditorTabController.h; sourceTree = ""; }; + D07BC7611F2A2B3700ED97AA /* TGPhotoEditorTabController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPhotoEditorTabController.m; sourceTree = ""; }; + D07BC7621F2A2B3700ED97AA /* TGPhotoEditorTintSwatchView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPhotoEditorTintSwatchView.h; sourceTree = ""; }; + D07BC7631F2A2B3700ED97AA /* TGPhotoEditorTintSwatchView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPhotoEditorTintSwatchView.m; sourceTree = ""; }; + D07BC7641F2A2B3700ED97AA /* TGPhotoEditorTintToolView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPhotoEditorTintToolView.h; sourceTree = ""; }; + D07BC7651F2A2B3700ED97AA /* TGPhotoEditorTintToolView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPhotoEditorTintToolView.m; sourceTree = ""; }; + D07BC7661F2A2B3700ED97AA /* TGPhotoEditorToolButtonsView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPhotoEditorToolButtonsView.h; sourceTree = ""; }; + D07BC7671F2A2B3700ED97AA /* TGPhotoEditorToolButtonsView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPhotoEditorToolButtonsView.m; sourceTree = ""; }; + D07BC7681F2A2B3700ED97AA /* TGPhotoEditorToolView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPhotoEditorToolView.h; sourceTree = ""; }; + D07BC78F1F2A2B5A00ED97AA /* TGPhotoEditorBlurAreaView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPhotoEditorBlurAreaView.h; sourceTree = ""; }; + D07BC7921F2A2B8900ED97AA /* GLProgram.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GLProgram.h; sourceTree = ""; }; + D07BC7931F2A2B8900ED97AA /* GLProgram.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GLProgram.m; sourceTree = ""; }; + D07BC7941F2A2B8900ED97AA /* GPUImage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GPUImage.h; sourceTree = ""; }; + D07BC7951F2A2B8900ED97AA /* GPUImageContext.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GPUImageContext.h; sourceTree = ""; }; + D07BC7961F2A2B8900ED97AA /* GPUImageContext.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GPUImageContext.m; sourceTree = ""; }; + D07BC7971F2A2B8900ED97AA /* GPUImageFilter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GPUImageFilter.h; sourceTree = ""; }; + D07BC7981F2A2B8900ED97AA /* GPUImageFilter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GPUImageFilter.m; sourceTree = ""; }; + D07BC7991F2A2B8900ED97AA /* GPUImageFramebuffer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GPUImageFramebuffer.h; sourceTree = ""; }; + D07BC79A1F2A2B8900ED97AA /* GPUImageFramebuffer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GPUImageFramebuffer.m; sourceTree = ""; }; + D07BC79B1F2A2B8900ED97AA /* GPUImageFramebufferCache.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GPUImageFramebufferCache.h; sourceTree = ""; }; + D07BC79C1F2A2B8900ED97AA /* GPUImageFramebufferCache.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GPUImageFramebufferCache.m; sourceTree = ""; }; + D07BC79D1F2A2B8900ED97AA /* GPUImageOutput.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GPUImageOutput.h; sourceTree = ""; }; + D07BC79E1F2A2B8900ED97AA /* GPUImageOutput.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GPUImageOutput.m; sourceTree = ""; }; + D07BC79F1F2A2B8900ED97AA /* GPUImageTwoInputFilter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GPUImageTwoInputFilter.h; sourceTree = ""; }; + D07BC7A01F2A2B8900ED97AA /* GPUImageTwoInputFilter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GPUImageTwoInputFilter.m; sourceTree = ""; }; + D07BC7B01F2A2BBE00ED97AA /* PGPhotoProcessPass.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PGPhotoProcessPass.h; sourceTree = ""; }; + D07BC7B11F2A2BBE00ED97AA /* PGPhotoProcessPass.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PGPhotoProcessPass.m; sourceTree = ""; }; + D07BC7B21F2A2BBE00ED97AA /* PGBlurTool.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PGBlurTool.h; sourceTree = ""; }; + D07BC7B31F2A2BBE00ED97AA /* PGBlurTool.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PGBlurTool.m; sourceTree = ""; }; + D07BC7B81F2A2BDD00ED97AA /* PGPhotoTool.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PGPhotoTool.h; sourceTree = ""; }; + D07BC7B91F2A2BDD00ED97AA /* PGPhotoTool.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PGPhotoTool.m; sourceTree = ""; }; + D07BC7BA1F2A2BDD00ED97AA /* PGPhotoToolComposer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PGPhotoToolComposer.h; sourceTree = ""; }; + D07BC7BB1F2A2BDD00ED97AA /* PGPhotoToolComposer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PGPhotoToolComposer.m; sourceTree = ""; }; + D07BC7C01F2A2C0A00ED97AA /* PGContrastTool.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PGContrastTool.h; sourceTree = ""; }; + D07BC7C11F2A2C0A00ED97AA /* PGContrastTool.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PGContrastTool.m; sourceTree = ""; }; + D07BC7C21F2A2C0A00ED97AA /* PGCurvesTool.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PGCurvesTool.h; sourceTree = ""; }; + D07BC7C31F2A2C0A00ED97AA /* PGCurvesTool.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PGCurvesTool.m; sourceTree = ""; }; + D07BC7C41F2A2C0A00ED97AA /* PGEnhanceTool.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PGEnhanceTool.h; sourceTree = ""; }; + D07BC7C51F2A2C0A00ED97AA /* PGEnhanceTool.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PGEnhanceTool.m; sourceTree = ""; }; + D07BC7C61F2A2C0A00ED97AA /* PGExposureTool.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PGExposureTool.h; sourceTree = ""; }; + D07BC7C71F2A2C0A00ED97AA /* PGExposureTool.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PGExposureTool.m; sourceTree = ""; }; + D07BC7C81F2A2C0A00ED97AA /* PGFadeTool.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PGFadeTool.h; sourceTree = ""; }; + D07BC7C91F2A2C0A00ED97AA /* PGFadeTool.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PGFadeTool.m; sourceTree = ""; }; + D07BC7CA1F2A2C0B00ED97AA /* PGGrainTool.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PGGrainTool.h; sourceTree = ""; }; + D07BC7CB1F2A2C0B00ED97AA /* PGGrainTool.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PGGrainTool.m; sourceTree = ""; }; + D07BC7CC1F2A2C0B00ED97AA /* PGHighlightsTool.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PGHighlightsTool.h; sourceTree = ""; }; + D07BC7CD1F2A2C0B00ED97AA /* PGHighlightsTool.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PGHighlightsTool.m; sourceTree = ""; }; + D07BC7CE1F2A2C0B00ED97AA /* PGPhotoBlurPass.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PGPhotoBlurPass.h; sourceTree = ""; }; + D07BC7CF1F2A2C0B00ED97AA /* PGPhotoBlurPass.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PGPhotoBlurPass.m; sourceTree = ""; }; + D07BC7D01F2A2C0B00ED97AA /* PGPhotoCustomFilterPass.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PGPhotoCustomFilterPass.h; sourceTree = ""; }; + D07BC7D11F2A2C0B00ED97AA /* PGPhotoCustomFilterPass.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PGPhotoCustomFilterPass.m; sourceTree = ""; }; + D07BC7D21F2A2C0B00ED97AA /* PGPhotoEnhanceColorConversionFilter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PGPhotoEnhanceColorConversionFilter.h; sourceTree = ""; }; + D07BC7D31F2A2C0B00ED97AA /* PGPhotoEnhanceColorConversionFilter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PGPhotoEnhanceColorConversionFilter.m; sourceTree = ""; }; + D07BC7D41F2A2C0B00ED97AA /* PGPhotoEnhanceInterpolationFilter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PGPhotoEnhanceInterpolationFilter.h; sourceTree = ""; }; + D07BC7D51F2A2C0B00ED97AA /* PGPhotoEnhanceInterpolationFilter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PGPhotoEnhanceInterpolationFilter.m; sourceTree = ""; }; + D07BC7D61F2A2C0B00ED97AA /* PGPhotoEnhanceLUTGenerator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PGPhotoEnhanceLUTGenerator.h; sourceTree = ""; }; + D07BC7D71F2A2C0B00ED97AA /* PGPhotoEnhanceLUTGenerator.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PGPhotoEnhanceLUTGenerator.m; sourceTree = ""; }; + D07BC7D81F2A2C0B00ED97AA /* PGPhotoEnhancePass.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PGPhotoEnhancePass.h; sourceTree = ""; }; + D07BC7D91F2A2C0B00ED97AA /* PGPhotoEnhancePass.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PGPhotoEnhancePass.m; sourceTree = ""; }; + D07BC7DA1F2A2C0B00ED97AA /* PGPhotoFilter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PGPhotoFilter.h; sourceTree = ""; }; + D07BC7DB1F2A2C0B00ED97AA /* PGPhotoFilter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PGPhotoFilter.m; sourceTree = ""; }; + D07BC7DC1F2A2C0B00ED97AA /* PGPhotoFilterDefinition.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PGPhotoFilterDefinition.h; sourceTree = ""; }; + D07BC7DD1F2A2C0B00ED97AA /* PGPhotoFilterDefinition.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PGPhotoFilterDefinition.m; sourceTree = ""; }; + D07BC7DE1F2A2C0B00ED97AA /* PGPhotoFilterThumbnailManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PGPhotoFilterThumbnailManager.h; sourceTree = ""; }; + D07BC7DF1F2A2C0B00ED97AA /* PGPhotoFilterThumbnailManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PGPhotoFilterThumbnailManager.m; sourceTree = ""; }; + D07BC7E01F2A2C0B00ED97AA /* PGPhotoGaussianBlurFilter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PGPhotoGaussianBlurFilter.h; sourceTree = ""; }; + D07BC7E11F2A2C0B00ED97AA /* PGPhotoGaussianBlurFilter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PGPhotoGaussianBlurFilter.m; sourceTree = ""; }; + D07BC7E21F2A2C0B00ED97AA /* PGPhotoHistogram.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PGPhotoHistogram.h; sourceTree = ""; }; + D07BC7E31F2A2C0B00ED97AA /* PGPhotoHistogram.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PGPhotoHistogram.m; sourceTree = ""; }; + D07BC7E41F2A2C0B00ED97AA /* PGPhotoHistogramGenerator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PGPhotoHistogramGenerator.h; sourceTree = ""; }; + D07BC7E51F2A2C0B00ED97AA /* PGPhotoHistogramGenerator.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PGPhotoHistogramGenerator.m; sourceTree = ""; }; + D07BC7E61F2A2C0B00ED97AA /* PGPhotoLookupFilterPass.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PGPhotoLookupFilterPass.h; sourceTree = ""; }; + D07BC7E71F2A2C0B00ED97AA /* PGPhotoLookupFilterPass.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PGPhotoLookupFilterPass.m; sourceTree = ""; }; + D07BC7E81F2A2C0B00ED97AA /* PGPhotoSharpenPass.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PGPhotoSharpenPass.h; sourceTree = ""; }; + D07BC7E91F2A2C0B00ED97AA /* PGPhotoSharpenPass.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PGPhotoSharpenPass.m; sourceTree = ""; }; + D07BC7EA1F2A2C0B00ED97AA /* PGSaturationTool.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PGSaturationTool.h; sourceTree = ""; }; + D07BC7EB1F2A2C0B00ED97AA /* PGSaturationTool.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PGSaturationTool.m; sourceTree = ""; }; + D07BC7EC1F2A2C0B00ED97AA /* PGShadowsTool.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PGShadowsTool.h; sourceTree = ""; }; + D07BC7ED1F2A2C0B00ED97AA /* PGShadowsTool.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PGShadowsTool.m; sourceTree = ""; }; + D07BC7EE1F2A2C0B00ED97AA /* PGSharpenTool.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PGSharpenTool.h; sourceTree = ""; }; + D07BC7EF1F2A2C0B00ED97AA /* PGSharpenTool.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PGSharpenTool.m; sourceTree = ""; }; + D07BC7F01F2A2C0B00ED97AA /* PGTintTool.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PGTintTool.h; sourceTree = ""; }; + D07BC7F11F2A2C0B00ED97AA /* PGTintTool.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PGTintTool.m; sourceTree = ""; }; + D07BC7F21F2A2C0B00ED97AA /* PGVignetteTool.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PGVignetteTool.h; sourceTree = ""; }; + D07BC7F31F2A2C0B00ED97AA /* PGVignetteTool.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PGVignetteTool.m; sourceTree = ""; }; + D07BC7F41F2A2C0B00ED97AA /* PGWarmthTool.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PGWarmthTool.h; sourceTree = ""; }; + D07BC7F51F2A2C0B00ED97AA /* PGWarmthTool.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PGWarmthTool.m; sourceTree = ""; }; + D07BC82D1F2A2D0C00ED97AA /* TGSecretTimerMenu.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGSecretTimerMenu.h; sourceTree = ""; }; + D07BC82E1F2A2D0C00ED97AA /* TGSecretTimerMenu.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGSecretTimerMenu.m; sourceTree = ""; }; + D07BC82F1F2A2D0C00ED97AA /* TGSecretTimerPickerItemView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGSecretTimerPickerItemView.h; sourceTree = ""; }; + D07BC8301F2A2D0C00ED97AA /* TGSecretTimerPickerItemView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGSecretTimerPickerItemView.m; sourceTree = ""; }; + D07BC8311F2A2D0C00ED97AA /* TGSecretTimerValueController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGSecretTimerValueController.h; sourceTree = ""; }; + D07BC8321F2A2D0C00ED97AA /* TGSecretTimerValueController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGSecretTimerValueController.m; sourceTree = ""; }; + D07BC8331F2A2D0C00ED97AA /* TGSecretTimerValueControllerItemView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGSecretTimerValueControllerItemView.h; sourceTree = ""; }; + D07BC8341F2A2D0C00ED97AA /* TGSecretTimerValueControllerItemView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGSecretTimerValueControllerItemView.m; sourceTree = ""; }; + D07BC83D1F2A2D7900ED97AA /* TGPhotoCaptionController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPhotoCaptionController.h; sourceTree = ""; }; + D07BC83E1F2A2D7900ED97AA /* TGPhotoCaptionController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPhotoCaptionController.m; sourceTree = ""; }; + D07BC83F1F2A2D7900ED97AA /* TGPhotoCaptionInputMixin.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPhotoCaptionInputMixin.h; sourceTree = ""; }; + D07BC8401F2A2D7900ED97AA /* TGPhotoCaptionInputMixin.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPhotoCaptionInputMixin.m; sourceTree = ""; }; + D07BC8461F2A2DA200ED97AA /* TGMenuSheetController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGMenuSheetController.h; sourceTree = ""; }; + D07BC8471F2A2DA200ED97AA /* TGMenuSheetController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGMenuSheetController.m; sourceTree = ""; }; + D07BC84A1F2A2DBD00ED97AA /* TGMenuSheetButtonItemView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGMenuSheetButtonItemView.h; sourceTree = ""; }; + D07BC84B1F2A2DBD00ED97AA /* TGMenuSheetButtonItemView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGMenuSheetButtonItemView.m; sourceTree = ""; }; + D07BC84C1F2A2DBD00ED97AA /* TGMenuSheetCollectionView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGMenuSheetCollectionView.h; sourceTree = ""; }; + D07BC84D1F2A2DBD00ED97AA /* TGMenuSheetCollectionView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGMenuSheetCollectionView.m; sourceTree = ""; }; + D07BC84E1F2A2DBD00ED97AA /* TGMenuSheetDimView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGMenuSheetDimView.h; sourceTree = ""; }; + D07BC84F1F2A2DBD00ED97AA /* TGMenuSheetDimView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGMenuSheetDimView.m; sourceTree = ""; }; + D07BC8501F2A2DBD00ED97AA /* TGMenuSheetItemView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGMenuSheetItemView.h; sourceTree = ""; }; + D07BC8511F2A2DBD00ED97AA /* TGMenuSheetItemView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGMenuSheetItemView.m; sourceTree = ""; }; + D07BC8521F2A2DBD00ED97AA /* TGMenuSheetTitleItemView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGMenuSheetTitleItemView.h; sourceTree = ""; }; + D07BC8531F2A2DBD00ED97AA /* TGMenuSheetTitleItemView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGMenuSheetTitleItemView.m; sourceTree = ""; }; + D07BC8541F2A2DBD00ED97AA /* TGMenuSheetView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGMenuSheetView.h; sourceTree = ""; }; + D07BC8551F2A2DBD00ED97AA /* TGMenuSheetView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGMenuSheetView.m; sourceTree = ""; }; + D07BC8631F2A2F1300ED97AA /* TGMediaPickerCaptionInputPanel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGMediaPickerCaptionInputPanel.h; sourceTree = ""; }; + D07BC8641F2A2F1300ED97AA /* TGMediaPickerCaptionInputPanel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGMediaPickerCaptionInputPanel.m; sourceTree = ""; }; + D07BC8681F2A2F3800ED97AA /* HPGrowingTextView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HPGrowingTextView.h; sourceTree = ""; }; + D07BC8691F2A2F3800ED97AA /* HPGrowingTextView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HPGrowingTextView.m; sourceTree = ""; }; + D07BC86A1F2A2F3800ED97AA /* HPTextViewInternal.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HPTextViewInternal.m; sourceTree = ""; }; + D07BC86E1F2A2F5300ED97AA /* HPTextViewInternal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HPTextViewInternal.h; sourceTree = ""; }; + D07BC8701F2A2F6500ED97AA /* TGInputTextTag.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGInputTextTag.h; sourceTree = ""; }; + D07BC8711F2A2F6500ED97AA /* TGInputTextTag.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGInputTextTag.m; sourceTree = ""; }; + D07BC8741F2A2F7B00ED97AA /* TGWeakDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGWeakDelegate.h; sourceTree = ""; }; + D07BC8751F2A2F7B00ED97AA /* TGWeakDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGWeakDelegate.m; sourceTree = ""; }; + D07BC8791F2A365000ED97AA /* TGProgressSpinnerView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGProgressSpinnerView.h; sourceTree = ""; }; + D07BC87A1F2A365000ED97AA /* TGProgressSpinnerView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGProgressSpinnerView.m; sourceTree = ""; }; + D07BC87B1F2A365000ED97AA /* TGProgressWindow.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGProgressWindow.h; sourceTree = ""; }; + D07BC87C1F2A365000ED97AA /* TGProgressWindow.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGProgressWindow.m; sourceTree = ""; }; + D07BC8811F2A367500ED97AA /* TGActivityIndicatorView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGActivityIndicatorView.h; sourceTree = ""; }; + D07BC8821F2A367500ED97AA /* TGActivityIndicatorView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGActivityIndicatorView.m; sourceTree = ""; }; + D07BC8851F2A375800ED97AA /* TGPhotoCropAreaView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPhotoCropAreaView.h; sourceTree = ""; }; + D07BC8861F2A375800ED97AA /* TGPhotoCropAreaView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPhotoCropAreaView.m; sourceTree = ""; }; + D07BC8871F2A375800ED97AA /* TGPhotoCropControl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPhotoCropControl.h; sourceTree = ""; }; + D07BC8881F2A375800ED97AA /* TGPhotoCropControl.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPhotoCropControl.m; sourceTree = ""; }; + D07BC8891F2A375800ED97AA /* TGPhotoCropController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPhotoCropController.h; sourceTree = ""; }; + D07BC88A1F2A375800ED97AA /* TGPhotoCropController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPhotoCropController.m; sourceTree = ""; }; + D07BC88B1F2A375800ED97AA /* TGPhotoCropGridView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPhotoCropGridView.h; sourceTree = ""; }; + D07BC88C1F2A375800ED97AA /* TGPhotoCropGridView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPhotoCropGridView.m; sourceTree = ""; }; + D07BC88D1F2A375800ED97AA /* TGPhotoCropRotationView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPhotoCropRotationView.h; sourceTree = ""; }; + D07BC88E1F2A375800ED97AA /* TGPhotoCropRotationView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPhotoCropRotationView.m; sourceTree = ""; }; + D07BC88F1F2A375800ED97AA /* TGPhotoCropScrollView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPhotoCropScrollView.h; sourceTree = ""; }; + D07BC8901F2A375800ED97AA /* TGPhotoCropScrollView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPhotoCropScrollView.m; sourceTree = ""; }; + D07BC8911F2A375800ED97AA /* TGPhotoCropView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPhotoCropView.h; sourceTree = ""; }; + D07BC8921F2A375800ED97AA /* TGPhotoCropView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPhotoCropView.m; sourceTree = ""; }; + D07BC8A11F2A37A500ED97AA /* TGPhotoAvatarCropController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPhotoAvatarCropController.h; sourceTree = ""; }; + D07BC8A21F2A37A500ED97AA /* TGPhotoAvatarCropController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPhotoAvatarCropController.m; sourceTree = ""; }; + D07BC8A31F2A37A500ED97AA /* TGPhotoAvatarCropView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPhotoAvatarCropView.h; sourceTree = ""; }; + D07BC8A41F2A37A500ED97AA /* TGPhotoAvatarCropView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPhotoAvatarCropView.m; sourceTree = ""; }; + D07BC8A91F2A37EC00ED97AA /* TGPhotoPaintActionsView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPhotoPaintActionsView.h; sourceTree = ""; }; + D07BC8AA1F2A37EC00ED97AA /* TGPhotoPaintActionsView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPhotoPaintActionsView.m; sourceTree = ""; }; + D07BC8AB1F2A37EC00ED97AA /* TGPhotoPaintColorPicker.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPhotoPaintColorPicker.h; sourceTree = ""; }; + D07BC8AC1F2A37EC00ED97AA /* TGPhotoPaintColorPicker.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPhotoPaintColorPicker.m; sourceTree = ""; }; + D07BC8AD1F2A37EC00ED97AA /* TGPhotoPaintController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPhotoPaintController.h; sourceTree = ""; }; + D07BC8AE1F2A37EC00ED97AA /* TGPhotoPaintController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPhotoPaintController.m; sourceTree = ""; }; + D07BC8AF1F2A37EC00ED97AA /* TGPhotoPaintEntityView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPhotoPaintEntityView.h; sourceTree = ""; }; + D07BC8B01F2A37EC00ED97AA /* TGPhotoPaintEntityView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPhotoPaintEntityView.m; sourceTree = ""; }; + D07BC8B11F2A37EC00ED97AA /* TGPhotoPaintFont.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPhotoPaintFont.h; sourceTree = ""; }; + D07BC8B21F2A37EC00ED97AA /* TGPhotoPaintFont.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPhotoPaintFont.m; sourceTree = ""; }; + D07BC8B31F2A37EC00ED97AA /* TGPhotoPaintScrollView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPhotoPaintScrollView.h; sourceTree = ""; }; + D07BC8B41F2A37EC00ED97AA /* TGPhotoPaintScrollView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPhotoPaintScrollView.m; sourceTree = ""; }; + D07BC8B51F2A37EC00ED97AA /* TGPhotoPaintSelectionContainerView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPhotoPaintSelectionContainerView.h; sourceTree = ""; }; + D07BC8B61F2A37EC00ED97AA /* TGPhotoPaintSelectionContainerView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPhotoPaintSelectionContainerView.m; sourceTree = ""; }; + D07BC8B71F2A37EC00ED97AA /* TGPhotoPaintSettingsView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPhotoPaintSettingsView.h; sourceTree = ""; }; + D07BC8B81F2A37EC00ED97AA /* TGPhotoPaintSettingsView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPhotoPaintSettingsView.m; sourceTree = ""; }; + D07BC8B91F2A37EC00ED97AA /* TGPhotoPaintSettingsWrapperView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPhotoPaintSettingsWrapperView.h; sourceTree = ""; }; + D07BC8BA1F2A37EC00ED97AA /* TGPhotoPaintSettingsWrapperView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPhotoPaintSettingsWrapperView.m; sourceTree = ""; }; + D07BC8BB1F2A37EC00ED97AA /* TGPhotoPaintSparseView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPhotoPaintSparseView.h; sourceTree = ""; }; + D07BC8BC1F2A37EC00ED97AA /* TGPhotoPaintSparseView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPhotoPaintSparseView.m; sourceTree = ""; }; + D07BC8BD1F2A37EC00ED97AA /* TGPhotoPaintTextEntity.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPhotoPaintTextEntity.h; sourceTree = ""; }; + D07BC8BE1F2A37EC00ED97AA /* TGPhotoPaintTextEntity.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPhotoPaintTextEntity.m; sourceTree = ""; }; + D07BC8D51F2A380D00ED97AA /* TGPaintBrush.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPaintBrush.h; sourceTree = ""; }; + D07BC8D61F2A380D00ED97AA /* TGPaintBrush.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPaintBrush.m; sourceTree = ""; }; + D07BC8D71F2A380D00ED97AA /* TGPaintBrushPreview.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPaintBrushPreview.h; sourceTree = ""; }; + D07BC8D81F2A380D00ED97AA /* TGPaintBrushPreview.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPaintBrushPreview.m; sourceTree = ""; }; + D07BC8D91F2A380D00ED97AA /* TGPaintBuffers.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPaintBuffers.h; sourceTree = ""; }; + D07BC8DA1F2A380D00ED97AA /* TGPaintBuffers.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPaintBuffers.m; sourceTree = ""; }; + D07BC8DB1F2A380D00ED97AA /* TGPaintCanvas.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPaintCanvas.h; sourceTree = ""; }; + D07BC8DC1F2A380D00ED97AA /* TGPaintCanvas.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPaintCanvas.m; sourceTree = ""; }; + D07BC8DD1F2A380D00ED97AA /* TGPaintEllipticalBrush.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPaintEllipticalBrush.h; sourceTree = ""; }; + D07BC8DE1F2A380D00ED97AA /* TGPaintEllipticalBrush.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPaintEllipticalBrush.m; sourceTree = ""; }; + D07BC8DF1F2A380D00ED97AA /* TGPaintFaceDebugView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPaintFaceDebugView.h; sourceTree = ""; }; + D07BC8E01F2A380D00ED97AA /* TGPaintFaceDebugView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPaintFaceDebugView.m; sourceTree = ""; }; + D07BC8E11F2A380D00ED97AA /* TGPaintFaceDetector.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPaintFaceDetector.h; sourceTree = ""; }; + D07BC8E21F2A380D00ED97AA /* TGPaintFaceDetector.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPaintFaceDetector.m; sourceTree = ""; }; + D07BC8E31F2A380D00ED97AA /* TGPainting.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPainting.h; sourceTree = ""; }; + D07BC8E41F2A380D00ED97AA /* TGPainting.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPainting.m; sourceTree = ""; }; + D07BC8E51F2A380D00ED97AA /* TGPaintingWrapperView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPaintingWrapperView.h; sourceTree = ""; }; + D07BC8E61F2A380D00ED97AA /* TGPaintingWrapperView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPaintingWrapperView.m; sourceTree = ""; }; + D07BC8E71F2A380D00ED97AA /* TGPaintInput.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPaintInput.h; sourceTree = ""; }; + D07BC8E81F2A380D00ED97AA /* TGPaintInput.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPaintInput.m; sourceTree = ""; }; + D07BC8E91F2A380D00ED97AA /* TGPaintNeonBrush.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPaintNeonBrush.h; sourceTree = ""; }; + D07BC8EA1F2A380D00ED97AA /* TGPaintNeonBrush.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPaintNeonBrush.m; sourceTree = ""; }; + D07BC8EB1F2A380D00ED97AA /* TGPaintPanGestureRecognizer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPaintPanGestureRecognizer.h; sourceTree = ""; }; + D07BC8EC1F2A380D00ED97AA /* TGPaintPanGestureRecognizer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPaintPanGestureRecognizer.m; sourceTree = ""; }; + D07BC8ED1F2A380D00ED97AA /* TGPaintPath.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPaintPath.h; sourceTree = ""; }; + D07BC8EE1F2A380D00ED97AA /* TGPaintPath.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPaintPath.m; sourceTree = ""; }; + D07BC8EF1F2A380D00ED97AA /* TGPaintRadialBrush.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPaintRadialBrush.h; sourceTree = ""; }; + D07BC8F01F2A380D00ED97AA /* TGPaintRadialBrush.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPaintRadialBrush.m; sourceTree = ""; }; + D07BC8F11F2A380D00ED97AA /* TGPaintRender.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPaintRender.h; sourceTree = ""; }; + D07BC8F21F2A380D00ED97AA /* TGPaintRender.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPaintRender.m; sourceTree = ""; }; + D07BC8F31F2A380D00ED97AA /* TGPaintShader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPaintShader.h; sourceTree = ""; }; + D07BC8F41F2A380D00ED97AA /* TGPaintShader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPaintShader.m; sourceTree = ""; }; + D07BC8F51F2A380D00ED97AA /* TGPaintShaderSet.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPaintShaderSet.h; sourceTree = ""; }; + D07BC8F61F2A380D00ED97AA /* TGPaintShaderSet.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPaintShaderSet.m; sourceTree = ""; }; + D07BC8F71F2A380D00ED97AA /* TGPaintSlice.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPaintSlice.h; sourceTree = ""; }; + D07BC8F81F2A380D00ED97AA /* TGPaintSlice.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPaintSlice.m; sourceTree = ""; }; + D07BC8F91F2A380D00ED97AA /* TGPaintState.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPaintState.h; sourceTree = ""; }; + D07BC8FA1F2A380D00ED97AA /* TGPaintState.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPaintState.m; sourceTree = ""; }; + D07BC8FB1F2A380D00ED97AA /* TGPaintSwatch.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPaintSwatch.h; sourceTree = ""; }; + D07BC8FC1F2A380D00ED97AA /* TGPaintSwatch.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPaintSwatch.m; sourceTree = ""; }; + D07BC8FD1F2A380D00ED97AA /* TGPaintTexture.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPaintTexture.h; sourceTree = ""; }; + D07BC8FE1F2A380D00ED97AA /* TGPaintTexture.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPaintTexture.m; sourceTree = ""; }; + D07BC9291F2A3A3F00ED97AA /* matrix.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = matrix.h; sourceTree = ""; }; + D07BC92A1F2A3A3F00ED97AA /* matrix.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = matrix.m; sourceTree = ""; }; + D07BC92D1F2A3BA600ED97AA /* TGPhotoEntitiesContainerView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPhotoEntitiesContainerView.h; sourceTree = ""; }; + D07BC92E1F2A3BA600ED97AA /* TGPhotoEntitiesContainerView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPhotoEntitiesContainerView.m; sourceTree = ""; }; + D07BC9311F2A3BD100ED97AA /* TGPhotoTextEntityView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPhotoTextEntityView.h; sourceTree = ""; }; + D07BC9321F2A3BD100ED97AA /* TGPhotoTextEntityView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPhotoTextEntityView.m; sourceTree = ""; }; + D07BC9331F2A3BD100ED97AA /* TGPhotoStickerEntityView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPhotoStickerEntityView.h; sourceTree = ""; }; + D07BC9341F2A3BD100ED97AA /* TGPhotoStickerEntityView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPhotoStickerEntityView.m; sourceTree = ""; }; + D07BC9391F2A3C1F00ED97AA /* TGPhotoQualityController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPhotoQualityController.h; sourceTree = ""; }; + D07BC93A1F2A3C1F00ED97AA /* TGPhotoQualityController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPhotoQualityController.m; sourceTree = ""; }; + D07BC93D1F2A3DB900ED97AA /* TGMessageImageViewOverlayView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGMessageImageViewOverlayView.h; sourceTree = ""; }; + D07BC93E1F2A3DB900ED97AA /* TGMessageImageViewOverlayView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGMessageImageViewOverlayView.m; sourceTree = ""; }; + D07BC9411F2A3E4400ED97AA /* TGSuggestionContext.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGSuggestionContext.h; sourceTree = ""; }; + D07BC9421F2A3E4400ED97AA /* TGSuggestionContext.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGSuggestionContext.m; sourceTree = ""; }; + D07BC9461F2A3EA900ED97AA /* TGHashtagPanelCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGHashtagPanelCell.h; sourceTree = ""; }; + D07BC9471F2A3EA900ED97AA /* TGHashtagPanelCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGHashtagPanelCell.m; sourceTree = ""; }; + D07BC9481F2A3EA900ED97AA /* TGMentionPanelCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGMentionPanelCell.h; sourceTree = ""; }; + D07BC9491F2A3EA900ED97AA /* TGMentionPanelCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGMentionPanelCell.m; sourceTree = ""; }; + D07BC94A1F2A3EA900ED97AA /* TGModernConversationHashtagsAssociatedPanel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGModernConversationHashtagsAssociatedPanel.h; sourceTree = ""; }; + D07BC94B1F2A3EA900ED97AA /* TGModernConversationHashtagsAssociatedPanel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGModernConversationHashtagsAssociatedPanel.m; sourceTree = ""; }; + D07BC94C1F2A3EA900ED97AA /* TGModernConversationMentionsAssociatedPanel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGModernConversationMentionsAssociatedPanel.h; sourceTree = ""; }; + D07BC94D1F2A3EA900ED97AA /* TGModernConversationMentionsAssociatedPanel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGModernConversationMentionsAssociatedPanel.m; sourceTree = ""; }; + D07BC9561F2A3EBF00ED97AA /* TGModernConversationAssociatedInputPanel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGModernConversationAssociatedInputPanel.h; sourceTree = ""; }; + D07BC9571F2A3EBF00ED97AA /* TGModernConversationAssociatedInputPanel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGModernConversationAssociatedInputPanel.m; sourceTree = ""; }; + D07BC95A1F2A3EF000ED97AA /* TGLetteredAvatarView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGLetteredAvatarView.h; sourceTree = ""; }; + D07BC95B1F2A3EF000ED97AA /* TGLetteredAvatarView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGLetteredAvatarView.m; sourceTree = ""; }; + D07BC95E1F2A3F0A00ED97AA /* TGGradientLabel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGGradientLabel.h; sourceTree = ""; }; + D07BC95F1F2A3F0A00ED97AA /* TGGradientLabel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGGradientLabel.m; sourceTree = ""; }; + D07BC9621F2A3F4000ED97AA /* TGRemoteImageView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGRemoteImageView.h; sourceTree = ""; }; + D07BC9631F2A3F4000ED97AA /* TGRemoteImageView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGRemoteImageView.m; sourceTree = ""; }; + D07BC9661F2A3F5C00ED97AA /* TGCache.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGCache.h; sourceTree = ""; }; + D07BC9671F2A3F5C00ED97AA /* TGCache.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGCache.m; sourceTree = ""; }; + D07BC96A1F2A43E300ED97AA /* TGPhotoStickersView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPhotoStickersView.h; sourceTree = ""; }; + D07BC96B1F2A43E300ED97AA /* TGPhotoStickersView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPhotoStickersView.m; sourceTree = ""; }; + D07BC96E1F2A467D00ED97AA /* TGPhotoTextSettingsView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPhotoTextSettingsView.h; sourceTree = ""; }; + D07BC96F1F2A467D00ED97AA /* TGPhotoTextSettingsView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPhotoTextSettingsView.m; sourceTree = ""; }; + D07BC9701F2A467D00ED97AA /* TGPhotoBrushSettingsView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPhotoBrushSettingsView.h; sourceTree = ""; }; + D07BC9711F2A467D00ED97AA /* TGPhotoBrushSettingsView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPhotoBrushSettingsView.m; sourceTree = ""; }; + D07BC9771F2A471000ED97AA /* TGStickerKeyboardTabPanel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGStickerKeyboardTabPanel.h; sourceTree = ""; }; + D07BC9781F2A471000ED97AA /* TGStickerKeyboardTabPanel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGStickerKeyboardTabPanel.m; sourceTree = ""; }; + D07BC97B1F2A472900ED97AA /* TGPhotoStickersCollectionLayout.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPhotoStickersCollectionLayout.h; sourceTree = ""; }; + D07BC97C1F2A472900ED97AA /* TGPhotoStickersCollectionLayout.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPhotoStickersCollectionLayout.m; sourceTree = ""; }; + D07BC97D1F2A472900ED97AA /* TGPhotoStickersCollectionView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPhotoStickersCollectionView.h; sourceTree = ""; }; + D07BC97E1F2A472900ED97AA /* TGPhotoStickersCollectionView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPhotoStickersCollectionView.m; sourceTree = ""; }; + D07BC97F1F2A472900ED97AA /* TGPhotoStickersSectionHeader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPhotoStickersSectionHeader.h; sourceTree = ""; }; + D07BC9801F2A472900ED97AA /* TGPhotoStickersSectionHeader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPhotoStickersSectionHeader.m; sourceTree = ""; }; + D07BC9811F2A472900ED97AA /* TGPhotoStickersSectionHeaderView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPhotoStickersSectionHeaderView.h; sourceTree = ""; }; + D07BC9821F2A472900ED97AA /* TGPhotoStickersSectionHeaderView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPhotoStickersSectionHeaderView.m; sourceTree = ""; }; + D07BC98F1F2A480800ED97AA /* TGStickerKeyboardTabCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGStickerKeyboardTabCell.h; sourceTree = ""; }; + D07BC9901F2A480800ED97AA /* TGStickerKeyboardTabCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGStickerKeyboardTabCell.m; sourceTree = ""; }; + D07BC9931F2A481C00ED97AA /* TGStickerKeyboardTabSettingsCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGStickerKeyboardTabSettingsCell.h; sourceTree = ""; }; + D07BC9941F2A481C00ED97AA /* TGStickerKeyboardTabSettingsCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGStickerKeyboardTabSettingsCell.m; sourceTree = ""; }; + D07BC9971F2A489C00ED97AA /* TGStickerPack.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGStickerPack.h; sourceTree = ""; }; + D07BC9981F2A489C00ED97AA /* TGStickerPack.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGStickerPack.m; sourceTree = ""; }; + D07BC99B1F2A494000ED97AA /* TGStickerCollectionViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGStickerCollectionViewCell.h; sourceTree = ""; }; + D07BC99C1F2A494000ED97AA /* TGStickerCollectionViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGStickerCollectionViewCell.m; sourceTree = ""; }; + D07BC9A01F2A49C200ED97AA /* TGItemPreviewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGItemPreviewController.h; sourceTree = ""; }; + D07BC9A11F2A49C200ED97AA /* TGItemPreviewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGItemPreviewController.m; sourceTree = ""; }; + D07BC9A41F2A49E300ED97AA /* TGItemPreviewView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGItemPreviewView.h; sourceTree = ""; }; + D07BC9A51F2A49E300ED97AA /* TGItemPreviewView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGItemPreviewView.m; sourceTree = ""; }; + D07BC9A81F2A4A0700ED97AA /* TGStickerItemPreviewView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGStickerItemPreviewView.h; sourceTree = ""; }; + D07BC9A91F2A4A0700ED97AA /* TGStickerItemPreviewView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGStickerItemPreviewView.m; sourceTree = ""; }; + D07BC9AC1F2A4A5100ED97AA /* TGItemMenuSheetPreviewView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGItemMenuSheetPreviewView.h; sourceTree = ""; }; + D07BC9AD1F2A4A5100ED97AA /* TGItemMenuSheetPreviewView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGItemMenuSheetPreviewView.m; sourceTree = ""; }; + D07BC9B01F2A4B6600ED97AA /* TGStickerAssociation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGStickerAssociation.h; sourceTree = ""; }; + D07BC9B11F2A4B6600ED97AA /* TGStickerAssociation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGStickerAssociation.m; sourceTree = ""; }; + D07BC9B41F2A700900ED97AA /* TGPhotoMaskPosition.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPhotoMaskPosition.h; sourceTree = ""; }; + D07BC9B51F2A700900ED97AA /* TGPhotoMaskPosition.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPhotoMaskPosition.m; sourceTree = ""; }; + D07BC9B81F2A705D00ED97AA /* TGPhotoFilterCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPhotoFilterCell.h; sourceTree = ""; }; + D07BC9B91F2A705D00ED97AA /* TGPhotoFilterCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPhotoFilterCell.m; sourceTree = ""; }; + D07BC9BC1F2A722400ED97AA /* TGHistogramView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGHistogramView.h; sourceTree = ""; }; + D07BC9BD1F2A722400ED97AA /* TGHistogramView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGHistogramView.m; sourceTree = ""; }; + D07BC9C31F2A9A2B00ED97AA /* TGMediaPickerCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGMediaPickerCell.h; sourceTree = ""; }; + D07BC9C41F2A9A2B00ED97AA /* TGMediaPickerCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGMediaPickerCell.m; sourceTree = ""; }; + D07BC9C51F2A9A2B00ED97AA /* TGMediaPickerController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGMediaPickerController.h; sourceTree = ""; }; + D07BC9C61F2A9A2B00ED97AA /* TGMediaPickerController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGMediaPickerController.m; sourceTree = ""; }; + D07BC9C71F2A9A2B00ED97AA /* TGMediaPickerGalleryGifItem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGMediaPickerGalleryGifItem.h; sourceTree = ""; }; + D07BC9C81F2A9A2B00ED97AA /* TGMediaPickerGalleryGifItem.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGMediaPickerGalleryGifItem.m; sourceTree = ""; }; + D07BC9C91F2A9A2B00ED97AA /* TGMediaPickerGalleryGifItemView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGMediaPickerGalleryGifItemView.h; sourceTree = ""; }; + D07BC9CA1F2A9A2B00ED97AA /* TGMediaPickerGalleryGifItemView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGMediaPickerGalleryGifItemView.m; sourceTree = ""; }; + D07BC9CB1F2A9A2B00ED97AA /* TGMediaPickerGalleryInterfaceView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGMediaPickerGalleryInterfaceView.h; sourceTree = ""; }; + D07BC9CC1F2A9A2B00ED97AA /* TGMediaPickerGalleryInterfaceView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGMediaPickerGalleryInterfaceView.m; sourceTree = ""; }; + D07BC9CD1F2A9A2B00ED97AA /* TGMediaPickerGalleryItem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGMediaPickerGalleryItem.h; sourceTree = ""; }; + D07BC9CE1F2A9A2B00ED97AA /* TGMediaPickerGalleryItem.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGMediaPickerGalleryItem.m; sourceTree = ""; }; + D07BC9CF1F2A9A2B00ED97AA /* TGMediaPickerGalleryModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGMediaPickerGalleryModel.h; sourceTree = ""; }; + D07BC9D01F2A9A2B00ED97AA /* TGMediaPickerGalleryModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGMediaPickerGalleryModel.m; sourceTree = ""; }; + D07BC9D11F2A9A2B00ED97AA /* TGMediaPickerGalleryPhotoItem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGMediaPickerGalleryPhotoItem.h; sourceTree = ""; }; + D07BC9D21F2A9A2B00ED97AA /* TGMediaPickerGalleryPhotoItem.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGMediaPickerGalleryPhotoItem.m; sourceTree = ""; }; + D07BC9D31F2A9A2B00ED97AA /* TGMediaPickerGalleryPhotoItemView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGMediaPickerGalleryPhotoItemView.h; sourceTree = ""; }; + D07BC9D41F2A9A2B00ED97AA /* TGMediaPickerGalleryPhotoItemView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGMediaPickerGalleryPhotoItemView.m; sourceTree = ""; }; + D07BC9D51F2A9A2B00ED97AA /* TGMediaPickerGallerySelectedItemsModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGMediaPickerGallerySelectedItemsModel.h; sourceTree = ""; }; + D07BC9D61F2A9A2B00ED97AA /* TGMediaPickerGallerySelectedItemsModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGMediaPickerGallerySelectedItemsModel.m; sourceTree = ""; }; + D07BC9D71F2A9A2B00ED97AA /* TGMediaPickerGalleryVideoItem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGMediaPickerGalleryVideoItem.h; sourceTree = ""; }; + D07BC9D81F2A9A2B00ED97AA /* TGMediaPickerGalleryVideoItem.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGMediaPickerGalleryVideoItem.m; sourceTree = ""; }; + D07BC9D91F2A9A2B00ED97AA /* TGMediaPickerGalleryVideoItemView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGMediaPickerGalleryVideoItemView.h; sourceTree = ""; }; + D07BC9DA1F2A9A2B00ED97AA /* TGMediaPickerGalleryVideoItemView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGMediaPickerGalleryVideoItemView.m; sourceTree = ""; }; + D07BC9DB1F2A9A2B00ED97AA /* TGMediaPickerGalleryVideoScrubber.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGMediaPickerGalleryVideoScrubber.h; sourceTree = ""; }; + D07BC9DC1F2A9A2B00ED97AA /* TGMediaPickerGalleryVideoScrubber.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGMediaPickerGalleryVideoScrubber.m; sourceTree = ""; }; + D07BC9DD1F2A9A2B00ED97AA /* TGMediaPickerGalleryVideoScrubberThumbnailView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGMediaPickerGalleryVideoScrubberThumbnailView.h; sourceTree = ""; }; + D07BC9DE1F2A9A2B00ED97AA /* TGMediaPickerGalleryVideoScrubberThumbnailView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGMediaPickerGalleryVideoScrubberThumbnailView.m; sourceTree = ""; }; + D07BC9DF1F2A9A2B00ED97AA /* TGMediaPickerGalleryVideoTrimView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGMediaPickerGalleryVideoTrimView.h; sourceTree = ""; }; + D07BC9E01F2A9A2B00ED97AA /* TGMediaPickerGalleryVideoTrimView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGMediaPickerGalleryVideoTrimView.m; sourceTree = ""; }; + D07BC9E11F2A9A2B00ED97AA /* TGMediaPickerLayoutMetrics.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGMediaPickerLayoutMetrics.h; sourceTree = ""; }; + D07BC9E21F2A9A2B00ED97AA /* TGMediaPickerLayoutMetrics.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGMediaPickerLayoutMetrics.m; sourceTree = ""; }; + D07BC9E31F2A9A2B00ED97AA /* TGMediaPickerModernGalleryMixin.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGMediaPickerModernGalleryMixin.h; sourceTree = ""; }; + D07BC9E41F2A9A2B00ED97AA /* TGMediaPickerModernGalleryMixin.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGMediaPickerModernGalleryMixin.m; sourceTree = ""; }; + D07BC9E51F2A9A2B00ED97AA /* TGMediaPickerPhotoCounterButton.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGMediaPickerPhotoCounterButton.h; sourceTree = ""; }; + D07BC9E61F2A9A2B00ED97AA /* TGMediaPickerPhotoCounterButton.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGMediaPickerPhotoCounterButton.m; sourceTree = ""; }; + D07BC9E71F2A9A2B00ED97AA /* TGMediaPickerPhotoStripCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGMediaPickerPhotoStripCell.h; sourceTree = ""; }; + D07BC9E81F2A9A2B00ED97AA /* TGMediaPickerPhotoStripCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGMediaPickerPhotoStripCell.m; sourceTree = ""; }; + D07BC9E91F2A9A2B00ED97AA /* TGMediaPickerPhotoStripView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGMediaPickerPhotoStripView.h; sourceTree = ""; }; + D07BC9EA1F2A9A2B00ED97AA /* TGMediaPickerPhotoStripView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGMediaPickerPhotoStripView.m; sourceTree = ""; }; + D07BC9EB1F2A9A2B00ED97AA /* TGMediaPickerScrubberHeaderView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGMediaPickerScrubberHeaderView.h; sourceTree = ""; }; + D07BC9EC1F2A9A2B00ED97AA /* TGMediaPickerScrubberHeaderView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGMediaPickerScrubberHeaderView.m; sourceTree = ""; }; + D07BC9ED1F2A9A2B00ED97AA /* TGMediaPickerSelectionGestureRecognizer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGMediaPickerSelectionGestureRecognizer.h; sourceTree = ""; }; + D07BC9EE1F2A9A2B00ED97AA /* TGMediaPickerSelectionGestureRecognizer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGMediaPickerSelectionGestureRecognizer.m; sourceTree = ""; }; + D07BC9EF1F2A9A2B00ED97AA /* TGMediaPickerToolbarView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGMediaPickerToolbarView.h; sourceTree = ""; }; + D07BC9F01F2A9A2B00ED97AA /* TGMediaPickerToolbarView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGMediaPickerToolbarView.m; sourceTree = ""; }; + D07BCA1F1F2A9A5300ED97AA /* TGCheckButtonView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGCheckButtonView.h; sourceTree = ""; }; + D07BCA201F2A9A5300ED97AA /* TGCheckButtonView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGCheckButtonView.m; sourceTree = ""; }; + D07BCA231F2A9A9600ED97AA /* TGModernGalleryImageItem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGModernGalleryImageItem.h; sourceTree = ""; }; + D07BCA241F2A9A9600ED97AA /* TGModernGalleryImageItem.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGModernGalleryImageItem.m; sourceTree = ""; }; + D07BCA251F2A9A9600ED97AA /* TGModernGalleryImageItemImageView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGModernGalleryImageItemImageView.h; sourceTree = ""; }; + D07BCA261F2A9A9600ED97AA /* TGModernGalleryImageItemImageView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGModernGalleryImageItemImageView.m; sourceTree = ""; }; + D07BCA271F2A9A9600ED97AA /* TGModernGalleryImageItemView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGModernGalleryImageItemView.h; sourceTree = ""; }; + D07BCA281F2A9A9600ED97AA /* TGModernGalleryImageItemView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGModernGalleryImageItemView.m; sourceTree = ""; }; + D07BCA2F1F2A9AE700ED97AA /* TGModernGallerySelectableItem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGModernGallerySelectableItem.h; sourceTree = ""; }; + D07BCA311F2A9B0400ED97AA /* TGModernGalleryEditableItem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGModernGalleryEditableItem.h; sourceTree = ""; }; + D07BCA321F2A9B0400ED97AA /* TGModernGalleryEditableItemView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGModernGalleryEditableItemView.h; sourceTree = ""; }; + D07BCA351F2A9B6A00ED97AA /* TGMediaAsset+TGMediaEditableItem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "TGMediaAsset+TGMediaEditableItem.h"; sourceTree = ""; }; + D07BCA361F2A9B6A00ED97AA /* TGMediaAsset+TGMediaEditableItem.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "TGMediaAsset+TGMediaEditableItem.m"; sourceTree = ""; }; + D07BCA391F2A9BE600ED97AA /* TGModernGalleryVideoContentView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGModernGalleryVideoContentView.h; sourceTree = ""; }; + D07BCA3A1F2A9BE600ED97AA /* TGModernGalleryVideoContentView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGModernGalleryVideoContentView.m; sourceTree = ""; }; + D07BCA3D1F2A9C6600ED97AA /* TGModernMediaListItem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGModernMediaListItem.h; sourceTree = ""; }; + D07BCA3E1F2A9C6600ED97AA /* TGModernMediaListItemContentView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGModernMediaListItemContentView.h; sourceTree = ""; }; + D07BCA3F1F2A9C6600ED97AA /* TGModernMediaListItemContentView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGModernMediaListItemContentView.m; sourceTree = ""; }; + D07BCA401F2A9C6600ED97AA /* TGModernMediaListItemView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGModernMediaListItemView.h; sourceTree = ""; }; + D07BCA411F2A9C6600ED97AA /* TGModernMediaListItemView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGModernMediaListItemView.m; sourceTree = ""; }; + D07BCA471F2A9CE300ED97AA /* TGModernMediaListSelectableItem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGModernMediaListSelectableItem.h; sourceTree = ""; }; + D07BCA491F2A9DAB00ED97AA /* TGModernAnimatedImagePlayer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGModernAnimatedImagePlayer.h; sourceTree = ""; }; + D07BCA4A1F2A9DAB00ED97AA /* TGModernAnimatedImagePlayer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGModernAnimatedImagePlayer.m; sourceTree = ""; }; + D07BCA4D1F2A9DDD00ED97AA /* FLAnimatedImage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLAnimatedImage.h; sourceTree = ""; }; + D07BCA4E1F2A9DDD00ED97AA /* FLAnimatedImage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLAnimatedImage.m; sourceTree = ""; }; + D07BCA511F2A9E1600ED97AA /* TGDraggableCollectionView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGDraggableCollectionView.h; sourceTree = ""; }; + D07BCA521F2A9E1600ED97AA /* TGDraggableCollectionView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGDraggableCollectionView.m; sourceTree = ""; }; + D07BCA531F2A9E1600ED97AA /* TGDraggableCollectionViewFlowLayout.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGDraggableCollectionViewFlowLayout.h; sourceTree = ""; }; + D07BCA541F2A9E1600ED97AA /* TGDraggableCollectionViewFlowLayout.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGDraggableCollectionViewFlowLayout.m; sourceTree = ""; }; + D07BCA671F2B3CE700ED97AA /* TGCameraController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGCameraController.h; sourceTree = ""; }; + D07BCA681F2B3CE700ED97AA /* TGCameraController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGCameraController.m; sourceTree = ""; }; + D07BCA691F2B3CE700ED97AA /* TGCameraFocusCrosshairsControl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGCameraFocusCrosshairsControl.h; sourceTree = ""; }; + D07BCA6A1F2B3CE700ED97AA /* TGCameraFocusCrosshairsControl.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGCameraFocusCrosshairsControl.m; sourceTree = ""; }; + D07BCA701F2B443700ED97AA /* TGMediaAssetsController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGMediaAssetsController.h; sourceTree = ""; }; + D07BCA711F2B443700ED97AA /* TGMediaAssetsController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGMediaAssetsController.m; sourceTree = ""; }; + D07BCA721F2B443700ED97AA /* TGMediaAssetsGifCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGMediaAssetsGifCell.h; sourceTree = ""; }; + D07BCA731F2B443700ED97AA /* TGMediaAssetsGifCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGMediaAssetsGifCell.m; sourceTree = ""; }; + D07BCA741F2B443700ED97AA /* TGMediaAssetsMomentsCollectionLayout.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGMediaAssetsMomentsCollectionLayout.h; sourceTree = ""; }; + D07BCA751F2B443700ED97AA /* TGMediaAssetsMomentsCollectionLayout.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGMediaAssetsMomentsCollectionLayout.m; sourceTree = ""; }; + D07BCA761F2B443700ED97AA /* TGMediaAssetsMomentsCollectionView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGMediaAssetsMomentsCollectionView.h; sourceTree = ""; }; + D07BCA771F2B443700ED97AA /* TGMediaAssetsMomentsCollectionView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGMediaAssetsMomentsCollectionView.m; sourceTree = ""; }; + D07BCA781F2B443700ED97AA /* TGMediaAssetsMomentsController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGMediaAssetsMomentsController.h; sourceTree = ""; }; + D07BCA791F2B443700ED97AA /* TGMediaAssetsMomentsController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGMediaAssetsMomentsController.m; sourceTree = ""; }; + D07BCA7A1F2B443700ED97AA /* TGMediaAssetsMomentsSectionHeader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGMediaAssetsMomentsSectionHeader.h; sourceTree = ""; }; + D07BCA7B1F2B443700ED97AA /* TGMediaAssetsMomentsSectionHeader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGMediaAssetsMomentsSectionHeader.m; sourceTree = ""; }; + D07BCA7C1F2B443700ED97AA /* TGMediaAssetsMomentsSectionHeaderView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGMediaAssetsMomentsSectionHeaderView.h; sourceTree = ""; }; + D07BCA7D1F2B443700ED97AA /* TGMediaAssetsMomentsSectionHeaderView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGMediaAssetsMomentsSectionHeaderView.m; sourceTree = ""; }; + D07BCA7E1F2B443700ED97AA /* TGMediaAssetsPhotoCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGMediaAssetsPhotoCell.h; sourceTree = ""; }; + D07BCA7F1F2B443700ED97AA /* TGMediaAssetsPhotoCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGMediaAssetsPhotoCell.m; sourceTree = ""; }; + D07BCA801F2B443700ED97AA /* TGMediaAssetsPickerController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGMediaAssetsPickerController.h; sourceTree = ""; }; + D07BCA811F2B443700ED97AA /* TGMediaAssetsPickerController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGMediaAssetsPickerController.m; sourceTree = ""; }; + D07BCA821F2B443700ED97AA /* TGMediaAssetsTipView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGMediaAssetsTipView.h; sourceTree = ""; }; + D07BCA831F2B443700ED97AA /* TGMediaAssetsTipView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGMediaAssetsTipView.m; sourceTree = ""; }; + D07BCA841F2B443700ED97AA /* TGMediaAssetsUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGMediaAssetsUtils.h; sourceTree = ""; }; + D07BCA851F2B443700ED97AA /* TGMediaAssetsUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGMediaAssetsUtils.m; sourceTree = ""; }; + D07BCA861F2B443700ED97AA /* TGMediaAssetsVideoCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGMediaAssetsVideoCell.h; sourceTree = ""; }; + D07BCA871F2B443700ED97AA /* TGMediaAssetsVideoCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGMediaAssetsVideoCell.m; sourceTree = ""; }; + D07BCAA01F2B445E00ED97AA /* TGMediaGroupCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGMediaGroupCell.h; sourceTree = ""; }; + D07BCAA11F2B445E00ED97AA /* TGMediaGroupCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGMediaGroupCell.m; sourceTree = ""; }; + D07BCAA21F2B445E00ED97AA /* TGMediaGroupsController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGMediaGroupsController.h; sourceTree = ""; }; + D07BCAA31F2B445E00ED97AA /* TGMediaGroupsController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGMediaGroupsController.m; sourceTree = ""; }; + D07BCAA81F2B44C100ED97AA /* TGModernBarButton.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGModernBarButton.h; sourceTree = ""; }; + D07BCAA91F2B44C100ED97AA /* TGModernBarButton.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGModernBarButton.m; sourceTree = ""; }; + D07BCAAC1F2B45DA00ED97AA /* TGFileUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGFileUtils.h; sourceTree = ""; }; + D07BCAAD1F2B45DA00ED97AA /* TGFileUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGFileUtils.m; sourceTree = ""; }; + D07BCAB01F2B460B00ED97AA /* TGGifConverter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGGifConverter.h; sourceTree = ""; }; + D07BCAB11F2B460B00ED97AA /* TGGifConverter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGGifConverter.m; sourceTree = ""; }; + D07BCAB51F2B4DE200ED97AA /* TGAttachmentCarouselItemView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGAttachmentCarouselItemView.h; sourceTree = ""; }; + D07BCAB61F2B4DE200ED97AA /* TGAttachmentCarouselItemView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGAttachmentCarouselItemView.m; sourceTree = ""; }; + D07BCAB91F2B4E2600ED97AA /* TGTransitionLayout.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGTransitionLayout.h; sourceTree = ""; }; + D07BCABA1F2B4E2600ED97AA /* TGTransitionLayout.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGTransitionLayout.m; sourceTree = ""; }; + D07BCABD1F2B4E3900ED97AA /* UICollectionView+TGTransitioning.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UICollectionView+TGTransitioning.h"; sourceTree = ""; }; + D07BCABE1F2B4E3900ED97AA /* UICollectionView+TGTransitioning.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UICollectionView+TGTransitioning.m"; sourceTree = ""; }; + D07BCAC11F2B4E6200ED97AA /* TGAttachmentCameraCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGAttachmentCameraCell.h; sourceTree = ""; }; + D07BCAC21F2B4E6200ED97AA /* TGAttachmentCameraCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGAttachmentCameraCell.m; sourceTree = ""; }; + D07BCAC31F2B4E6200ED97AA /* TGAttachmentCameraView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGAttachmentCameraView.h; sourceTree = ""; }; + D07BCAC41F2B4E6200ED97AA /* TGAttachmentCameraView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGAttachmentCameraView.m; sourceTree = ""; }; + D07BCAC51F2B4E6200ED97AA /* TGMediaAvatarMenuMixin.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGMediaAvatarMenuMixin.m; sourceTree = ""; }; + D07BCACB1F2B4E7300ED97AA /* TGMediaAvatarMenuMixin.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGMediaAvatarMenuMixin.h; sourceTree = ""; }; + D07BCACD1F2B4E9000ED97AA /* TGAttachmentMenuCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGAttachmentMenuCell.h; sourceTree = ""; }; + D07BCACE1F2B4E9000ED97AA /* TGAttachmentMenuCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGAttachmentMenuCell.m; sourceTree = ""; }; + D07BCAD51F2B4F2800ED97AA /* TGOverlayFormsheetController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TGOverlayFormsheetController.h; path = LegacyComponents/TGOverlayFormsheetController.h; sourceTree = ""; }; + D07BCAD61F2B4F2800ED97AA /* TGOverlayFormsheetController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TGOverlayFormsheetController.m; path = LegacyComponents/TGOverlayFormsheetController.m; sourceTree = ""; }; + D07BCAD71F2B4F2800ED97AA /* TGOverlayFormsheetWindow.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TGOverlayFormsheetWindow.h; path = LegacyComponents/TGOverlayFormsheetWindow.h; sourceTree = ""; }; + D07BCAD81F2B4F2800ED97AA /* TGOverlayFormsheetWindow.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TGOverlayFormsheetWindow.m; path = LegacyComponents/TGOverlayFormsheetWindow.m; sourceTree = ""; }; + D07BCADD1F2B4F5E00ED97AA /* TGLegacyCameraController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGLegacyCameraController.h; sourceTree = ""; }; + D07BCADE1F2B4F5E00ED97AA /* TGLegacyCameraController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGLegacyCameraController.m; sourceTree = ""; }; + D07BCAE11F2B502F00ED97AA /* TGImagePickerController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGImagePickerController.h; sourceTree = ""; }; + D07BCAE21F2B502F00ED97AA /* TGImagePickerController.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = TGImagePickerController.mm; sourceTree = ""; }; + D07BCAE51F2B507600ED97AA /* TGAttachmentGifCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGAttachmentGifCell.h; sourceTree = ""; }; + D07BCAE61F2B507600ED97AA /* TGAttachmentGifCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGAttachmentGifCell.m; sourceTree = ""; }; + D07BCAE71F2B507600ED97AA /* TGAttachmentVideoCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGAttachmentVideoCell.h; sourceTree = ""; }; + D07BCAE81F2B507600ED97AA /* TGAttachmentVideoCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGAttachmentVideoCell.m; sourceTree = ""; }; + D07BCAE91F2B507600ED97AA /* TGAttachmentPhotoCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGAttachmentPhotoCell.h; sourceTree = ""; }; + D07BCAEA1F2B507600ED97AA /* TGAttachmentPhotoCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGAttachmentPhotoCell.m; sourceTree = ""; }; + D07BCAF11F2B509200ED97AA /* TGAttachmentAssetCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGAttachmentAssetCell.h; sourceTree = ""; }; + D07BCAF21F2B509200ED97AA /* TGAttachmentAssetCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGAttachmentAssetCell.m; sourceTree = ""; }; + D07BCAF51F2B50AA00ED97AA /* TGMediaAvatarEditorTransition.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGMediaAvatarEditorTransition.h; sourceTree = ""; }; + D07BCAF61F2B50AA00ED97AA /* TGMediaAvatarEditorTransition.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGMediaAvatarEditorTransition.m; sourceTree = ""; }; + D07BCAF91F2B517900ED97AA /* TGLegacyMediaPickerTipView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGLegacyMediaPickerTipView.h; sourceTree = ""; }; + D07BCAFA1F2B517900ED97AA /* TGLegacyMediaPickerTipView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGLegacyMediaPickerTipView.m; sourceTree = ""; }; + D07BCB011F2B546400ED97AA /* LegacyComponentsContext.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LegacyComponentsContext.m; sourceTree = ""; }; + D07BCB041F2B646A00ED97AA /* TGPasswordEntryView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPasswordEntryView.h; sourceTree = ""; }; + D07BCB051F2B646A00ED97AA /* TGPasswordEntryView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPasswordEntryView.m; sourceTree = ""; }; + D07BCB061F2B646A00ED97AA /* TGPasscodeEntryController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPasscodeEntryController.h; sourceTree = ""; }; + D07BCB071F2B646A00ED97AA /* TGPasscodeEntryController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPasscodeEntryController.m; sourceTree = ""; }; + D07BCB081F2B646A00ED97AA /* TGPasscodeEntryKeyboardView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPasscodeEntryKeyboardView.h; sourceTree = ""; }; + D07BCB091F2B646A00ED97AA /* TGPasscodeEntryKeyboardView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPasscodeEntryKeyboardView.m; sourceTree = ""; }; + D07BCB0A1F2B646A00ED97AA /* TGPasscodePinDotView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPasscodePinDotView.h; sourceTree = ""; }; + D07BCB0B1F2B646A00ED97AA /* TGPasscodePinDotView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPasscodePinDotView.m; sourceTree = ""; }; + D07BCB0C1F2B646A00ED97AA /* TGPasscodePinView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPasscodePinView.h; sourceTree = ""; }; + D07BCB0D1F2B646A00ED97AA /* TGPasscodePinView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPasscodePinView.m; sourceTree = ""; }; + D07BCB0E1F2B646A00ED97AA /* TGPasscodeButtonView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPasscodeButtonView.h; sourceTree = ""; }; + D07BCB0F1F2B646A00ED97AA /* TGPasscodeButtonView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPasscodeButtonView.m; sourceTree = ""; }; + D07BCB101F2B646A00ED97AA /* TGPasscodeBackground.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPasscodeBackground.h; sourceTree = ""; }; + D07BCB111F2B646A00ED97AA /* TGTextField.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGTextField.h; sourceTree = ""; }; + D07BCB121F2B646A00ED97AA /* TGTextField.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGTextField.m; sourceTree = ""; }; + D07BCB131F2B646A00ED97AA /* TGDefaultPasscodeBackground.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGDefaultPasscodeBackground.h; sourceTree = ""; }; + D07BCB141F2B646A00ED97AA /* TGDefaultPasscodeBackground.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGDefaultPasscodeBackground.m; sourceTree = ""; }; + D07BCB261F2B652C00ED97AA /* TGImageBasedPasscodeBackground.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGImageBasedPasscodeBackground.h; sourceTree = ""; }; + D07BCB271F2B652C00ED97AA /* TGImageBasedPasscodeBackground.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGImageBasedPasscodeBackground.m; sourceTree = ""; }; + D07BCB2B1F2B65F100ED97AA /* TGBuiltinWallpaperInfo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGBuiltinWallpaperInfo.h; sourceTree = ""; }; + D07BCB2C1F2B65F100ED97AA /* TGBuiltinWallpaperInfo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGBuiltinWallpaperInfo.m; sourceTree = ""; }; + D07BCB2D1F2B65F100ED97AA /* TGColorWallpaperInfo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGColorWallpaperInfo.h; sourceTree = ""; }; + D07BCB2E1F2B65F100ED97AA /* TGColorWallpaperInfo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGColorWallpaperInfo.m; sourceTree = ""; }; + D07BCB2F1F2B65F100ED97AA /* TGCustomImageWallpaperInfo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGCustomImageWallpaperInfo.h; sourceTree = ""; }; + D07BCB301F2B65F100ED97AA /* TGCustomImageWallpaperInfo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGCustomImageWallpaperInfo.m; sourceTree = ""; }; + D07BCB311F2B65F100ED97AA /* TGRemoteWallpaperInfo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGRemoteWallpaperInfo.h; sourceTree = ""; }; + D07BCB321F2B65F100ED97AA /* TGRemoteWallpaperInfo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGRemoteWallpaperInfo.m; sourceTree = ""; }; + D07BCB331F2B65F100ED97AA /* TGWallpaperInfo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGWallpaperInfo.h; sourceTree = ""; }; + D07BCB341F2B65F100ED97AA /* TGWallpaperInfo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGWallpaperInfo.m; sourceTree = ""; }; + D07BCB401F2B6A5600ED97AA /* TGPIPAblePlayerView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPIPAblePlayerView.h; sourceTree = ""; }; + D07BCB411F2B6A5600ED97AA /* TGEmbedInstagramPlayerView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGEmbedInstagramPlayerView.h; sourceTree = ""; }; + D07BCB421F2B6A5600ED97AA /* TGEmbedInstagramPlayerView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGEmbedInstagramPlayerView.m; sourceTree = ""; }; + D07BCB431F2B6A5600ED97AA /* TGEmbedPIPButton.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGEmbedPIPButton.h; sourceTree = ""; }; + D07BCB441F2B6A5600ED97AA /* TGEmbedPIPButton.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGEmbedPIPButton.m; sourceTree = ""; }; + D07BCB451F2B6A5600ED97AA /* TGEmbedPIPPullArrowView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGEmbedPIPPullArrowView.h; sourceTree = ""; }; + D07BCB461F2B6A5600ED97AA /* TGEmbedPIPPullArrowView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGEmbedPIPPullArrowView.m; sourceTree = ""; }; + D07BCB471F2B6A5600ED97AA /* TGEmbedPlayerControls.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGEmbedPlayerControls.h; sourceTree = ""; }; + D07BCB481F2B6A5600ED97AA /* TGEmbedPlayerControls.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGEmbedPlayerControls.m; sourceTree = ""; }; + D07BCB491F2B6A5600ED97AA /* TGEmbedPlayerScrubber.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGEmbedPlayerScrubber.h; sourceTree = ""; }; + D07BCB4A1F2B6A5600ED97AA /* TGEmbedPlayerScrubber.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGEmbedPlayerScrubber.m; sourceTree = ""; }; + D07BCB4B1F2B6A5600ED97AA /* TGEmbedPlayerState.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGEmbedPlayerState.h; sourceTree = ""; }; + D07BCB4C1F2B6A5600ED97AA /* TGEmbedPlayerState.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGEmbedPlayerState.m; sourceTree = ""; }; + D07BCB4D1F2B6A5600ED97AA /* TGEmbedPlayerView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGEmbedPlayerView.h; sourceTree = ""; }; + D07BCB4E1F2B6A5600ED97AA /* TGEmbedPlayerView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGEmbedPlayerView.m; sourceTree = ""; }; + D07BCB4F1F2B6A5600ED97AA /* TGEmbedSoundCloudPlayerView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGEmbedSoundCloudPlayerView.h; sourceTree = ""; }; + D07BCB501F2B6A5600ED97AA /* TGEmbedSoundCloudPlayerView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGEmbedSoundCloudPlayerView.m; sourceTree = ""; }; + D07BCB511F2B6A5600ED97AA /* TGEmbedVideoPlayerView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGEmbedVideoPlayerView.h; sourceTree = ""; }; + D07BCB521F2B6A5600ED97AA /* TGEmbedVideoPlayerView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGEmbedVideoPlayerView.m; sourceTree = ""; }; + D07BCB531F2B6A5600ED97AA /* TGEmbedVimeoPlayerView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGEmbedVimeoPlayerView.h; sourceTree = ""; }; + D07BCB541F2B6A5600ED97AA /* TGEmbedVimeoPlayerView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGEmbedVimeoPlayerView.m; sourceTree = ""; }; + D07BCB551F2B6A5600ED97AA /* TGEmbedVinePlayerView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGEmbedVinePlayerView.h; sourceTree = ""; }; + D07BCB561F2B6A5600ED97AA /* TGEmbedVinePlayerView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGEmbedVinePlayerView.m; sourceTree = ""; }; + D07BCB571F2B6A5600ED97AA /* TGEmbedVKPlayerView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGEmbedVKPlayerView.h; sourceTree = ""; }; + D07BCB581F2B6A5600ED97AA /* TGEmbedVKPlayerView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGEmbedVKPlayerView.m; sourceTree = ""; }; + D07BCB591F2B6A5600ED97AA /* TGEmbedYoutubePlayerView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGEmbedYoutubePlayerView.h; sourceTree = ""; }; + D07BCB5A1F2B6A5600ED97AA /* TGEmbedYoutubePlayerView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGEmbedYoutubePlayerView.m; sourceTree = ""; }; + D07BCB761F2B6DB900ED97AA /* TGEmbedCoubPlayerView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGEmbedCoubPlayerView.h; sourceTree = ""; }; + D07BCB771F2B6DB900ED97AA /* TGEmbedCoubPlayerView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGEmbedCoubPlayerView.m; sourceTree = ""; }; + D07BCB7B1F2B6F6300ED97AA /* AVAsset+CBExtension.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "AVAsset+CBExtension.h"; sourceTree = ""; }; + D07BCB7C1F2B6F6300ED97AA /* AVAsset+CBExtension.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "AVAsset+CBExtension.m"; sourceTree = ""; }; + D07BCB7D1F2B6F6300ED97AA /* CBAssetDownloadManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CBAssetDownloadManager.h; sourceTree = ""; }; + D07BCB7E1F2B6F6300ED97AA /* CBAssetDownloadManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CBAssetDownloadManager.m; sourceTree = ""; }; + D07BCB7F1F2B6F6300ED97AA /* CBChunkDownloadOperation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CBChunkDownloadOperation.h; sourceTree = ""; }; + D07BCB801F2B6F6300ED97AA /* CBChunkDownloadOperation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CBChunkDownloadOperation.m; sourceTree = ""; }; + D07BCB811F2B6F6300ED97AA /* CBConstance.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CBConstance.h; sourceTree = ""; }; + D07BCB821F2B6F6300ED97AA /* CBConstance.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CBConstance.m; sourceTree = ""; }; + D07BCB831F2B6F6300ED97AA /* CBCoubAsset.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CBCoubAsset.h; sourceTree = ""; }; + D07BCB841F2B6F6300ED97AA /* CBCoubAudioSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CBCoubAudioSource.h; sourceTree = ""; }; + D07BCB851F2B6F6300ED97AA /* CBCoubAudioSource.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CBCoubAudioSource.m; sourceTree = ""; }; + D07BCB861F2B6F6300ED97AA /* CBCoubAuthorVO.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CBCoubAuthorVO.h; sourceTree = ""; }; + D07BCB871F2B6F6300ED97AA /* CBCoubAuthorVO.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CBCoubAuthorVO.m; sourceTree = ""; }; + D07BCB881F2B6F6300ED97AA /* CBCoubDownloadOperation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CBCoubDownloadOperation.h; sourceTree = ""; }; + D07BCB891F2B6F6300ED97AA /* CBCoubDownloadOperation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CBCoubDownloadOperation.m; sourceTree = ""; }; + D07BCB8A1F2B6F6300ED97AA /* CBCoubLoopCompositionMaker.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CBCoubLoopCompositionMaker.h; sourceTree = ""; }; + D07BCB8B1F2B6F6300ED97AA /* CBCoubLoopCompositionMaker.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CBCoubLoopCompositionMaker.m; sourceTree = ""; }; + D07BCB8C1F2B6F6300ED97AA /* CBCoubNew.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CBCoubNew.h; sourceTree = ""; }; + D07BCB8D1F2B6F6300ED97AA /* CBCoubNew.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CBCoubNew.m; sourceTree = ""; }; + D07BCB8E1F2B6F6300ED97AA /* CBCoubPlayer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CBCoubPlayer.h; sourceTree = ""; }; + D07BCB8F1F2B6F6300ED97AA /* CBCoubPlayer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CBCoubPlayer.m; sourceTree = ""; }; + D07BCB901F2B6F6300ED97AA /* CBCoubPlayerContance.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CBCoubPlayerContance.h; sourceTree = ""; }; + D07BCB911F2B6F6300ED97AA /* CBCoubPlayerContance.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CBCoubPlayerContance.m; sourceTree = ""; }; + D07BCB921F2B6F6300ED97AA /* CBCoubVideoSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CBCoubVideoSource.h; sourceTree = ""; }; + D07BCB931F2B6F6300ED97AA /* CBCoubVideoSource.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CBCoubVideoSource.m; sourceTree = ""; }; + D07BCB941F2B6F6300ED97AA /* CBDownloadOperation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CBDownloadOperation.h; sourceTree = ""; }; + D07BCB951F2B6F6300ED97AA /* CBDownloadOperationDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CBDownloadOperationDelegate.h; sourceTree = ""; }; + D07BCB961F2B6F6300ED97AA /* CBGenericDownloadOperation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CBGenericDownloadOperation.h; sourceTree = ""; }; + D07BCB971F2B6F6300ED97AA /* CBGenericDownloadOperation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CBGenericDownloadOperation.m; sourceTree = ""; }; + D07BCB981F2B6F6300ED97AA /* CBJSONCoubMapper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CBJSONCoubMapper.h; sourceTree = ""; }; + D07BCB991F2B6F6300ED97AA /* CBJSONCoubMapper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CBJSONCoubMapper.m; sourceTree = ""; }; + D07BCB9A1F2B6F6300ED97AA /* CBLibrary.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CBLibrary.h; sourceTree = ""; }; + D07BCB9B1F2B6F6300ED97AA /* CBLibrary.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CBLibrary.m; sourceTree = ""; }; + D07BCB9C1F2B6F6300ED97AA /* CBPlayerLayerView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CBPlayerLayerView.h; sourceTree = ""; }; + D07BCB9D1F2B6F6300ED97AA /* CBPlayerLayerView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CBPlayerLayerView.m; sourceTree = ""; }; + D07BCB9E1F2B6F6300ED97AA /* CBPlayerView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CBPlayerView.h; sourceTree = ""; }; + D07BCB9F1F2B6F6300ED97AA /* CBPlayerView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CBPlayerView.m; sourceTree = ""; }; + D07BCBA01F2B6F6300ED97AA /* CBTagNew.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CBTagNew.h; sourceTree = ""; }; + D07BCBA11F2B6F6300ED97AA /* CBTagNew.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CBTagNew.m; sourceTree = ""; }; + D07BCBA21F2B6F6300ED97AA /* CBVideoPlayer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CBVideoPlayer.h; sourceTree = ""; }; + D07BCBA31F2B6F6300ED97AA /* CBVideoPlayer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CBVideoPlayer.m; sourceTree = ""; }; + D07BCBA41F2B6F6300ED97AA /* NSDictionary+CBExtensions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSDictionary+CBExtensions.h"; sourceTree = ""; }; + D07BCBA51F2B6F6300ED97AA /* NSDictionary+CBExtensions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSDictionary+CBExtensions.m"; sourceTree = ""; }; + D07BCBD11F2B6FFE00ED97AA /* LegacyHTTPRequestOperation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LegacyHTTPRequestOperation.h; sourceTree = ""; }; + D07BCBD51F2B72BD00ED97AA /* NSMutableArray+STKAudioPlayer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSMutableArray+STKAudioPlayer.h"; sourceTree = ""; }; + D07BCBD61F2B72BD00ED97AA /* NSMutableArray+STKAudioPlayer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSMutableArray+STKAudioPlayer.m"; sourceTree = ""; }; + D07BCBD71F2B72BD00ED97AA /* STKAudioPlayer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = STKAudioPlayer.h; sourceTree = ""; }; + D07BCBD81F2B72BD00ED97AA /* STKAudioPlayer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = STKAudioPlayer.m; sourceTree = ""; }; + D07BCBDD1F2B72DC00ED97AA /* STKAutoRecoveringHTTPDataSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = STKAutoRecoveringHTTPDataSource.h; sourceTree = ""; }; + D07BCBDE1F2B72DC00ED97AA /* STKAutoRecoveringHTTPDataSource.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = STKAutoRecoveringHTTPDataSource.m; sourceTree = ""; }; + D07BCBDF1F2B72DC00ED97AA /* STKCoreFoundationDataSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = STKCoreFoundationDataSource.h; sourceTree = ""; }; + D07BCBE01F2B72DC00ED97AA /* STKCoreFoundationDataSource.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = STKCoreFoundationDataSource.m; sourceTree = ""; }; + D07BCBE11F2B72DC00ED97AA /* STKDataSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = STKDataSource.h; sourceTree = ""; }; + D07BCBE21F2B72DC00ED97AA /* STKDataSource.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = STKDataSource.m; sourceTree = ""; }; + D07BCBE31F2B72DC00ED97AA /* STKDataSourceWrapper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = STKDataSourceWrapper.h; sourceTree = ""; }; + D07BCBE41F2B72DC00ED97AA /* STKDataSourceWrapper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = STKDataSourceWrapper.m; sourceTree = ""; }; + D07BCBE51F2B72DC00ED97AA /* STKHTTPDataSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = STKHTTPDataSource.h; sourceTree = ""; }; + D07BCBE61F2B72DC00ED97AA /* STKHTTPDataSource.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = STKHTTPDataSource.m; sourceTree = ""; }; + D07BCBE71F2B72DC00ED97AA /* STKLocalFileDataSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = STKLocalFileDataSource.h; sourceTree = ""; }; + D07BCBE81F2B72DC00ED97AA /* STKLocalFileDataSource.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = STKLocalFileDataSource.m; sourceTree = ""; }; + D07BCBE91F2B72DC00ED97AA /* STKQueueEntry.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = STKQueueEntry.h; sourceTree = ""; }; + D07BCBEA1F2B72DC00ED97AA /* STKQueueEntry.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = STKQueueEntry.m; sourceTree = ""; }; + D07BCBF91F2B757700ED97AA /* TGEmbedPIPScrubber.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGEmbedPIPScrubber.h; sourceTree = ""; }; + D07BCBFA1F2B757700ED97AA /* TGEmbedPIPScrubber.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGEmbedPIPScrubber.m; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -891,17 +2165,31 @@ D017797F1F2107B80044446D /* LMDB */, D017776D1F1F91850044446D /* PS Coding */, D017776C1F1F91780044446D /* Peers */, + D07BC9451F2A3E9900ED97AA /* Suggestions */, D01777841F1F93430044446D /* Message */, D0177AC01F23D9160044446D /* ActionStage */, D01778AF1F1FFF6E0044446D /* Basic UI Components */, + D07BC8781F2A363800ED97AA /* Progress Window */, + D07BC8451F2A2D9700ED97AA /* Menu Sheet */, + D07BC8671F2A2F2B00ED97AA /* HPGrowingTextView */, + D07BC9761F2A470000ED97AA /* Sticker Keyboard */, + D07BC99F1F2A49AD00ED97AA /* Item Preview */, D0177AE81F23DF040044446D /* Image Manager */, D01778C81F200B810044446D /* Navigation UI Components */, D01779281F20FEC90044446D /* Assets Library */, D0177A451F21F6E50044446D /* Gallery */, + D07BC7911F2A2B7000ED97AA /* GPUImage */, D01779551F21030E0044446D /* Photo Editor Utils */, D0177A251F2144540044446D /* Photo Editor */, D01779661F2103C60044446D /* Photo Paint */, D0177B031F26419E0044446D /* Camera */, + D07BC8621F2A2F0500ED97AA /* Media Picker */, + D07BCA6F1F2B441F00ED97AA /* Media Assets Picker */, + D07BCAB41F2B4DD600ED97AA /* Attachment Menu */, + D07BC82C1F2A2CFE00ED97AA /* Secret Timer */, + D07BCB031F2B63D700ED97AA /* Passcode */, + D07BCB2A1F2B65C400ED97AA /* Wallpapers */, + D07BCB3F1F2B69D400ED97AA /* Embed Video */, D017772A1F1F8F100044446D /* LegacyComponents.h */, D017772B1F1F8F100044446D /* Info.plist */, ); @@ -948,6 +2236,8 @@ D017789B1F1FC99A0044446D /* LegacyComponentsInternal.h */, D017789C1F1FC99A0044446D /* LegacyComponentsInternal.m */, D0177A1D1F21403E0044446D /* LegacyComponentsContext.h */, + D07BCB011F2B546400ED97AA /* LegacyComponentsContext.m */, + D07BCAAD1F2B45DA00ED97AA /* TGFileUtils.m */, D01777491F1F8FE60044446D /* TGLocalization.h */, D017774A1F1F8FE60044446D /* TGLocalization.m */, D017774B1F1F8FE60044446D /* TGPluralization.h */, @@ -993,6 +2283,13 @@ D0177A9E1F2222990044446D /* TGKeyCommand.m */, D0177A9F1F2222990044446D /* TGKeyCommandController.h */, D0177AA01F2222990044446D /* TGKeyCommandController.m */, + D07BC8741F2A2F7B00ED97AA /* TGWeakDelegate.h */, + D07BC8751F2A2F7B00ED97AA /* TGWeakDelegate.m */, + D07BC9291F2A3A3F00ED97AA /* matrix.h */, + D07BC92A1F2A3A3F00ED97AA /* matrix.m */, + D07BC9661F2A3F5C00ED97AA /* TGCache.h */, + D07BC9671F2A3F5C00ED97AA /* TGCache.m */, + D07BCAAC1F2B45DA00ED97AA /* TGFileUtils.h */, ); name = Utils; sourceTree = ""; @@ -1112,6 +2409,12 @@ D01777811F1F93250044446D /* TGImageInfo.mm */, D01777851F1F93550044446D /* TGMessage.h */, D01777861F1F93550044446D /* TGMessage.mm */, + D07BC9971F2A489C00ED97AA /* TGStickerPack.h */, + D07BC9981F2A489C00ED97AA /* TGStickerPack.m */, + D07BC9B01F2A4B6600ED97AA /* TGStickerAssociation.h */, + D07BC9B11F2A4B6600ED97AA /* TGStickerAssociation.m */, + D07BC9B41F2A700900ED97AA /* TGPhotoMaskPosition.h */, + D07BC9B51F2A700900ED97AA /* TGPhotoMaskPosition.m */, ); name = Message; sourceTree = ""; @@ -1147,6 +2450,32 @@ D0177A781F2204360044446D /* TGModernToolbarButton.m */, D0177A731F2202260044446D /* TGModernBackToolbarButton.h */, D0177A741F2202260044446D /* TGModernBackToolbarButton.m */, + D07BC6D51F2A18FE00ED97AA /* UIControl+HitTestEdgeInsets.h */, + D07BC6D61F2A18FE00ED97AA /* UIControl+HitTestEdgeInsets.m */, + D07BC6FD1F2A1A7700ED97AA /* TGMenuView.h */, + D07BC6FE1F2A1A7700ED97AA /* TGMenuView.m */, + D07BC7111F2A269400ED97AA /* TGImageView.h */, + D07BC7121F2A269400ED97AA /* TGImageView.m */, + D07BC7251F2A2A5300ED97AA /* UICollectionView+Utils.h */, + D07BC7261F2A2A5300ED97AA /* UICollectionView+Utils.m */, + D07BC8811F2A367500ED97AA /* TGActivityIndicatorView.h */, + D07BC8821F2A367500ED97AA /* TGActivityIndicatorView.m */, + D07BC93D1F2A3DB900ED97AA /* TGMessageImageViewOverlayView.h */, + D07BC93E1F2A3DB900ED97AA /* TGMessageImageViewOverlayView.m */, + D07BC95A1F2A3EF000ED97AA /* TGLetteredAvatarView.h */, + D07BC95B1F2A3EF000ED97AA /* TGLetteredAvatarView.m */, + D07BC95E1F2A3F0A00ED97AA /* TGGradientLabel.h */, + D07BC95F1F2A3F0A00ED97AA /* TGGradientLabel.m */, + D07BC9621F2A3F4000ED97AA /* TGRemoteImageView.h */, + D07BC9631F2A3F4000ED97AA /* TGRemoteImageView.m */, + D07BCA1F1F2A9A5300ED97AA /* TGCheckButtonView.h */, + D07BCA201F2A9A5300ED97AA /* TGCheckButtonView.m */, + D07BCA511F2A9E1600ED97AA /* TGDraggableCollectionView.h */, + D07BCA521F2A9E1600ED97AA /* TGDraggableCollectionView.m */, + D07BCA531F2A9E1600ED97AA /* TGDraggableCollectionViewFlowLayout.h */, + D07BCA541F2A9E1600ED97AA /* TGDraggableCollectionViewFlowLayout.m */, + D07BCAA81F2B44C100ED97AA /* TGModernBarButton.h */, + D07BCAA91F2B44C100ED97AA /* TGModernBarButton.m */, ); name = "Basic UI Components"; sourceTree = ""; @@ -1171,6 +2500,10 @@ D01778E31F20CAE60044446D /* TGOverlayControllerWindow.h */, D01778E41F20CAE60044446D /* TGOverlayControllerWindow.m */, D01778F11F20CC7A0044446D /* TGRootControllerProtocol.h */, + D07BCAD51F2B4F2800ED97AA /* TGOverlayFormsheetController.h */, + D07BCAD61F2B4F2800ED97AA /* TGOverlayFormsheetController.m */, + D07BCAD71F2B4F2800ED97AA /* TGOverlayFormsheetWindow.h */, + D07BCAD81F2B4F2800ED97AA /* TGOverlayFormsheetWindow.m */, ); name = "Navigation UI Components"; path = ..; @@ -1215,6 +2548,8 @@ D017794E1F2100280044446D /* TGMediaSelectionContext.m */, D01779511F2100520044446D /* TGMediaEditingContext.h */, D01779521F2100520044446D /* TGMediaEditingContext.m */, + D07BCA351F2A9B6A00ED97AA /* TGMediaAsset+TGMediaEditableItem.h */, + D07BCA361F2A9B6A00ED97AA /* TGMediaAsset+TGMediaEditableItem.m */, ); name = "Assets Library"; sourceTree = ""; @@ -1234,6 +2569,8 @@ D0177A311F21F1980044446D /* UIImage+TGMediaEditableItem.m */, D0177A411F21F62A0044446D /* TGMediaVideoConverter.h */, D0177A421F21F62A0044446D /* TGMediaVideoConverter.m */, + D07BCAB01F2B460B00ED97AA /* TGGifConverter.h */, + D07BCAB11F2B460B00ED97AA /* TGGifConverter.m */, ); name = "Photo Editor Utils"; sourceTree = ""; @@ -1251,6 +2588,90 @@ D017796A1F2103DB0044446D /* TGPhotoPaintStickerEntity.m */, D017796F1F2103FD0044446D /* TGPaintUndoManager.h */, D01779701F2103FD0044446D /* TGPaintUndoManager.m */, + D07BC8A91F2A37EC00ED97AA /* TGPhotoPaintActionsView.h */, + D07BC8AA1F2A37EC00ED97AA /* TGPhotoPaintActionsView.m */, + D07BC8AB1F2A37EC00ED97AA /* TGPhotoPaintColorPicker.h */, + D07BC8AC1F2A37EC00ED97AA /* TGPhotoPaintColorPicker.m */, + D07BC8AD1F2A37EC00ED97AA /* TGPhotoPaintController.h */, + D07BC8AE1F2A37EC00ED97AA /* TGPhotoPaintController.m */, + D07BC8AF1F2A37EC00ED97AA /* TGPhotoPaintEntityView.h */, + D07BC8B01F2A37EC00ED97AA /* TGPhotoPaintEntityView.m */, + D07BC8B11F2A37EC00ED97AA /* TGPhotoPaintFont.h */, + D07BC8B21F2A37EC00ED97AA /* TGPhotoPaintFont.m */, + D07BC8B31F2A37EC00ED97AA /* TGPhotoPaintScrollView.h */, + D07BC8B41F2A37EC00ED97AA /* TGPhotoPaintScrollView.m */, + D07BC8B51F2A37EC00ED97AA /* TGPhotoPaintSelectionContainerView.h */, + D07BC8B61F2A37EC00ED97AA /* TGPhotoPaintSelectionContainerView.m */, + D07BC8B71F2A37EC00ED97AA /* TGPhotoPaintSettingsView.h */, + D07BC8B81F2A37EC00ED97AA /* TGPhotoPaintSettingsView.m */, + D07BC8B91F2A37EC00ED97AA /* TGPhotoPaintSettingsWrapperView.h */, + D07BC8BA1F2A37EC00ED97AA /* TGPhotoPaintSettingsWrapperView.m */, + D07BC8BB1F2A37EC00ED97AA /* TGPhotoPaintSparseView.h */, + D07BC8BC1F2A37EC00ED97AA /* TGPhotoPaintSparseView.m */, + D07BC8BD1F2A37EC00ED97AA /* TGPhotoPaintTextEntity.h */, + D07BC8BE1F2A37EC00ED97AA /* TGPhotoPaintTextEntity.m */, + D07BC8D51F2A380D00ED97AA /* TGPaintBrush.h */, + D07BC8D61F2A380D00ED97AA /* TGPaintBrush.m */, + D07BC8D71F2A380D00ED97AA /* TGPaintBrushPreview.h */, + D07BC8D81F2A380D00ED97AA /* TGPaintBrushPreview.m */, + D07BC8D91F2A380D00ED97AA /* TGPaintBuffers.h */, + D07BC8DA1F2A380D00ED97AA /* TGPaintBuffers.m */, + D07BC8DB1F2A380D00ED97AA /* TGPaintCanvas.h */, + D07BC8DC1F2A380D00ED97AA /* TGPaintCanvas.m */, + D07BC8DD1F2A380D00ED97AA /* TGPaintEllipticalBrush.h */, + D07BC8DE1F2A380D00ED97AA /* TGPaintEllipticalBrush.m */, + D07BC8DF1F2A380D00ED97AA /* TGPaintFaceDebugView.h */, + D07BC8E01F2A380D00ED97AA /* TGPaintFaceDebugView.m */, + D07BC8E11F2A380D00ED97AA /* TGPaintFaceDetector.h */, + D07BC8E21F2A380D00ED97AA /* TGPaintFaceDetector.m */, + D07BC8E31F2A380D00ED97AA /* TGPainting.h */, + D07BC8E41F2A380D00ED97AA /* TGPainting.m */, + D07BC8E51F2A380D00ED97AA /* TGPaintingWrapperView.h */, + D07BC8E61F2A380D00ED97AA /* TGPaintingWrapperView.m */, + D07BC8E71F2A380D00ED97AA /* TGPaintInput.h */, + D07BC8E81F2A380D00ED97AA /* TGPaintInput.m */, + D07BC8E91F2A380D00ED97AA /* TGPaintNeonBrush.h */, + D07BC8EA1F2A380D00ED97AA /* TGPaintNeonBrush.m */, + D07BC8EB1F2A380D00ED97AA /* TGPaintPanGestureRecognizer.h */, + D07BC8EC1F2A380D00ED97AA /* TGPaintPanGestureRecognizer.m */, + D07BC8ED1F2A380D00ED97AA /* TGPaintPath.h */, + D07BC8EE1F2A380D00ED97AA /* TGPaintPath.m */, + D07BC8EF1F2A380D00ED97AA /* TGPaintRadialBrush.h */, + D07BC8F01F2A380D00ED97AA /* TGPaintRadialBrush.m */, + D07BC8F11F2A380D00ED97AA /* TGPaintRender.h */, + D07BC8F21F2A380D00ED97AA /* TGPaintRender.m */, + D07BC8F31F2A380D00ED97AA /* TGPaintShader.h */, + D07BC8F41F2A380D00ED97AA /* TGPaintShader.m */, + D07BC8F51F2A380D00ED97AA /* TGPaintShaderSet.h */, + D07BC8F61F2A380D00ED97AA /* TGPaintShaderSet.m */, + D07BC8F71F2A380D00ED97AA /* TGPaintSlice.h */, + D07BC8F81F2A380D00ED97AA /* TGPaintSlice.m */, + D07BC8F91F2A380D00ED97AA /* TGPaintState.h */, + D07BC8FA1F2A380D00ED97AA /* TGPaintState.m */, + D07BC8FB1F2A380D00ED97AA /* TGPaintSwatch.h */, + D07BC8FC1F2A380D00ED97AA /* TGPaintSwatch.m */, + D07BC8FD1F2A380D00ED97AA /* TGPaintTexture.h */, + D07BC8FE1F2A380D00ED97AA /* TGPaintTexture.m */, + D07BC92D1F2A3BA600ED97AA /* TGPhotoEntitiesContainerView.h */, + D07BC92E1F2A3BA600ED97AA /* TGPhotoEntitiesContainerView.m */, + D07BC9311F2A3BD100ED97AA /* TGPhotoTextEntityView.h */, + D07BC9321F2A3BD100ED97AA /* TGPhotoTextEntityView.m */, + D07BC9331F2A3BD100ED97AA /* TGPhotoStickerEntityView.h */, + D07BC9341F2A3BD100ED97AA /* TGPhotoStickerEntityView.m */, + D07BC96A1F2A43E300ED97AA /* TGPhotoStickersView.h */, + D07BC96B1F2A43E300ED97AA /* TGPhotoStickersView.m */, + D07BC96E1F2A467D00ED97AA /* TGPhotoTextSettingsView.h */, + D07BC96F1F2A467D00ED97AA /* TGPhotoTextSettingsView.m */, + D07BC9701F2A467D00ED97AA /* TGPhotoBrushSettingsView.h */, + D07BC9711F2A467D00ED97AA /* TGPhotoBrushSettingsView.m */, + D07BC97B1F2A472900ED97AA /* TGPhotoStickersCollectionLayout.h */, + D07BC97C1F2A472900ED97AA /* TGPhotoStickersCollectionLayout.m */, + D07BC97D1F2A472900ED97AA /* TGPhotoStickersCollectionView.h */, + D07BC97E1F2A472900ED97AA /* TGPhotoStickersCollectionView.m */, + D07BC97F1F2A472900ED97AA /* TGPhotoStickersSectionHeader.h */, + D07BC9801F2A472900ED97AA /* TGPhotoStickersSectionHeader.m */, + D07BC9811F2A472900ED97AA /* TGPhotoStickersSectionHeaderView.h */, + D07BC9821F2A472900ED97AA /* TGPhotoStickersSectionHeaderView.m */, ); name = "Photo Paint"; sourceTree = ""; @@ -1340,6 +2761,156 @@ children = ( D0177A261F2144700044446D /* TGPhotoEditorAnimation.h */, D0177A271F2144700044446D /* TGPhotoEditorAnimation.m */, + D07BC73F1F2A2AC500ED97AA /* TGPhotoEditorButton.h */, + D07BC7401F2A2AC500ED97AA /* TGPhotoEditorButton.m */, + D07BC7151F2A29B700ED97AA /* TGPhotoEditorController.h */, + D07BC7161F2A29B700ED97AA /* TGPhotoEditorController.m */, + D07BC7191F2A29E400ED97AA /* TGPhotoToolbarView.h */, + D07BC71A1F2A29E400ED97AA /* TGPhotoToolbarView.m */, + D07BC71B1F2A29E400ED97AA /* TGPhotoToolCell.h */, + D07BC71C1F2A29E400ED97AA /* TGPhotoToolCell.m */, + D07BC71D1F2A29E400ED97AA /* TGPhotoToolsController.h */, + D07BC71E1F2A29E400ED97AA /* TGPhotoToolsController.m */, + D07BC7291F2A2A7D00ED97AA /* PGPhotoEditor.h */, + D07BC72A1F2A2A7D00ED97AA /* PGPhotoEditor.m */, + D07BC72B1F2A2A7D00ED97AA /* PGPhotoEditorItem.h */, + D07BC72C1F2A2A7D00ED97AA /* PGPhotoEditorPicture.h */, + D07BC72D1F2A2A7D00ED97AA /* PGPhotoEditorPicture.m */, + D07BC72E1F2A2A7D00ED97AA /* PGPhotoEditorRawDataInput.h */, + D07BC72F1F2A2A7D00ED97AA /* PGPhotoEditorRawDataInput.m */, + D07BC7301F2A2A7D00ED97AA /* PGPhotoEditorRawDataOutput.h */, + D07BC7311F2A2A7D00ED97AA /* PGPhotoEditorRawDataOutput.m */, + D07BC7321F2A2A7D00ED97AA /* PGPhotoEditorView.h */, + D07BC7331F2A2A7D00ED97AA /* PGPhotoEditorView.m */, + D07BC78F1F2A2B5A00ED97AA /* TGPhotoEditorBlurAreaView.h */, + D07BC7431F2A2B3700ED97AA /* TGPhotoEditorBlurAreaView.m */, + D07BC7441F2A2B3700ED97AA /* TGPhotoEditorBlurToolView.h */, + D07BC7451F2A2B3700ED97AA /* TGPhotoEditorBlurToolView.m */, + D07BC7461F2A2B3700ED97AA /* TGPhotoEditorBlurTypeButton.h */, + D07BC7471F2A2B3700ED97AA /* TGPhotoEditorBlurTypeButton.m */, + D07BC7481F2A2B3700ED97AA /* TGPhotoEditorBlurView.h */, + D07BC7491F2A2B3700ED97AA /* TGPhotoEditorBlurView.m */, + D07BC74A1F2A2B3700ED97AA /* TGPhotoEditorCollectionView.h */, + D07BC74B1F2A2B3700ED97AA /* TGPhotoEditorCollectionView.m */, + D07BC74C1F2A2B3700ED97AA /* TGPhotoEditorCurvesHistogramView.h */, + D07BC74D1F2A2B3700ED97AA /* TGPhotoEditorCurvesHistogramView.m */, + D07BC74E1F2A2B3700ED97AA /* TGPhotoEditorCurvesToolView.h */, + D07BC74F1F2A2B3700ED97AA /* TGPhotoEditorCurvesToolView.m */, + D07BC7501F2A2B3700ED97AA /* TGPhotoEditorGenericToolView.h */, + D07BC7511F2A2B3700ED97AA /* TGPhotoEditorGenericToolView.m */, + D07BC7521F2A2B3700ED97AA /* TGPhotoEditorHUDView.h */, + D07BC7531F2A2B3700ED97AA /* TGPhotoEditorHUDView.m */, + D07BC7541F2A2B3700ED97AA /* TGPhotoEditorInterfaceAssets.h */, + D07BC7551F2A2B3700ED97AA /* TGPhotoEditorInterfaceAssets.m */, + D07BC7561F2A2B3700ED97AA /* TGPhotoEditorItemController.h */, + D07BC7571F2A2B3700ED97AA /* TGPhotoEditorItemController.m */, + D07BC7581F2A2B3700ED97AA /* TGPhotoEditorLinearBlurView.h */, + D07BC7591F2A2B3700ED97AA /* TGPhotoEditorLinearBlurView.m */, + D07BC75A1F2A2B3700ED97AA /* TGPhotoEditorPreviewView.h */, + D07BC75B1F2A2B3700ED97AA /* TGPhotoEditorPreviewView.m */, + D07BC75C1F2A2B3700ED97AA /* TGPhotoEditorRadialBlurView.h */, + D07BC75D1F2A2B3700ED97AA /* TGPhotoEditorRadialBlurView.m */, + D07BC75E1F2A2B3700ED97AA /* TGPhotoEditorSliderView.h */, + D07BC75F1F2A2B3700ED97AA /* TGPhotoEditorSliderView.m */, + D07BC7601F2A2B3700ED97AA /* TGPhotoEditorTabController.h */, + D07BC7611F2A2B3700ED97AA /* TGPhotoEditorTabController.m */, + D07BC7621F2A2B3700ED97AA /* TGPhotoEditorTintSwatchView.h */, + D07BC7631F2A2B3700ED97AA /* TGPhotoEditorTintSwatchView.m */, + D07BC7641F2A2B3700ED97AA /* TGPhotoEditorTintToolView.h */, + D07BC7651F2A2B3700ED97AA /* TGPhotoEditorTintToolView.m */, + D07BC7661F2A2B3700ED97AA /* TGPhotoEditorToolButtonsView.h */, + D07BC7671F2A2B3700ED97AA /* TGPhotoEditorToolButtonsView.m */, + D07BC7681F2A2B3700ED97AA /* TGPhotoEditorToolView.h */, + D07BC7B01F2A2BBE00ED97AA /* PGPhotoProcessPass.h */, + D07BC7B11F2A2BBE00ED97AA /* PGPhotoProcessPass.m */, + D07BC7B21F2A2BBE00ED97AA /* PGBlurTool.h */, + D07BC7B31F2A2BBE00ED97AA /* PGBlurTool.m */, + D07BC7B81F2A2BDD00ED97AA /* PGPhotoTool.h */, + D07BC7B91F2A2BDD00ED97AA /* PGPhotoTool.m */, + D07BC7BA1F2A2BDD00ED97AA /* PGPhotoToolComposer.h */, + D07BC7BB1F2A2BDD00ED97AA /* PGPhotoToolComposer.m */, + D07BC7C01F2A2C0A00ED97AA /* PGContrastTool.h */, + D07BC7C11F2A2C0A00ED97AA /* PGContrastTool.m */, + D07BC7C21F2A2C0A00ED97AA /* PGCurvesTool.h */, + D07BC7C31F2A2C0A00ED97AA /* PGCurvesTool.m */, + D07BC7C41F2A2C0A00ED97AA /* PGEnhanceTool.h */, + D07BC7C51F2A2C0A00ED97AA /* PGEnhanceTool.m */, + D07BC7C61F2A2C0A00ED97AA /* PGExposureTool.h */, + D07BC7C71F2A2C0A00ED97AA /* PGExposureTool.m */, + D07BC7C81F2A2C0A00ED97AA /* PGFadeTool.h */, + D07BC7C91F2A2C0A00ED97AA /* PGFadeTool.m */, + D07BC7CA1F2A2C0B00ED97AA /* PGGrainTool.h */, + D07BC7CB1F2A2C0B00ED97AA /* PGGrainTool.m */, + D07BC7CC1F2A2C0B00ED97AA /* PGHighlightsTool.h */, + D07BC7CD1F2A2C0B00ED97AA /* PGHighlightsTool.m */, + D07BC7CE1F2A2C0B00ED97AA /* PGPhotoBlurPass.h */, + D07BC7CF1F2A2C0B00ED97AA /* PGPhotoBlurPass.m */, + D07BC7D01F2A2C0B00ED97AA /* PGPhotoCustomFilterPass.h */, + D07BC7D11F2A2C0B00ED97AA /* PGPhotoCustomFilterPass.m */, + D07BC7D21F2A2C0B00ED97AA /* PGPhotoEnhanceColorConversionFilter.h */, + D07BC7D31F2A2C0B00ED97AA /* PGPhotoEnhanceColorConversionFilter.m */, + D07BC7D41F2A2C0B00ED97AA /* PGPhotoEnhanceInterpolationFilter.h */, + D07BC7D51F2A2C0B00ED97AA /* PGPhotoEnhanceInterpolationFilter.m */, + D07BC7D61F2A2C0B00ED97AA /* PGPhotoEnhanceLUTGenerator.h */, + D07BC7D71F2A2C0B00ED97AA /* PGPhotoEnhanceLUTGenerator.m */, + D07BC7D81F2A2C0B00ED97AA /* PGPhotoEnhancePass.h */, + D07BC7D91F2A2C0B00ED97AA /* PGPhotoEnhancePass.m */, + D07BC7DA1F2A2C0B00ED97AA /* PGPhotoFilter.h */, + D07BC7DB1F2A2C0B00ED97AA /* PGPhotoFilter.m */, + D07BC7DC1F2A2C0B00ED97AA /* PGPhotoFilterDefinition.h */, + D07BC7DD1F2A2C0B00ED97AA /* PGPhotoFilterDefinition.m */, + D07BC7DE1F2A2C0B00ED97AA /* PGPhotoFilterThumbnailManager.h */, + D07BC7DF1F2A2C0B00ED97AA /* PGPhotoFilterThumbnailManager.m */, + D07BC7E01F2A2C0B00ED97AA /* PGPhotoGaussianBlurFilter.h */, + D07BC7E11F2A2C0B00ED97AA /* PGPhotoGaussianBlurFilter.m */, + D07BC7E21F2A2C0B00ED97AA /* PGPhotoHistogram.h */, + D07BC7E31F2A2C0B00ED97AA /* PGPhotoHistogram.m */, + D07BC7E41F2A2C0B00ED97AA /* PGPhotoHistogramGenerator.h */, + D07BC7E51F2A2C0B00ED97AA /* PGPhotoHistogramGenerator.m */, + D07BC7E61F2A2C0B00ED97AA /* PGPhotoLookupFilterPass.h */, + D07BC7E71F2A2C0B00ED97AA /* PGPhotoLookupFilterPass.m */, + D07BC7E81F2A2C0B00ED97AA /* PGPhotoSharpenPass.h */, + D07BC7E91F2A2C0B00ED97AA /* PGPhotoSharpenPass.m */, + D07BC7EA1F2A2C0B00ED97AA /* PGSaturationTool.h */, + D07BC7EB1F2A2C0B00ED97AA /* PGSaturationTool.m */, + D07BC7EC1F2A2C0B00ED97AA /* PGShadowsTool.h */, + D07BC7ED1F2A2C0B00ED97AA /* PGShadowsTool.m */, + D07BC7EE1F2A2C0B00ED97AA /* PGSharpenTool.h */, + D07BC7EF1F2A2C0B00ED97AA /* PGSharpenTool.m */, + D07BC7F01F2A2C0B00ED97AA /* PGTintTool.h */, + D07BC7F11F2A2C0B00ED97AA /* PGTintTool.m */, + D07BC7F21F2A2C0B00ED97AA /* PGVignetteTool.h */, + D07BC7F31F2A2C0B00ED97AA /* PGVignetteTool.m */, + D07BC7F41F2A2C0B00ED97AA /* PGWarmthTool.h */, + D07BC7F51F2A2C0B00ED97AA /* PGWarmthTool.m */, + D07BC83D1F2A2D7900ED97AA /* TGPhotoCaptionController.h */, + D07BC83E1F2A2D7900ED97AA /* TGPhotoCaptionController.m */, + D07BC83F1F2A2D7900ED97AA /* TGPhotoCaptionInputMixin.h */, + D07BC8401F2A2D7900ED97AA /* TGPhotoCaptionInputMixin.m */, + D07BC8851F2A375800ED97AA /* TGPhotoCropAreaView.h */, + D07BC8861F2A375800ED97AA /* TGPhotoCropAreaView.m */, + D07BC8871F2A375800ED97AA /* TGPhotoCropControl.h */, + D07BC8881F2A375800ED97AA /* TGPhotoCropControl.m */, + D07BC8891F2A375800ED97AA /* TGPhotoCropController.h */, + D07BC88A1F2A375800ED97AA /* TGPhotoCropController.m */, + D07BC88B1F2A375800ED97AA /* TGPhotoCropGridView.h */, + D07BC88C1F2A375800ED97AA /* TGPhotoCropGridView.m */, + D07BC88D1F2A375800ED97AA /* TGPhotoCropRotationView.h */, + D07BC88E1F2A375800ED97AA /* TGPhotoCropRotationView.m */, + D07BC88F1F2A375800ED97AA /* TGPhotoCropScrollView.h */, + D07BC8901F2A375800ED97AA /* TGPhotoCropScrollView.m */, + D07BC8911F2A375800ED97AA /* TGPhotoCropView.h */, + D07BC8921F2A375800ED97AA /* TGPhotoCropView.m */, + D07BC8A11F2A37A500ED97AA /* TGPhotoAvatarCropController.h */, + D07BC8A21F2A37A500ED97AA /* TGPhotoAvatarCropController.m */, + D07BC8A31F2A37A500ED97AA /* TGPhotoAvatarCropView.h */, + D07BC8A41F2A37A500ED97AA /* TGPhotoAvatarCropView.m */, + D07BC9391F2A3C1F00ED97AA /* TGPhotoQualityController.h */, + D07BC93A1F2A3C1F00ED97AA /* TGPhotoQualityController.m */, + D07BC9B81F2A705D00ED97AA /* TGPhotoFilterCell.h */, + D07BC9B91F2A705D00ED97AA /* TGPhotoFilterCell.m */, + D07BC9BC1F2A722400ED97AA /* TGHistogramView.h */, + D07BC9BD1F2A722400ED97AA /* TGHistogramView.m */, ); name = "Photo Editor"; sourceTree = ""; @@ -1381,6 +2952,17 @@ D0177A9A1F22204A0044446D /* TGModernGalleryEmbeddedStickersHeaderView.m */, D0177AA51F22239A0044446D /* TGModernGalleryController.h */, D0177AA61F22239A0044446D /* TGModernGalleryController.m */, + D07BCA231F2A9A9600ED97AA /* TGModernGalleryImageItem.h */, + D07BCA241F2A9A9600ED97AA /* TGModernGalleryImageItem.m */, + D07BCA251F2A9A9600ED97AA /* TGModernGalleryImageItemImageView.h */, + D07BCA261F2A9A9600ED97AA /* TGModernGalleryImageItemImageView.m */, + D07BCA271F2A9A9600ED97AA /* TGModernGalleryImageItemView.h */, + D07BCA281F2A9A9600ED97AA /* TGModernGalleryImageItemView.m */, + D07BCA2F1F2A9AE700ED97AA /* TGModernGallerySelectableItem.h */, + D07BCA311F2A9B0400ED97AA /* TGModernGalleryEditableItem.h */, + D07BCA321F2A9B0400ED97AA /* TGModernGalleryEditableItemView.h */, + D07BCA391F2A9BE600ED97AA /* TGModernGalleryVideoContentView.h */, + D07BCA3A1F2A9BE600ED97AA /* TGModernGalleryVideoContentView.m */, ); name = Gallery; sourceTree = ""; @@ -1443,10 +3025,459 @@ D0177B131F2641B10044446D /* PGCameraVolumeButtonHandler.m */, D0177B2D1F26430D0044446D /* TGCameraPreviewView.h */, D0177B2E1F26430D0044446D /* TGCameraPreviewView.m */, + D07BC6C91F2A18B700ED97AA /* TGCameraMainPhoneView.h */, + D07BC6CA1F2A18B700ED97AA /* TGCameraMainPhoneView.m */, + D07BC6CB1F2A18B700ED97AA /* TGCameraMainTabletView.h */, + D07BC6CC1F2A18B700ED97AA /* TGCameraMainTabletView.m */, + D07BC6CD1F2A18B700ED97AA /* TGCameraMainView.h */, + D07BC6CE1F2A18B700ED97AA /* TGCameraMainView.m */, + D07BC6D91F2A19A700ED97AA /* TGCameraFlashActiveView.h */, + D07BC6DA1F2A19A700ED97AA /* TGCameraFlashActiveView.m */, + D07BC6DB1F2A19A700ED97AA /* TGCameraFlashControl.h */, + D07BC6DC1F2A19A700ED97AA /* TGCameraFlashControl.m */, + D07BC6DD1F2A19A700ED97AA /* TGCameraFlipButton.h */, + D07BC6DE1F2A19A700ED97AA /* TGCameraFlipButton.m */, + D07BC6DF1F2A19A700ED97AA /* TGCameraInterfaceAssets.h */, + D07BC6E01F2A19A700ED97AA /* TGCameraInterfaceAssets.m */, + D07BC6E11F2A19A700ED97AA /* TGCameraModeControl.h */, + D07BC6E21F2A19A700ED97AA /* TGCameraModeControl.m */, + D07BC6E31F2A19A700ED97AA /* TGCameraSegmentsView.h */, + D07BC6E41F2A19A700ED97AA /* TGCameraSegmentsView.m */, + D07BC6E51F2A19A700ED97AA /* TGCameraShutterButton.h */, + D07BC6E61F2A19A700ED97AA /* TGCameraShutterButton.m */, + D07BC6E71F2A19A700ED97AA /* TGCameraTimeCodeView.h */, + D07BC6E81F2A19A700ED97AA /* TGCameraTimeCodeView.m */, + D07BC6E91F2A19A700ED97AA /* TGCameraZoomView.h */, + D07BC6EA1F2A19A700ED97AA /* TGCameraZoomView.m */, + D07BC70D1F2A25AE00ED97AA /* TGCameraPhotoPreviewController.h */, + D07BC70E1F2A25AE00ED97AA /* TGCameraPhotoPreviewController.m */, + D07BCA671F2B3CE700ED97AA /* TGCameraController.h */, + D07BCA681F2B3CE700ED97AA /* TGCameraController.m */, + D07BCA691F2B3CE700ED97AA /* TGCameraFocusCrosshairsControl.h */, + D07BCA6A1F2B3CE700ED97AA /* TGCameraFocusCrosshairsControl.m */, ); name = Camera; sourceTree = ""; }; + D07BC7911F2A2B7000ED97AA /* GPUImage */ = { + isa = PBXGroup; + children = ( + D07BC7921F2A2B8900ED97AA /* GLProgram.h */, + D07BC7931F2A2B8900ED97AA /* GLProgram.m */, + D07BC7941F2A2B8900ED97AA /* GPUImage.h */, + D07BC7951F2A2B8900ED97AA /* GPUImageContext.h */, + D07BC7961F2A2B8900ED97AA /* GPUImageContext.m */, + D07BC7971F2A2B8900ED97AA /* GPUImageFilter.h */, + D07BC7981F2A2B8900ED97AA /* GPUImageFilter.m */, + D07BC7991F2A2B8900ED97AA /* GPUImageFramebuffer.h */, + D07BC79A1F2A2B8900ED97AA /* GPUImageFramebuffer.m */, + D07BC79B1F2A2B8900ED97AA /* GPUImageFramebufferCache.h */, + D07BC79C1F2A2B8900ED97AA /* GPUImageFramebufferCache.m */, + D07BC79D1F2A2B8900ED97AA /* GPUImageOutput.h */, + D07BC79E1F2A2B8900ED97AA /* GPUImageOutput.m */, + D07BC79F1F2A2B8900ED97AA /* GPUImageTwoInputFilter.h */, + D07BC7A01F2A2B8900ED97AA /* GPUImageTwoInputFilter.m */, + ); + name = GPUImage; + sourceTree = ""; + }; + D07BC82C1F2A2CFE00ED97AA /* Secret Timer */ = { + isa = PBXGroup; + children = ( + D07BC82D1F2A2D0C00ED97AA /* TGSecretTimerMenu.h */, + D07BC82E1F2A2D0C00ED97AA /* TGSecretTimerMenu.m */, + D07BC82F1F2A2D0C00ED97AA /* TGSecretTimerPickerItemView.h */, + D07BC8301F2A2D0C00ED97AA /* TGSecretTimerPickerItemView.m */, + D07BC8311F2A2D0C00ED97AA /* TGSecretTimerValueController.h */, + D07BC8321F2A2D0C00ED97AA /* TGSecretTimerValueController.m */, + D07BC8331F2A2D0C00ED97AA /* TGSecretTimerValueControllerItemView.h */, + D07BC8341F2A2D0C00ED97AA /* TGSecretTimerValueControllerItemView.m */, + ); + name = "Secret Timer"; + sourceTree = ""; + }; + D07BC8451F2A2D9700ED97AA /* Menu Sheet */ = { + isa = PBXGroup; + children = ( + D07BC8461F2A2DA200ED97AA /* TGMenuSheetController.h */, + D07BC8471F2A2DA200ED97AA /* TGMenuSheetController.m */, + D07BC84A1F2A2DBD00ED97AA /* TGMenuSheetButtonItemView.h */, + D07BC84B1F2A2DBD00ED97AA /* TGMenuSheetButtonItemView.m */, + D07BC84C1F2A2DBD00ED97AA /* TGMenuSheetCollectionView.h */, + D07BC84D1F2A2DBD00ED97AA /* TGMenuSheetCollectionView.m */, + D07BC84E1F2A2DBD00ED97AA /* TGMenuSheetDimView.h */, + D07BC84F1F2A2DBD00ED97AA /* TGMenuSheetDimView.m */, + D07BC8501F2A2DBD00ED97AA /* TGMenuSheetItemView.h */, + D07BC8511F2A2DBD00ED97AA /* TGMenuSheetItemView.m */, + D07BC8521F2A2DBD00ED97AA /* TGMenuSheetTitleItemView.h */, + D07BC8531F2A2DBD00ED97AA /* TGMenuSheetTitleItemView.m */, + D07BC8541F2A2DBD00ED97AA /* TGMenuSheetView.h */, + D07BC8551F2A2DBD00ED97AA /* TGMenuSheetView.m */, + ); + name = "Menu Sheet"; + sourceTree = ""; + }; + D07BC8621F2A2F0500ED97AA /* Media Picker */ = { + isa = PBXGroup; + children = ( + D07BC9C31F2A9A2B00ED97AA /* TGMediaPickerCell.h */, + D07BC9C41F2A9A2B00ED97AA /* TGMediaPickerCell.m */, + D07BC9C51F2A9A2B00ED97AA /* TGMediaPickerController.h */, + D07BC9C61F2A9A2B00ED97AA /* TGMediaPickerController.m */, + D07BC9C71F2A9A2B00ED97AA /* TGMediaPickerGalleryGifItem.h */, + D07BC9C81F2A9A2B00ED97AA /* TGMediaPickerGalleryGifItem.m */, + D07BC9C91F2A9A2B00ED97AA /* TGMediaPickerGalleryGifItemView.h */, + D07BC9CA1F2A9A2B00ED97AA /* TGMediaPickerGalleryGifItemView.m */, + D07BC9CB1F2A9A2B00ED97AA /* TGMediaPickerGalleryInterfaceView.h */, + D07BC9CC1F2A9A2B00ED97AA /* TGMediaPickerGalleryInterfaceView.m */, + D07BC9CD1F2A9A2B00ED97AA /* TGMediaPickerGalleryItem.h */, + D07BC9CE1F2A9A2B00ED97AA /* TGMediaPickerGalleryItem.m */, + D07BC9CF1F2A9A2B00ED97AA /* TGMediaPickerGalleryModel.h */, + D07BC9D01F2A9A2B00ED97AA /* TGMediaPickerGalleryModel.m */, + D07BC9D11F2A9A2B00ED97AA /* TGMediaPickerGalleryPhotoItem.h */, + D07BC9D21F2A9A2B00ED97AA /* TGMediaPickerGalleryPhotoItem.m */, + D07BC9D31F2A9A2B00ED97AA /* TGMediaPickerGalleryPhotoItemView.h */, + D07BC9D41F2A9A2B00ED97AA /* TGMediaPickerGalleryPhotoItemView.m */, + D07BC9D51F2A9A2B00ED97AA /* TGMediaPickerGallerySelectedItemsModel.h */, + D07BC9D61F2A9A2B00ED97AA /* TGMediaPickerGallerySelectedItemsModel.m */, + D07BC9D71F2A9A2B00ED97AA /* TGMediaPickerGalleryVideoItem.h */, + D07BC9D81F2A9A2B00ED97AA /* TGMediaPickerGalleryVideoItem.m */, + D07BC9D91F2A9A2B00ED97AA /* TGMediaPickerGalleryVideoItemView.h */, + D07BC9DA1F2A9A2B00ED97AA /* TGMediaPickerGalleryVideoItemView.m */, + D07BC9DB1F2A9A2B00ED97AA /* TGMediaPickerGalleryVideoScrubber.h */, + D07BC9DC1F2A9A2B00ED97AA /* TGMediaPickerGalleryVideoScrubber.m */, + D07BC9DD1F2A9A2B00ED97AA /* TGMediaPickerGalleryVideoScrubberThumbnailView.h */, + D07BC9DE1F2A9A2B00ED97AA /* TGMediaPickerGalleryVideoScrubberThumbnailView.m */, + D07BC9DF1F2A9A2B00ED97AA /* TGMediaPickerGalleryVideoTrimView.h */, + D07BC9E01F2A9A2B00ED97AA /* TGMediaPickerGalleryVideoTrimView.m */, + D07BC9E11F2A9A2B00ED97AA /* TGMediaPickerLayoutMetrics.h */, + D07BC9E21F2A9A2B00ED97AA /* TGMediaPickerLayoutMetrics.m */, + D07BC9E31F2A9A2B00ED97AA /* TGMediaPickerModernGalleryMixin.h */, + D07BC9E41F2A9A2B00ED97AA /* TGMediaPickerModernGalleryMixin.m */, + D07BC9E51F2A9A2B00ED97AA /* TGMediaPickerPhotoCounterButton.h */, + D07BC9E61F2A9A2B00ED97AA /* TGMediaPickerPhotoCounterButton.m */, + D07BC9E71F2A9A2B00ED97AA /* TGMediaPickerPhotoStripCell.h */, + D07BC9E81F2A9A2B00ED97AA /* TGMediaPickerPhotoStripCell.m */, + D07BC9E91F2A9A2B00ED97AA /* TGMediaPickerPhotoStripView.h */, + D07BC9EA1F2A9A2B00ED97AA /* TGMediaPickerPhotoStripView.m */, + D07BC9EB1F2A9A2B00ED97AA /* TGMediaPickerScrubberHeaderView.h */, + D07BC9EC1F2A9A2B00ED97AA /* TGMediaPickerScrubberHeaderView.m */, + D07BC9ED1F2A9A2B00ED97AA /* TGMediaPickerSelectionGestureRecognizer.h */, + D07BC9EE1F2A9A2B00ED97AA /* TGMediaPickerSelectionGestureRecognizer.m */, + D07BC9EF1F2A9A2B00ED97AA /* TGMediaPickerToolbarView.h */, + D07BC9F01F2A9A2B00ED97AA /* TGMediaPickerToolbarView.m */, + D07BC8631F2A2F1300ED97AA /* TGMediaPickerCaptionInputPanel.h */, + D07BC8641F2A2F1300ED97AA /* TGMediaPickerCaptionInputPanel.m */, + D07BCA3D1F2A9C6600ED97AA /* TGModernMediaListItem.h */, + D07BCA3E1F2A9C6600ED97AA /* TGModernMediaListItemContentView.h */, + D07BCA3F1F2A9C6600ED97AA /* TGModernMediaListItemContentView.m */, + D07BCA401F2A9C6600ED97AA /* TGModernMediaListItemView.h */, + D07BCA411F2A9C6600ED97AA /* TGModernMediaListItemView.m */, + D07BCA471F2A9CE300ED97AA /* TGModernMediaListSelectableItem.h */, + D07BCA491F2A9DAB00ED97AA /* TGModernAnimatedImagePlayer.h */, + D07BCA4A1F2A9DAB00ED97AA /* TGModernAnimatedImagePlayer.m */, + D07BCA4D1F2A9DDD00ED97AA /* FLAnimatedImage.h */, + D07BCA4E1F2A9DDD00ED97AA /* FLAnimatedImage.m */, + ); + name = "Media Picker"; + sourceTree = ""; + }; + D07BC8671F2A2F2B00ED97AA /* HPGrowingTextView */ = { + isa = PBXGroup; + children = ( + D07BC8681F2A2F3800ED97AA /* HPGrowingTextView.h */, + D07BC8691F2A2F3800ED97AA /* HPGrowingTextView.m */, + D07BC86E1F2A2F5300ED97AA /* HPTextViewInternal.h */, + D07BC86A1F2A2F3800ED97AA /* HPTextViewInternal.m */, + D07BC8701F2A2F6500ED97AA /* TGInputTextTag.h */, + D07BC8711F2A2F6500ED97AA /* TGInputTextTag.m */, + ); + name = HPGrowingTextView; + sourceTree = ""; + }; + D07BC8781F2A363800ED97AA /* Progress Window */ = { + isa = PBXGroup; + children = ( + D07BC8791F2A365000ED97AA /* TGProgressSpinnerView.h */, + D07BC87A1F2A365000ED97AA /* TGProgressSpinnerView.m */, + D07BC87B1F2A365000ED97AA /* TGProgressWindow.h */, + D07BC87C1F2A365000ED97AA /* TGProgressWindow.m */, + ); + name = "Progress Window"; + sourceTree = ""; + }; + D07BC9451F2A3E9900ED97AA /* Suggestions */ = { + isa = PBXGroup; + children = ( + D07BC9561F2A3EBF00ED97AA /* TGModernConversationAssociatedInputPanel.h */, + D07BC9571F2A3EBF00ED97AA /* TGModernConversationAssociatedInputPanel.m */, + D07BC9461F2A3EA900ED97AA /* TGHashtagPanelCell.h */, + D07BC9471F2A3EA900ED97AA /* TGHashtagPanelCell.m */, + D07BC9481F2A3EA900ED97AA /* TGMentionPanelCell.h */, + D07BC9491F2A3EA900ED97AA /* TGMentionPanelCell.m */, + D07BC94A1F2A3EA900ED97AA /* TGModernConversationHashtagsAssociatedPanel.h */, + D07BC94B1F2A3EA900ED97AA /* TGModernConversationHashtagsAssociatedPanel.m */, + D07BC94C1F2A3EA900ED97AA /* TGModernConversationMentionsAssociatedPanel.h */, + D07BC94D1F2A3EA900ED97AA /* TGModernConversationMentionsAssociatedPanel.m */, + D07BC9411F2A3E4400ED97AA /* TGSuggestionContext.h */, + D07BC9421F2A3E4400ED97AA /* TGSuggestionContext.m */, + ); + name = Suggestions; + sourceTree = ""; + }; + D07BC9761F2A470000ED97AA /* Sticker Keyboard */ = { + isa = PBXGroup; + children = ( + D07BC9771F2A471000ED97AA /* TGStickerKeyboardTabPanel.h */, + D07BC9781F2A471000ED97AA /* TGStickerKeyboardTabPanel.m */, + D07BC98F1F2A480800ED97AA /* TGStickerKeyboardTabCell.h */, + D07BC9901F2A480800ED97AA /* TGStickerKeyboardTabCell.m */, + D07BC9931F2A481C00ED97AA /* TGStickerKeyboardTabSettingsCell.h */, + D07BC9941F2A481C00ED97AA /* TGStickerKeyboardTabSettingsCell.m */, + D07BC99B1F2A494000ED97AA /* TGStickerCollectionViewCell.h */, + D07BC99C1F2A494000ED97AA /* TGStickerCollectionViewCell.m */, + D07BC9A81F2A4A0700ED97AA /* TGStickerItemPreviewView.h */, + D07BC9A91F2A4A0700ED97AA /* TGStickerItemPreviewView.m */, + ); + name = "Sticker Keyboard"; + sourceTree = ""; + }; + D07BC99F1F2A49AD00ED97AA /* Item Preview */ = { + isa = PBXGroup; + children = ( + D07BC9A01F2A49C200ED97AA /* TGItemPreviewController.h */, + D07BC9A11F2A49C200ED97AA /* TGItemPreviewController.m */, + D07BC9A41F2A49E300ED97AA /* TGItemPreviewView.h */, + D07BC9A51F2A49E300ED97AA /* TGItemPreviewView.m */, + D07BC9AC1F2A4A5100ED97AA /* TGItemMenuSheetPreviewView.h */, + D07BC9AD1F2A4A5100ED97AA /* TGItemMenuSheetPreviewView.m */, + ); + name = "Item Preview"; + sourceTree = ""; + }; + D07BCA6F1F2B441F00ED97AA /* Media Assets Picker */ = { + isa = PBXGroup; + children = ( + D07BCAA01F2B445E00ED97AA /* TGMediaGroupCell.h */, + D07BCAA11F2B445E00ED97AA /* TGMediaGroupCell.m */, + D07BCAA21F2B445E00ED97AA /* TGMediaGroupsController.h */, + D07BCAA31F2B445E00ED97AA /* TGMediaGroupsController.m */, + D07BCA701F2B443700ED97AA /* TGMediaAssetsController.h */, + D07BCA711F2B443700ED97AA /* TGMediaAssetsController.m */, + D07BCA721F2B443700ED97AA /* TGMediaAssetsGifCell.h */, + D07BCA731F2B443700ED97AA /* TGMediaAssetsGifCell.m */, + D07BCA741F2B443700ED97AA /* TGMediaAssetsMomentsCollectionLayout.h */, + D07BCA751F2B443700ED97AA /* TGMediaAssetsMomentsCollectionLayout.m */, + D07BCA761F2B443700ED97AA /* TGMediaAssetsMomentsCollectionView.h */, + D07BCA771F2B443700ED97AA /* TGMediaAssetsMomentsCollectionView.m */, + D07BCA781F2B443700ED97AA /* TGMediaAssetsMomentsController.h */, + D07BCA791F2B443700ED97AA /* TGMediaAssetsMomentsController.m */, + D07BCA7A1F2B443700ED97AA /* TGMediaAssetsMomentsSectionHeader.h */, + D07BCA7B1F2B443700ED97AA /* TGMediaAssetsMomentsSectionHeader.m */, + D07BCA7C1F2B443700ED97AA /* TGMediaAssetsMomentsSectionHeaderView.h */, + D07BCA7D1F2B443700ED97AA /* TGMediaAssetsMomentsSectionHeaderView.m */, + D07BCA7E1F2B443700ED97AA /* TGMediaAssetsPhotoCell.h */, + D07BCA7F1F2B443700ED97AA /* TGMediaAssetsPhotoCell.m */, + D07BCA801F2B443700ED97AA /* TGMediaAssetsPickerController.h */, + D07BCA811F2B443700ED97AA /* TGMediaAssetsPickerController.m */, + D07BCA821F2B443700ED97AA /* TGMediaAssetsTipView.h */, + D07BCA831F2B443700ED97AA /* TGMediaAssetsTipView.m */, + D07BCA841F2B443700ED97AA /* TGMediaAssetsUtils.h */, + D07BCA851F2B443700ED97AA /* TGMediaAssetsUtils.m */, + D07BCA861F2B443700ED97AA /* TGMediaAssetsVideoCell.h */, + D07BCA871F2B443700ED97AA /* TGMediaAssetsVideoCell.m */, + ); + name = "Media Assets Picker"; + sourceTree = ""; + }; + D07BCAB41F2B4DD600ED97AA /* Attachment Menu */ = { + isa = PBXGroup; + children = ( + D07BCAF91F2B517900ED97AA /* TGLegacyMediaPickerTipView.h */, + D07BCAFA1F2B517900ED97AA /* TGLegacyMediaPickerTipView.m */, + D07BCAB51F2B4DE200ED97AA /* TGAttachmentCarouselItemView.h */, + D07BCAB61F2B4DE200ED97AA /* TGAttachmentCarouselItemView.m */, + D07BCAB91F2B4E2600ED97AA /* TGTransitionLayout.h */, + D07BCABA1F2B4E2600ED97AA /* TGTransitionLayout.m */, + D07BCABD1F2B4E3900ED97AA /* UICollectionView+TGTransitioning.h */, + D07BCABE1F2B4E3900ED97AA /* UICollectionView+TGTransitioning.m */, + D07BCAC11F2B4E6200ED97AA /* TGAttachmentCameraCell.h */, + D07BCAC21F2B4E6200ED97AA /* TGAttachmentCameraCell.m */, + D07BCAC31F2B4E6200ED97AA /* TGAttachmentCameraView.h */, + D07BCAC41F2B4E6200ED97AA /* TGAttachmentCameraView.m */, + D07BCACB1F2B4E7300ED97AA /* TGMediaAvatarMenuMixin.h */, + D07BCAC51F2B4E6200ED97AA /* TGMediaAvatarMenuMixin.m */, + D07BCACD1F2B4E9000ED97AA /* TGAttachmentMenuCell.h */, + D07BCACE1F2B4E9000ED97AA /* TGAttachmentMenuCell.m */, + D07BCADD1F2B4F5E00ED97AA /* TGLegacyCameraController.h */, + D07BCADE1F2B4F5E00ED97AA /* TGLegacyCameraController.m */, + D07BCAE11F2B502F00ED97AA /* TGImagePickerController.h */, + D07BCAE21F2B502F00ED97AA /* TGImagePickerController.mm */, + D07BCAE51F2B507600ED97AA /* TGAttachmentGifCell.h */, + D07BCAE61F2B507600ED97AA /* TGAttachmentGifCell.m */, + D07BCAE71F2B507600ED97AA /* TGAttachmentVideoCell.h */, + D07BCAE81F2B507600ED97AA /* TGAttachmentVideoCell.m */, + D07BCAE91F2B507600ED97AA /* TGAttachmentPhotoCell.h */, + D07BCAEA1F2B507600ED97AA /* TGAttachmentPhotoCell.m */, + D07BCAF11F2B509200ED97AA /* TGAttachmentAssetCell.h */, + D07BCAF21F2B509200ED97AA /* TGAttachmentAssetCell.m */, + D07BCAF51F2B50AA00ED97AA /* TGMediaAvatarEditorTransition.h */, + D07BCAF61F2B50AA00ED97AA /* TGMediaAvatarEditorTransition.m */, + ); + name = "Attachment Menu"; + sourceTree = ""; + }; + D07BCB031F2B63D700ED97AA /* Passcode */ = { + isa = PBXGroup; + children = ( + D07BCB041F2B646A00ED97AA /* TGPasswordEntryView.h */, + D07BCB051F2B646A00ED97AA /* TGPasswordEntryView.m */, + D07BCB061F2B646A00ED97AA /* TGPasscodeEntryController.h */, + D07BCB071F2B646A00ED97AA /* TGPasscodeEntryController.m */, + D07BCB081F2B646A00ED97AA /* TGPasscodeEntryKeyboardView.h */, + D07BCB091F2B646A00ED97AA /* TGPasscodeEntryKeyboardView.m */, + D07BCB0A1F2B646A00ED97AA /* TGPasscodePinDotView.h */, + D07BCB0B1F2B646A00ED97AA /* TGPasscodePinDotView.m */, + D07BCB0C1F2B646A00ED97AA /* TGPasscodePinView.h */, + D07BCB0D1F2B646A00ED97AA /* TGPasscodePinView.m */, + D07BCB0E1F2B646A00ED97AA /* TGPasscodeButtonView.h */, + D07BCB0F1F2B646A00ED97AA /* TGPasscodeButtonView.m */, + D07BCB101F2B646A00ED97AA /* TGPasscodeBackground.h */, + D07BCB111F2B646A00ED97AA /* TGTextField.h */, + D07BCB121F2B646A00ED97AA /* TGTextField.m */, + D07BCB131F2B646A00ED97AA /* TGDefaultPasscodeBackground.h */, + D07BCB141F2B646A00ED97AA /* TGDefaultPasscodeBackground.m */, + D07BCB261F2B652C00ED97AA /* TGImageBasedPasscodeBackground.h */, + D07BCB271F2B652C00ED97AA /* TGImageBasedPasscodeBackground.m */, + ); + name = Passcode; + sourceTree = ""; + }; + D07BCB2A1F2B65C400ED97AA /* Wallpapers */ = { + isa = PBXGroup; + children = ( + D07BCB2B1F2B65F100ED97AA /* TGBuiltinWallpaperInfo.h */, + D07BCB2C1F2B65F100ED97AA /* TGBuiltinWallpaperInfo.m */, + D07BCB2D1F2B65F100ED97AA /* TGColorWallpaperInfo.h */, + D07BCB2E1F2B65F100ED97AA /* TGColorWallpaperInfo.m */, + D07BCB2F1F2B65F100ED97AA /* TGCustomImageWallpaperInfo.h */, + D07BCB301F2B65F100ED97AA /* TGCustomImageWallpaperInfo.m */, + D07BCB311F2B65F100ED97AA /* TGRemoteWallpaperInfo.h */, + D07BCB321F2B65F100ED97AA /* TGRemoteWallpaperInfo.m */, + D07BCB331F2B65F100ED97AA /* TGWallpaperInfo.h */, + D07BCB341F2B65F100ED97AA /* TGWallpaperInfo.m */, + ); + name = Wallpapers; + sourceTree = ""; + }; + D07BCB3F1F2B69D400ED97AA /* Embed Video */ = { + isa = PBXGroup; + children = ( + D07BCB7A1F2B6F5800ED97AA /* Coub */, + D07BCB401F2B6A5600ED97AA /* TGPIPAblePlayerView.h */, + D07BCB411F2B6A5600ED97AA /* TGEmbedInstagramPlayerView.h */, + D07BCB421F2B6A5600ED97AA /* TGEmbedInstagramPlayerView.m */, + D07BCB431F2B6A5600ED97AA /* TGEmbedPIPButton.h */, + D07BCB441F2B6A5600ED97AA /* TGEmbedPIPButton.m */, + D07BCB451F2B6A5600ED97AA /* TGEmbedPIPPullArrowView.h */, + D07BCB461F2B6A5600ED97AA /* TGEmbedPIPPullArrowView.m */, + D07BCB471F2B6A5600ED97AA /* TGEmbedPlayerControls.h */, + D07BCB481F2B6A5600ED97AA /* TGEmbedPlayerControls.m */, + D07BCB491F2B6A5600ED97AA /* TGEmbedPlayerScrubber.h */, + D07BCB4A1F2B6A5600ED97AA /* TGEmbedPlayerScrubber.m */, + D07BCB4B1F2B6A5600ED97AA /* TGEmbedPlayerState.h */, + D07BCB4C1F2B6A5600ED97AA /* TGEmbedPlayerState.m */, + D07BCB4D1F2B6A5600ED97AA /* TGEmbedPlayerView.h */, + D07BCB4E1F2B6A5600ED97AA /* TGEmbedPlayerView.m */, + D07BCB4F1F2B6A5600ED97AA /* TGEmbedSoundCloudPlayerView.h */, + D07BCB501F2B6A5600ED97AA /* TGEmbedSoundCloudPlayerView.m */, + D07BCB511F2B6A5600ED97AA /* TGEmbedVideoPlayerView.h */, + D07BCB521F2B6A5600ED97AA /* TGEmbedVideoPlayerView.m */, + D07BCB531F2B6A5600ED97AA /* TGEmbedVimeoPlayerView.h */, + D07BCB541F2B6A5600ED97AA /* TGEmbedVimeoPlayerView.m */, + D07BCB551F2B6A5600ED97AA /* TGEmbedVinePlayerView.h */, + D07BCB561F2B6A5600ED97AA /* TGEmbedVinePlayerView.m */, + D07BCB571F2B6A5600ED97AA /* TGEmbedVKPlayerView.h */, + D07BCB581F2B6A5600ED97AA /* TGEmbedVKPlayerView.m */, + D07BCB591F2B6A5600ED97AA /* TGEmbedYoutubePlayerView.h */, + D07BCB5A1F2B6A5600ED97AA /* TGEmbedYoutubePlayerView.m */, + D07BCB761F2B6DB900ED97AA /* TGEmbedCoubPlayerView.h */, + D07BCB771F2B6DB900ED97AA /* TGEmbedCoubPlayerView.m */, + D07BCBF91F2B757700ED97AA /* TGEmbedPIPScrubber.h */, + D07BCBFA1F2B757700ED97AA /* TGEmbedPIPScrubber.m */, + ); + name = "Embed Video"; + sourceTree = ""; + }; + D07BCB7A1F2B6F5800ED97AA /* Coub */ = { + isa = PBXGroup; + children = ( + D07BCB7B1F2B6F6300ED97AA /* AVAsset+CBExtension.h */, + D07BCB7C1F2B6F6300ED97AA /* AVAsset+CBExtension.m */, + D07BCB7D1F2B6F6300ED97AA /* CBAssetDownloadManager.h */, + D07BCB7E1F2B6F6300ED97AA /* CBAssetDownloadManager.m */, + D07BCB7F1F2B6F6300ED97AA /* CBChunkDownloadOperation.h */, + D07BCB801F2B6F6300ED97AA /* CBChunkDownloadOperation.m */, + D07BCB811F2B6F6300ED97AA /* CBConstance.h */, + D07BCB821F2B6F6300ED97AA /* CBConstance.m */, + D07BCB831F2B6F6300ED97AA /* CBCoubAsset.h */, + D07BCB841F2B6F6300ED97AA /* CBCoubAudioSource.h */, + D07BCB851F2B6F6300ED97AA /* CBCoubAudioSource.m */, + D07BCB861F2B6F6300ED97AA /* CBCoubAuthorVO.h */, + D07BCB871F2B6F6300ED97AA /* CBCoubAuthorVO.m */, + D07BCB881F2B6F6300ED97AA /* CBCoubDownloadOperation.h */, + D07BCB891F2B6F6300ED97AA /* CBCoubDownloadOperation.m */, + D07BCB8A1F2B6F6300ED97AA /* CBCoubLoopCompositionMaker.h */, + D07BCB8B1F2B6F6300ED97AA /* CBCoubLoopCompositionMaker.m */, + D07BCB8C1F2B6F6300ED97AA /* CBCoubNew.h */, + D07BCB8D1F2B6F6300ED97AA /* CBCoubNew.m */, + D07BCB8E1F2B6F6300ED97AA /* CBCoubPlayer.h */, + D07BCB8F1F2B6F6300ED97AA /* CBCoubPlayer.m */, + D07BCB901F2B6F6300ED97AA /* CBCoubPlayerContance.h */, + D07BCB911F2B6F6300ED97AA /* CBCoubPlayerContance.m */, + D07BCB921F2B6F6300ED97AA /* CBCoubVideoSource.h */, + D07BCB931F2B6F6300ED97AA /* CBCoubVideoSource.m */, + D07BCB941F2B6F6300ED97AA /* CBDownloadOperation.h */, + D07BCB951F2B6F6300ED97AA /* CBDownloadOperationDelegate.h */, + D07BCB961F2B6F6300ED97AA /* CBGenericDownloadOperation.h */, + D07BCB971F2B6F6300ED97AA /* CBGenericDownloadOperation.m */, + D07BCB981F2B6F6300ED97AA /* CBJSONCoubMapper.h */, + D07BCB991F2B6F6300ED97AA /* CBJSONCoubMapper.m */, + D07BCB9A1F2B6F6300ED97AA /* CBLibrary.h */, + D07BCB9B1F2B6F6300ED97AA /* CBLibrary.m */, + D07BCB9C1F2B6F6300ED97AA /* CBPlayerLayerView.h */, + D07BCB9D1F2B6F6300ED97AA /* CBPlayerLayerView.m */, + D07BCB9E1F2B6F6300ED97AA /* CBPlayerView.h */, + D07BCB9F1F2B6F6300ED97AA /* CBPlayerView.m */, + D07BCBA01F2B6F6300ED97AA /* CBTagNew.h */, + D07BCBA11F2B6F6300ED97AA /* CBTagNew.m */, + D07BCBA21F2B6F6300ED97AA /* CBVideoPlayer.h */, + D07BCBA31F2B6F6300ED97AA /* CBVideoPlayer.m */, + D07BCBA41F2B6F6300ED97AA /* NSDictionary+CBExtensions.h */, + D07BCBA51F2B6F6300ED97AA /* NSDictionary+CBExtensions.m */, + D07BCBD11F2B6FFE00ED97AA /* LegacyHTTPRequestOperation.h */, + D07BCBD51F2B72BD00ED97AA /* NSMutableArray+STKAudioPlayer.h */, + D07BCBD61F2B72BD00ED97AA /* NSMutableArray+STKAudioPlayer.m */, + D07BCBD71F2B72BD00ED97AA /* STKAudioPlayer.h */, + D07BCBD81F2B72BD00ED97AA /* STKAudioPlayer.m */, + D07BCBDD1F2B72DC00ED97AA /* STKAutoRecoveringHTTPDataSource.h */, + D07BCBDE1F2B72DC00ED97AA /* STKAutoRecoveringHTTPDataSource.m */, + D07BCBDF1F2B72DC00ED97AA /* STKCoreFoundationDataSource.h */, + D07BCBE01F2B72DC00ED97AA /* STKCoreFoundationDataSource.m */, + D07BCBE11F2B72DC00ED97AA /* STKDataSource.h */, + D07BCBE21F2B72DC00ED97AA /* STKDataSource.m */, + D07BCBE31F2B72DC00ED97AA /* STKDataSourceWrapper.h */, + D07BCBE41F2B72DC00ED97AA /* STKDataSourceWrapper.m */, + D07BCBE51F2B72DC00ED97AA /* STKHTTPDataSource.h */, + D07BCBE61F2B72DC00ED97AA /* STKHTTPDataSource.m */, + D07BCBE71F2B72DC00ED97AA /* STKLocalFileDataSource.h */, + D07BCBE81F2B72DC00ED97AA /* STKLocalFileDataSource.m */, + D07BCBE91F2B72DC00ED97AA /* STKQueueEntry.h */, + D07BCBEA1F2B72DC00ED97AA /* STKQueueEntry.m */, + ); + name = Coub; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -1454,22 +3485,37 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( + D07BC9A61F2A49E300ED97AA /* TGItemPreviewView.h in Headers */, + D07BC8A51F2A37A500ED97AA /* TGPhotoAvatarCropController.h in Headers */, D0177A001F2139980044446D /* POPLayerExtras.h in Headers */, D01778691F1F99180044446D /* NSInputStream+TL.h in Headers */, D0177ADD1F23D9B80044446D /* SGraphObjectNode.h in Headers */, D01777531F1F8FE60044446D /* PSCoding.h in Headers */, + D07BC6F91F2A19A700ED97AA /* TGCameraTimeCodeView.h in Headers */, D0177A131F213B440044446D /* NSValue+JNWAdditions.h in Headers */, D017781D1F1F961D0044446D /* TGMessageEntityMention.h in Headers */, + D07BC7FE1F2A2C0B00ED97AA /* PGFadeTool.h in Headers */, D01778ED1F20CAE60044446D /* TGOverlayController.h in Headers */, D017796B1F2103DB0044446D /* TGPhotoPaintEntity.h in Headers */, + D07BC7AE1F2A2B8900ED97AA /* GPUImageTwoInputFilter.h in Headers */, D0177A481F21F7010044446D /* TGModernGalleryVideoView.h in Headers */, D01778F71F20CDAC0044446D /* TGHacks.h in Headers */, D01779161F20F4500044446D /* TGStaticBackdropImageData.h in Headers */, D01779031F20D16B0044446D /* FreedomUIKit.h in Headers */, + D07BCB241F2B646A00ED97AA /* TGDefaultPasscodeBackground.h in Headers */, + D07BCA2D1F2A9A9600ED97AA /* TGModernGalleryImageItemView.h in Headers */, + D07BCBBD1F2B6F6300ED97AA /* CBCoubVideoSource.h in Headers */, + D07BC6D11F2A18B700ED97AA /* TGCameraMainTabletView.h in Headers */, + D07BCBDB1F2B72BD00ED97AA /* STKAudioPlayer.h in Headers */, D01779901F2108130044446D /* PSLMDBKeyValueReaderWriter.h in Headers */, D01779E01F2139980044446D /* POPAnimatableProperty.h in Headers */, D01778A91F1FD0900044446D /* TGImageUtils.h in Headers */, + D07BC8991F2A375800ED97AA /* TGPhotoCropGridView.h in Headers */, + D07BC7861F2A2B3700ED97AA /* TGPhotoEditorTabController.h in Headers */, + D07BCBC71F2B6F6300ED97AA /* CBPlayerLayerView.h in Headers */, D01778031F1F961D0044446D /* TGAuthorSignatureMediaAttachment.h in Headers */, + D07BC7781F2A2B3700ED97AA /* TGPhotoEditorHUDView.h in Headers */, + D07BCAB71F2B4DE200ED97AA /* TGAttachmentCarouselItemView.h in Headers */, D01779F81F2139980044446D /* POPCustomAnimation.h in Headers */, D0177A4F1F21F7C90044446D /* TGModernGalleryZoomableItemViewContent.h in Headers */, D01779E41F2139980044446D /* POPAnimationEvent.h in Headers */, @@ -1477,206 +3523,516 @@ D01778D71F2014E10044446D /* TGPopoverController.h in Headers */, D01779F01F2139980044446D /* POPAnimator.h in Headers */, D01778511F1F961D0044446D /* TGVideoInfo.h in Headers */, + D07BCBEF1F2B72DC00ED97AA /* STKDataSource.h in Headers */, + D07BCA0F1F2A9A2B00ED97AA /* TGMediaPickerLayoutMetrics.h in Headers */, + D07BC8391F2A2D0C00ED97AA /* TGSecretTimerValueController.h in Headers */, D017791A1F20F4A20044446D /* TGImageLuminanceMap.h in Headers */, D01777601F1F8FE60044446D /* TGBotInfo.h in Headers */, + D07BC9741F2A467D00ED97AA /* TGPhotoBrushSettingsView.h in Headers */, D0177A1A1F213B9E0044446D /* TransformationMatrix.h in Headers */, D0177A7D1F2204640044446D /* TGModernButton.h in Headers */, D01778191F1F961D0044446D /* TGMessageEntityHashtag.h in Headers */, + D07BCA341F2A9B0400ED97AA /* TGModernGalleryEditableItemView.h in Headers */, D0177A911F221BB10044446D /* TGModernGalleryTransitionView.h in Headers */, D01778E51F20CAE60044446D /* TGNavigationController.h in Headers */, D01777721F1F92420044446D /* TGPhoneUtils.h in Headers */, + D07BC7721F2A2B3700ED97AA /* TGPhotoEditorCurvesHistogramView.h in Headers */, D0177A501F21F7C90044446D /* TGModernGalleryZoomableScrollView.h in Headers */, + D07BC9541F2A3EA900ED97AA /* TGModernConversationMentionsAssociatedPanel.h in Headers */, D01778FB1F20CF6B0044446D /* TGBackdropView.h in Headers */, D01779F51F2139980044446D /* POPBasicAnimationInternal.h in Headers */, + D07BC8651F2A2F1300ED97AA /* TGMediaPickerCaptionInputPanel.h in Headers */, + D07BC9191F2A380D00ED97AA /* TGPaintRadialBrush.h in Headers */, + D07BC7131F2A269400ED97AA /* TGImageView.h in Headers */, D01779751F2104320044446D /* TGVideoEditAdjustments.h in Headers */, + D07BCA031F2A9A2B00ED97AA /* TGMediaPickerGallerySelectedItemsModel.h in Headers */, D01779EF1F2139980044446D /* POPAnimationTracerInternal.h in Headers */, D01777761F1F92570044446D /* RMPhoneFormat.h in Headers */, + D07BCA371F2A9B6A00ED97AA /* TGMediaAsset+TGMediaEditableItem.h in Headers */, + D07BC7361F2A2A7D00ED97AA /* PGPhotoEditorItem.h in Headers */, + D07BC77C1F2A2B3700ED97AA /* TGPhotoEditorItemController.h in Headers */, + D07BC6ED1F2A19A700ED97AA /* TGCameraFlashControl.h in Headers */, + D07BC7AC1F2A2B8900ED97AA /* GPUImageOutput.h in Headers */, + D07BCA111F2A9A2B00ED97AA /* TGMediaPickerModernGalleryMixin.h in Headers */, + D07BC9B61F2A700900ED97AA /* TGPhotoMaskPosition.h in Headers */, D01778BE1F200A9A0044446D /* UIScrollView+TGHacks.h in Headers */, + D07BCBD31F2B6FFE00ED97AA /* LegacyHTTPRequestOperation.h in Headers */, D0177A6F1F2201F00044446D /* TGModernGalleryDefaultFooterView.h in Headers */, + D07BCB371F2B65F100ED97AA /* TGColorWallpaperInfo.h in Headers */, D01777F61F1F961D0044446D /* TGPeerIdAdapter.h in Headers */, + D07BC82A1F2A2C0B00ED97AA /* PGWarmthTool.h in Headers */, + D07BC7FC1F2A2C0B00ED97AA /* PGExposureTool.h in Headers */, + D07BC8101F2A2C0B00ED97AA /* PGPhotoFilter.h in Headers */, D01779431F20FFF60044446D /* TGMediaAssetFetchResult.h in Headers */, D0177AD01F23D9810044446D /* ASQueue.h in Headers */, + D07BC8BF1F2A37EC00ED97AA /* TGPhotoPaintActionsView.h in Headers */, D0177A521F21F7C90044446D /* TGModernGalleryZoomableScrollViewSwipeGestureRecognizer.h in Headers */, + D07BCA8A1F2B443700ED97AA /* TGMediaAssetsGifCell.h in Headers */, D01779261F20FE480044446D /* TGObserverProxy.h in Headers */, + D07BCADF1F2B4F5E00ED97AA /* TGLegacyCameraController.h in Headers */, D017782F1F1F961D0044446D /* TGReplyMarkupAttachment.h in Headers */, + D07BCB211F2B646A00ED97AA /* TGPasscodeBackground.h in Headers */, D01778371F1F961D0044446D /* TGAudioMediaAttachment.h in Headers */, + D07BC6F51F2A19A700ED97AA /* TGCameraSegmentsView.h in Headers */, + D07BC87F1F2A365000ED97AA /* TGProgressWindow.h in Headers */, + D07BC8001F2A2C0B00ED97AA /* PGGrainTool.h in Headers */, D01779EA1F2139980044446D /* POPAnimationPrivate.h in Headers */, D017775C1F1F8FE60044446D /* PSKeyValueStore.h in Headers */, D0177A8F1F221BB10044446D /* TGModernGalleryModel.h in Headers */, D0177AF11F23DF6D0044446D /* TGImageManager.h in Headers */, D01777511F1F8FE60044446D /* LegacyComponentsGlobals.h in Headers */, + D07BC9BE1F2A722400ED97AA /* TGHistogramView.h in Headers */, D01778211F1F961D0044446D /* TGMessageEntityPre.h in Headers */, + D07BCA8C1F2B443700ED97AA /* TGMediaAssetsMomentsCollectionLayout.h in Headers */, + D07BC91B1F2A380D00ED97AA /* TGPaintRender.h in Headers */, + D07BCAA41F2B445E00ED97AA /* TGMediaGroupCell.h in Headers */, D0177A661F21FB9B0044446D /* TGModernGalleryItem.h in Headers */, D01779531F2100520044446D /* TGMediaEditingContext.h in Headers */, D017772C1F1F8F100044446D /* LegacyComponents.h in Headers */, + D07BC81A1F2A2C0B00ED97AA /* PGPhotoHistogramGenerator.h in Headers */, D0177B181F2641B10044446D /* PGCameraDeviceAngleSampler.h in Headers */, D0177A0A1F2139980044446D /* POPSpringSolver.h in Headers */, + D07BCB3D1F2B65F100ED97AA /* TGWallpaperInfo.h in Headers */, D017794F1F2100280044446D /* TGMediaSelectionContext.h in Headers */, D01778111F1F961D0044446D /* TGMessageEntityBold.h in Headers */, + D07BC6EB1F2A19A700ED97AA /* TGCameraFlashActiveView.h in Headers */, + D07BC6CF1F2A18B700ED97AA /* TGCameraMainPhoneView.h in Headers */, D0177A061F2139980044446D /* POPPropertyAnimationInternal.h in Headers */, D017789D1F1FC99A0044446D /* LegacyComponentsInternal.h in Headers */, D01779F21F2139980044446D /* POPAnimatorPrivate.h in Headers */, + D07BC71F1F2A29E400ED97AA /* TGPhotoToolbarView.h in Headers */, + D07BC7231F2A29E400ED97AA /* TGPhotoToolsController.h in Headers */, D01777621F1F8FE60044446D /* TGLocalization.h in Headers */, D01778E71F20CAE60044446D /* TGNavigationBar.h in Headers */, D0177A1F1F21403E0044446D /* LegacyComponentsContext.h in Headers */, + D07BCAEB1F2B507600ED97AA /* TGAttachmentGifCell.h in Headers */, D01778481F1F961D0044446D /* TGDocumentAttributeFilename.h in Headers */, D0177A751F2202260044446D /* TGModernBackToolbarButton.h in Headers */, + D07BCA0D1F2A9A2B00ED97AA /* TGMediaPickerGalleryVideoTrimView.h in Headers */, + D07BC9911F2A480800ED97AA /* TGStickerKeyboardTabCell.h in Headers */, + D07BC9F91F2A9A2B00ED97AA /* TGMediaPickerGalleryInterfaceView.h in Headers */, D01778BA1F2009880044446D /* Freedom.h in Headers */, + D07BCA8E1F2B443700ED97AA /* TGMediaAssetsMomentsCollectionView.h in Headers */, D0177A881F2219220044446D /* TGModernGalleryZoomableItemView.h in Headers */, D0177A321F21F1980044446D /* AVURLAsset+TGMediaItem.h in Headers */, + D07BC7171F2A29B700ED97AA /* TGPhotoEditorController.h in Headers */, + D07BCA9A1F2B443700ED97AA /* TGMediaAssetsTipView.h in Headers */, + D07BC9831F2A472900ED97AA /* TGPhotoStickersCollectionLayout.h in Headers */, D01779141F20F4500044446D /* TGStaticBackdropAreaData.h in Headers */, D01778171F1F961D0044446D /* TGMessageEntityEmail.h in Headers */, D01779E21F2139980044446D /* POPAnimation.h in Headers */, D01777F41F1F961D0044446D /* TGTextCheckingResult.h in Headers */, D01778231F1F961D0044446D /* TGMessageEntityTextUrl.h in Headers */, D0177A9B1F22204A0044446D /* TGModernGalleryEmbeddedStickersHeaderView.h in Headers */, + D07BCAAE1F2B45DA00ED97AA /* TGFileUtils.h in Headers */, D0177B1E1F2641B10044446D /* PGCameraMovieWriter.h in Headers */, D01779641F2103910044446D /* TGPaintUtils.h in Headers */, + D07BCB741F2B6A5600ED97AA /* TGEmbedYoutubePlayerView.h in Headers */, + D07BC8C51F2A37EC00ED97AA /* TGPhotoPaintEntityView.h in Headers */, D0177AA11F2222990044446D /* TGKeyCommand.h in Headers */, + D07BC8C91F2A37EC00ED97AA /* TGPhotoPaintScrollView.h in Headers */, + D07BCBB51F2B6F6300ED97AA /* CBCoubLoopCompositionMaker.h in Headers */, + D07BC9351F2A3BD100ED97AA /* TGPhotoTextEntityView.h in Headers */, + D07BCBAC1F2B6F6300ED97AA /* CBConstance.h in Headers */, D01778391F1F961D0044446D /* TGAudioWaveform.h in Headers */, + D07BC7821F2A2B3700ED97AA /* TGPhotoEditorRadialBlurView.h in Headers */, + D07BC7A31F2A2B8900ED97AA /* GPUImage.h in Headers */, + D07BCA0B1F2A9A2B00ED97AA /* TGMediaPickerGalleryVideoScrubberThumbnailView.h in Headers */, D0177A021F2139980044446D /* POPMath.h in Headers */, + D07BCABF1F2B4E3900ED97AA /* UICollectionView+TGTransitioning.h in Headers */, D01777FF1F1F961D0044446D /* TGMessageHole.h in Headers */, + D07BC78E1F2A2B3700ED97AA /* TGPhotoEditorToolView.h in Headers */, + D07BC7741F2A2B3700ED97AA /* TGPhotoEditorCurvesToolView.h in Headers */, D0177A711F2201F00044446D /* TGModernGalleryDefaultInterfaceView.h in Headers */, D01779491F20FFF60044446D /* TGMediaAssetMoment.h in Headers */, + D07BCAED1F2B507600ED97AA /* TGAttachmentVideoCell.h in Headers */, + D07BC9251F2A380D00ED97AA /* TGPaintSwatch.h in Headers */, + D07BC91D1F2A380D00ED97AA /* TGPaintShader.h in Headers */, + D07BC9641F2A3F4000ED97AA /* TGRemoteImageView.h in Headers */, + D07BCB661F2B6A5600ED97AA /* TGEmbedPlayerState.h in Headers */, + D07BCBC51F2B6F6300ED97AA /* CBLibrary.h in Headers */, D01779841F2107D70044446D /* lmdb.h in Headers */, D0177AF71F23DFC70044446D /* TGDataResource.h in Headers */, + D07BC6D31F2A18B700ED97AA /* TGCameraMainView.h in Headers */, D0177A071F2139980044446D /* POPSpringAnimation.h in Headers */, + D07BC9951F2A481C00ED97AA /* TGStickerKeyboardTabSettingsCell.h in Headers */, D0177A341F21F1980044446D /* UIImage+TGMediaEditableItem.h in Headers */, + D07BC94E1F2A3EA900ED97AA /* TGHashtagPanelCell.h in Headers */, + D07BCBAF1F2B6F6300ED97AA /* CBCoubAudioSource.h in Headers */, + D07BC7841F2A2B3700ED97AA /* TGPhotoEditorSliderView.h in Headers */, + D07BCBCF1F2B6F6300ED97AA /* NSDictionary+CBExtensions.h in Headers */, D017777E1F1F930B0044446D /* TGConversation.h in Headers */, + D07BC8281F2A2C0B00ED97AA /* PGVignetteTool.h in Headers */, + D07BCBCB1F2B6F6300ED97AA /* CBTagNew.h in Headers */, + D07BC89D1F2A375800ED97AA /* TGPhotoCropScrollView.h in Headers */, D017781F1F1F961D0044446D /* TGMessageEntityMentionName.h in Headers */, D0177AD91F23D9B80044446D /* SGraphListNode.h in Headers */, + D07BCA571F2A9E1600ED97AA /* TGDraggableCollectionViewFlowLayout.h in Headers */, + D07BCB151F2B646A00ED97AA /* TGPasswordEntryView.h in Headers */, D0177A041F2139980044446D /* POPPropertyAnimation.h in Headers */, + D07BCB281F2B652C00ED97AA /* TGImageBasedPasscodeBackground.h in Headers */, + D07BC8CB1F2A37EC00ED97AA /* TGPhotoPaintSelectionContainerView.h in Headers */, + D07BCB621F2B6A5600ED97AA /* TGEmbedPlayerControls.h in Headers */, + D07BC7371F2A2A7D00ED97AA /* PGPhotoEditorPicture.h in Headers */, + D07BC85A1F2A2DBD00ED97AA /* TGMenuSheetDimView.h in Headers */, + D07BC9AE1F2A4A5100ED97AA /* TGItemMenuSheetPreviewView.h in Headers */, + D07BC7F81F2A2C0B00ED97AA /* PGCurvesTool.h in Headers */, + D07BC83B1F2A2D0C00ED97AA /* TGSecretTimerValueControllerItemView.h in Headers */, D01777551F1F8FE60044446D /* PSKeyValueCoder.h in Headers */, + D07BCA211F2A9A5300ED97AA /* TGCheckButtonView.h in Headers */, D0177AC71F23D92C0044446D /* ASActor.h in Headers */, + D07BC9271F2A380D00ED97AA /* TGPaintTexture.h in Headers */, + D07BC9AA1F2A4A0700ED97AA /* TGStickerItemPreviewView.h in Headers */, D01778EB1F20CAE60044446D /* TGViewController+TGRecursiveEnumeration.h in Headers */, + D07BC6FB1F2A19A700ED97AA /* TGCameraZoomView.h in Headers */, + D07BC9721F2A467D00ED97AA /* TGPhotoTextSettingsView.h in Headers */, + D07BC8A71F2A37A500ED97AA /* TGPhotoAvatarCropView.h in Headers */, + D07BCBCD1F2B6F6300ED97AA /* CBVideoPlayer.h in Headers */, + D07BCBB91F2B6F6300ED97AA /* CBCoubPlayer.h in Headers */, + D07BCAB21F2B460B00ED97AA /* TGGifConverter.h in Headers */, D01778011F1F961D0044446D /* TGMessageViewCountContentProperty.h in Headers */, + D07BCA9C1F2B443700ED97AA /* TGMediaAssetsUtils.h in Headers */, D0177A6E1F2201F00044446D /* TGModernGalleryDefaultFooterAccessoryView.h in Headers */, D017777A1F1F927A0044446D /* NSObject+TGLock.h in Headers */, D01779451F20FFF60044446D /* TGMediaAssetFetchResultChange.h in Headers */, D01778A71F1FD0900044446D /* TGFont.h in Headers */, + D07BC6F11F2A19A700ED97AA /* TGCameraInterfaceAssets.h in Headers */, + D07BC9F71F2A9A2B00ED97AA /* TGMediaPickerGalleryGifItemView.h in Headers */, D0177A2C1F2145B80044446D /* LegacyComponentsAccessChecker.h in Headers */, + D07BC8C71F2A37EC00ED97AA /* TGPhotoPaintFont.h in Headers */, + D07BCBAE1F2B6F6300ED97AA /* CBCoubAsset.h in Headers */, D0177B221F2641B10044446D /* PGCameraVolumeButtonHandler.h in Headers */, + D07BCA6B1F2B3CE700ED97AA /* TGCameraController.h in Headers */, + D07BCA451F2A9C6600ED97AA /* TGModernMediaListItemView.h in Headers */, + D07BC85C1F2A2DBD00ED97AA /* TGMenuSheetItemView.h in Headers */, + D07BC8721F2A2F6500ED97AA /* TGInputTextTag.h in Headers */, + D07BCB721F2B6A5600ED97AA /* TGEmbedVKPlayerView.h in Headers */, + D07BC81C1F2A2C0B00ED97AA /* PGPhotoLookupFilterPass.h in Headers */, + D07BCACC1F2B4E7300ED97AA /* TGMediaAvatarMenuMixin.h in Headers */, D01778CB1F200BAC0044446D /* TGRTLScreenEdgePanGestureRecognizer.h in Headers */, + D07BC7B41F2A2BBE00ED97AA /* PGPhotoProcessPass.h in Headers */, + D07BC8121F2A2C0B00ED97AA /* PGPhotoFilterDefinition.h in Headers */, + D07BCADB1F2B4F2800ED97AA /* TGOverlayFormsheetWindow.h in Headers */, + D07BC7AA1F2A2B8900ED97AA /* GPUImageFramebufferCache.h in Headers */, D01779921F2108130044446D /* PSLMDBKeyValueCursor.h in Headers */, D01779F31F2139980044446D /* POPBasicAnimation.h in Headers */, D01779601F2103680044446D /* TGPaintingData.h in Headers */, D01779FE1F2139980044446D /* POPGeometry.h in Headers */, + D07BCA981F2B443700ED97AA /* TGMediaAssetsPickerController.h in Headers */, + D07BCBF51F2B72DC00ED97AA /* STKLocalFileDataSource.h in Headers */, + D07BC9F11F2A9A2B00ED97AA /* TGMediaPickerCell.h in Headers */, + D07BC8371F2A2D0C00ED97AA /* TGSecretTimerPickerItemView.h in Headers */, + D07BC8D31F2A37EC00ED97AA /* TGPhotoPaintTextEntity.h in Headers */, + D07BC86F1F2A2F5300ED97AA /* HPTextViewInternal.h in Headers */, D017794B1F20FFF60044446D /* TGMediaAssetMomentList.h in Headers */, + D07BCA4B1F2A9DAB00ED97AA /* TGModernAnimatedImagePlayer.h in Headers */, + D07BC9231F2A380D00ED97AA /* TGPaintState.h in Headers */, + D07BCBED1F2B72DC00ED97AA /* STKCoreFoundationDataSource.h in Headers */, + D07BC9A21F2A49C200ED97AA /* TGItemPreviewController.h in Headers */, + D07BCA091F2A9A2B00ED97AA /* TGMediaPickerGalleryVideoScrubber.h in Headers */, D0177A161F213B7D0044446D /* UnitBezier.h in Headers */, + D07BC8431F2A2D7900ED97AA /* TGPhotoCaptionInputMixin.h in Headers */, D01779311F20FFAC0044446D /* TGMediaAssetsLibrary.h in Headers */, + D07BCAF71F2B50AA00ED97AA /* TGMediaAvatarEditorTransition.h in Headers */, + D07BC7211F2A29E400ED97AA /* TGPhotoToolCell.h in Headers */, + D07BC9FF1F2A9A2B00ED97AA /* TGMediaPickerGalleryPhotoItem.h in Headers */, + D07BC77E1F2A2B3700ED97AA /* TGPhotoEditorLinearBlurView.h in Headers */, D01779711F2103FD0044446D /* TGPaintUndoManager.h in Headers */, + D07BCB1B1F2B646A00ED97AA /* TGPasscodePinDotView.h in Headers */, + D07BCAEF1F2B507600ED97AA /* TGAttachmentPhotoCell.h in Headers */, + D07BC7B61F2A2BBE00ED97AA /* PGBlurTool.h in Headers */, + D07BCA921F2B443700ED97AA /* TGMediaAssetsMomentsSectionHeader.h in Headers */, + D07BC7881F2A2B3700ED97AA /* TGPhotoEditorTintSwatchView.h in Headers */, + D07BC8221F2A2C0B00ED97AA /* PGShadowsTool.h in Headers */, D01778311F1F961D0044446D /* TGInstantPage.h in Headers */, + D07BC96C1F2A43E300ED97AA /* TGPhotoStickersView.h in Headers */, + D07BC7A41F2A2B8900ED97AA /* GPUImageContext.h in Headers */, + D07BCB171F2B646A00ED97AA /* TGPasscodeEntryController.h in Headers */, + D07BC9FD1F2A9A2B00ED97AA /* TGMediaPickerGalleryModel.h in Headers */, D017792F1F20FFAC0044446D /* TGMediaAssetsLegacyLibrary.h in Headers */, + D07BC87D1F2A365000ED97AA /* TGProgressSpinnerView.h in Headers */, D017790E1F20F4370044446D /* UIImage+TG.h in Headers */, + D07BC89F1F2A375800ED97AA /* TGPhotoCropView.h in Headers */, + D07BC7A11F2A2B8900ED97AA /* GLProgram.h in Headers */, D01778271F1F961D0044446D /* TGMessageEntitiesAttachment.h in Headers */, + D07BC9791F2A471000ED97AA /* TGStickerKeyboardTabPanel.h in Headers */, D017775B1F1F8FE60044446D /* PSKeyValueReader.h in Headers */, D0177B201F2641B10044446D /* PGCameraShotMetadata.h in Headers */, D0177A111F213B440044446D /* JNWSpringAnimation.h in Headers */, + D07BC8C31F2A37EC00ED97AA /* TGPhotoPaintController.h in Headers */, + D07BCAD91F2B4F2800ED97AA /* TGOverlayFormsheetController.h in Headers */, D01779871F2107D70044446D /* midl.h in Headers */, D01778131F1F961D0044446D /* TGMessageEntityBotCommand.h in Headers */, D0177A611F21FB250044446D /* TGModernGalleryScrollView.h in Headers */, D01779E61F2139980044446D /* POPAnimationEventInternal.h in Headers */, D017782B1F1F961D0044446D /* TGBotReplyMarkupButton.h in Headers */, + D07BC9891F2A472900ED97AA /* TGPhotoStickersSectionHeaderView.h in Headers */, + D07BCA331F2A9B0400ED97AA /* TGModernGalleryEditableItem.h in Headers */, D01779EB1F2139980044446D /* POPAnimationRuntime.h in Headers */, + D07BCA291F2A9A9600ED97AA /* TGModernGalleryImageItem.h in Headers */, D017784D1F1F961D0044446D /* TGForwardedMessageMediaAttachment.h in Headers */, D01777F71F1F961D0044446D /* TGChannelBannedRights.h in Headers */, + D07BCBB31F2B6F6300ED97AA /* CBCoubDownloadOperation.h in Headers */, + D07BCA1B1F2A9A2B00ED97AA /* TGMediaPickerSelectionGestureRecognizer.h in Headers */, + D07BCA171F2A9A2B00ED97AA /* TGMediaPickerPhotoStripView.h in Headers */, D0177B1C1F2641B10044446D /* PGCameraMomentSession.h in Headers */, D01778C21F200AF70044446D /* TGAnimationBlockDelegate.h in Headers */, + D07BCBC31F2B6F6300ED97AA /* CBJSONCoubMapper.h in Headers */, D01779F61F2139980044446D /* POPCGUtils.h in Headers */, + D07BC76A1F2A2B3700ED97AA /* TGPhotoEditorBlurToolView.h in Headers */, D01778401F1F961D0044446D /* TGDocumentAttributeSticker.h in Headers */, D0177A431F21F62A0044446D /* TGMediaVideoConverter.h in Headers */, D0177AA31F2222990044446D /* TGKeyCommandController.h in Headers */, + D07BCACF1F2B4E9000ED97AA /* TGAttachmentMenuCell.h in Headers */, D01779FA1F2139980044446D /* POPDecayAnimation.h in Headers */, + D07BCBB11F2B6F6300ED97AA /* CBCoubAuthorVO.h in Headers */, + D07BCBF31F2B72DC00ED97AA /* STKHTTPDataSource.h in Headers */, + D07BC78C1F2A2B3700ED97AA /* TGPhotoEditorToolButtonsView.h in Headers */, + D07BCBBB1F2B6F6300ED97AA /* CBCoubPlayerContance.h in Headers */, + D07BC81E1F2A2C0B00ED97AA /* PGPhotoSharpenPass.h in Headers */, + D07BCBF71F2B72DC00ED97AA /* STKQueueEntry.h in Headers */, D017783E1F1F961D0044446D /* TGDocumentAttributeImageSize.h in Headers */, + D07BC8181F2A2C0B00ED97AA /* PGPhotoHistogram.h in Headers */, + D07BC92F1F2A3BA600ED97AA /* TGPhotoEntitiesContainerView.h in Headers */, D017780F1F1F961D0044446D /* TGMessageEntity.h in Headers */, + D07BC9581F2A3EBF00ED97AA /* TGModernConversationAssociatedInputPanel.h in Headers */, D017795C1F2103440044446D /* PGPhotoEditorValues.h in Headers */, + D07BC89B1F2A375800ED97AA /* TGPhotoCropRotationView.h in Headers */, + D07BCA941F2B443700ED97AA /* TGMediaAssetsMomentsSectionHeaderView.h in Headers */, D0177AEF1F23DF6D0044446D /* TGImageDataSource.h in Headers */, D01777681F1F8FE60044446D /* TGUser.h in Headers */, + D07BCA551F2A9E1600ED97AA /* TGDraggableCollectionView.h in Headers */, + D07BC7901F2A2B5A00ED97AA /* TGPhotoEditorBlurAreaView.h in Headers */, + D07BCAC61F2B4E6200ED97AA /* TGAttachmentCameraCell.h in Headers */, + D07BC90F1F2A380D00ED97AA /* TGPaintingWrapperView.h in Headers */, + D07BC8561F2A2DBD00ED97AA /* TGMenuSheetButtonItemView.h in Headers */, D01779581F21031F0044446D /* TGPhotoEditorUtils.h in Headers */, D01778571F1F961D0044446D /* TGLocationMediaAttachment.h in Headers */, + D07BCAC81F2B4E6200ED97AA /* TGAttachmentCameraView.h in Headers */, + D07BC9521F2A3EA900ED97AA /* TGModernConversationHashtagsAssociatedPanel.h in Headers */, + D07BCA151F2A9A2B00ED97AA /* TGMediaPickerPhotoStripCell.h in Headers */, + D07BCB6E1F2B6A5600ED97AA /* TGEmbedVimeoPlayerView.h in Headers */, + D07BC73D1F2A2A7D00ED97AA /* PGPhotoEditorView.h in Headers */, + D07BCB6C1F2B6A5600ED97AA /* TGEmbedVideoPlayerView.h in Headers */, D0177ADB1F23D9B80044446D /* SGraphNode.h in Headers */, D01778531F1F961D0044446D /* TGVideoMediaAttachment.h in Headers */, + D07BC7761F2A2B3700ED97AA /* TGPhotoEditorGenericToolView.h in Headers */, + D07BC93F1F2A3DB900ED97AA /* TGMessageImageViewOverlayView.h in Headers */, D01778EF1F20CAE60044446D /* TGOverlayControllerWindow.h in Headers */, + D07BCBBF1F2B6F6300ED97AA /* CBDownloadOperation.h in Headers */, + D07BCA2B1F2A9A9600ED97AA /* TGModernGalleryImageItemImageView.h in Headers */, + D07BC9871F2A472900ED97AA /* TGPhotoStickersSectionHeader.h in Headers */, + D07BCA1D1F2A9A2B00ED97AA /* TGMediaPickerToolbarView.h in Headers */, + D07BC9171F2A380D00ED97AA /* TGPaintPath.h in Headers */, + D07BCA071F2A9A2B00ED97AA /* TGMediaPickerGalleryVideoItemView.h in Headers */, + D07BCBAA1F2B6F6300ED97AA /* CBChunkDownloadOperation.h in Headers */, + D07BC7801F2A2B3700ED97AA /* TGPhotoEditorPreviewView.h in Headers */, D017797B1F21075C0044446D /* TGModernCache.h in Headers */, D0177B1A1F2641B10044446D /* PGCameraMomentSegment.h in Headers */, + D07BC6F71F2A19A700ED97AA /* TGCameraShutterButton.h in Headers */, D01777641F1F8FE60044446D /* TGPluralization.h in Headers */, D01779471F20FFF60044446D /* TGMediaAssetGroup.h in Headers */, + D07BCB681F2B6A5600ED97AA /* TGEmbedPlayerView.h in Headers */, D01779A61F210A120044446D /* TGMediaAssetModernImageSignals.h in Headers */, + D07BCA131F2A9A2B00ED97AA /* TGMediaPickerPhotoCounterButton.h in Headers */, + D07BC90B1F2A380D00ED97AA /* TGPaintFaceDetector.h in Headers */, + D07BCA481F2A9CE300ED97AA /* TGModernMediaListSelectableItem.h in Headers */, + D07BC9F31F2A9A2B00ED97AA /* TGMediaPickerController.h in Headers */, D01778F31F20CC7A0044446D /* TGRootControllerProtocol.h in Headers */, + D07BC8161F2A2C0B00ED97AA /* PGPhotoGaussianBlurFilter.h in Headers */, D017783B1F1F961D0044446D /* TGStickerPackReference.h in Headers */, D01779DF1F2139980044446D /* POPAction.h in Headers */, D017791E1F20F7090044446D /* UIDevice+PlatformInfo.h in Headers */, + D07BC9991F2A489C00ED97AA /* TGStickerPack.h in Headers */, + D07BC7A61F2A2B8900ED97AA /* GPUImageFilter.h in Headers */, + D07BC9681F2A3F5C00ED97AA /* TGCache.h in Headers */, D0177AF31F23DF6D0044446D /* TGImageManagerTask.h in Headers */, D0177ACE1F23D9810044446D /* ASHandle.h in Headers */, D0177A671F21FB9B0044446D /* TGModernGalleryItemView.h in Headers */, + D07BC78A1F2A2B3700ED97AA /* TGPhotoEditorTintToolView.h in Headers */, + D07BC8261F2A2C0B00ED97AA /* PGTintTool.h in Headers */, D0177A561F21F7F40044446D /* TGDoubleTapGestureRecognizer.h in Headers */, + D07BC90D1F2A380D00ED97AA /* TGPainting.h in Headers */, D017796D1F2103DB0044446D /* TGPhotoPaintStickerEntity.h in Headers */, + D07BC8021F2A2C0B00ED97AA /* PGHighlightsTool.h in Headers */, + D07BC95C1F2A3EF000ED97AA /* TGLetteredAvatarView.h in Headers */, D01777571F1F8FE60044446D /* PSKeyValueDecoder.h in Headers */, + D07BCBA61F2B6F6300ED97AA /* AVAsset+CBExtension.h in Headers */, D017780D1F1F961D0044446D /* TGBotContextResultAttachment.h in Headers */, + D07BCA3B1F2A9BE600ED97AA /* TGModernGalleryVideoContentView.h in Headers */, + D07BC8C11F2A37EC00ED97AA /* TGPhotoPaintColorPicker.h in Headers */, D0177B2F1F26430D0044446D /* TGCameraPreviewView.h in Headers */, D01779E71F2139980044446D /* POPAnimationExtras.h in Headers */, D01778071F1F961D0044446D /* TGInvoiceMediaAttachment.h in Headers */, + D07BC8581F2A2DBD00ED97AA /* TGMenuSheetCollectionView.h in Headers */, + D07BCA301F2A9AE700ED97AA /* TGModernGallerySelectableItem.h in Headers */, + D07BC9211F2A380D00ED97AA /* TGPaintSlice.h in Headers */, + D07BC8971F2A375800ED97AA /* TGPhotoCropController.h in Headers */, D01778421F1F961D0044446D /* TGDocumentAttributeVideo.h in Headers */, + D07BC9501F2A3EA900ED97AA /* TGMentionPanelCell.h in Headers */, D01779AA1F210A2C0044446D /* TGMediaAssetImageSignals.h in Headers */, + D07BCBC11F2B6F6300ED97AA /* CBGenericDownloadOperation.h in Headers */, + D07BCA4F1F2A9DDD00ED97AA /* FLAnimatedImage.h in Headers */, D01778331F1F961D0044446D /* TGWebPageMediaAttachment.h in Headers */, + D07BC7A81F2A2B8900ED97AA /* GPUImageFramebuffer.h in Headers */, D01779331F20FFAC0044446D /* TGMediaAssetsModernLibrary.h in Headers */, + D07BC7BC1F2A2BDD00ED97AA /* PGPhotoTool.h in Headers */, D0177A971F221DB60044446D /* TGModernGalleryContainerView.h in Headers */, D017790A1F20F3F90044446D /* TGImageBlur.h in Headers */, + D07BC8931F2A375800ED97AA /* TGPhotoCropAreaView.h in Headers */, + D07BC85E1F2A2DBD00ED97AA /* TGMenuSheetTitleItemView.h in Headers */, D017799C1F2108670044446D /* TGMemoryImageCache.h in Headers */, + D07BC80C1F2A2C0B00ED97AA /* PGPhotoEnhanceLUTGenerator.h in Headers */, + D07BCAAA1F2B44C100ED97AA /* TGModernBarButton.h in Headers */, D01777821F1F93250044446D /* TGImageInfo.h in Headers */, D01779ED1F2139980044446D /* POPAnimationTracer.h in Headers */, D0177A931F221BB10044446D /* TGModernGalleryView.h in Headers */, + D07BC7F61F2A2C0B00ED97AA /* PGContrastTool.h in Headers */, D0177B141F2641B10044446D /* PGCamera.h in Headers */, + D07BCB5B1F2B6A5600ED97AA /* TGPIPAblePlayerView.h in Headers */, + D07BC9851F2A472900ED97AA /* TGPhotoStickersCollectionView.h in Headers */, D017782D1F1F961D0044446D /* TGBotReplyMarkupRow.h in Headers */, D017775E1F1F8FE60044446D /* TGBotComandInfo.h in Headers */, + D07BC9071F2A380D00ED97AA /* TGPaintEllipticalBrush.h in Headers */, + D07BCB191F2B646A00ED97AA /* TGPasscodeEntryKeyboardView.h in Headers */, + D07BC7701F2A2B3700ED97AA /* TGPhotoEditorCollectionView.h in Headers */, + D07BCA901F2B443700ED97AA /* TGMediaAssetsMomentsController.h in Headers */, + D07BC86B1F2A2F3800ED97AA /* HPGrowingTextView.h in Headers */, + D07BC8761F2A2F7B00ED97AA /* TGWeakDelegate.h in Headers */, + D07BC8FF1F2A380D00ED97AA /* TGPaintBrush.h in Headers */, D01777871F1F93550044446D /* TGMessage.h in Headers */, D01777591F1F8FE60044446D /* PSKeyValueEncoder.h in Headers */, D0177A3C1F21F2E50044446D /* TGTimerTarget.h in Headers */, D017798E1F2108130044446D /* PSLMDBKeyValueStore.h in Headers */, D017784B1F1F961D0044446D /* TGUnsupportedMediaAttachment.h in Headers */, + D07BCB391F2B65F100ED97AA /* TGCustomImageWallpaperInfo.h in Headers */, D01778461F1F961D0044446D /* TGDocumentAttributeAudio.h in Headers */, + D07BCBEB1F2B72DC00ED97AA /* STKAutoRecoveringHTTPDataSource.h in Headers */, D01778AD1F1FFDB80044446D /* TGDateUtils.h in Headers */, + D07BCB351F2B65F100ED97AA /* TGBuiltinWallpaperInfo.h in Headers */, + D07BC9601F2A3F0A00ED97AA /* TGGradientLabel.h in Headers */, + D07BC80A1F2A2C0B00ED97AA /* PGPhotoEnhanceInterpolationFilter.h in Headers */, + D07BC8241F2A2C0B00ED97AA /* PGSharpenTool.h in Headers */, D017785B1F1F961D0044446D /* TGMediaAttachment.h in Headers */, D01778351F1F961D0044446D /* TGReplyMessageMediaAttachment.h in Headers */, + D07BC9FB1F2A9A2B00ED97AA /* TGMediaPickerGalleryItem.h in Headers */, + D07BC6EF1F2A19A700ED97AA /* TGCameraFlipButton.h in Headers */, + D07BC7341F2A2A7D00ED97AA /* PGPhotoEditor.h in Headers */, D017780C1F1F961D0044446D /* TGViaUserAttachment.h in Headers */, + D07BCA051F2A9A2B00ED97AA /* TGMediaPickerGalleryVideoItem.h in Headers */, D017775D1F1F8FE60044446D /* PSKeyValueWriter.h in Headers */, + D07BC7FA1F2A2C0B00ED97AA /* PGEnhanceTool.h in Headers */, + D07BC92B1F2A3A3F00ED97AA /* matrix.h in Headers */, + D07BCAA61F2B445E00ED97AA /* TGMediaGroupsController.h in Headers */, + D07BC9111F2A380D00ED97AA /* TGPaintInput.h in Headers */, D01777FD1F1F961D0044446D /* TGMessageGroup.h in Headers */, D01777541F1F8FE60044446D /* PSData.h in Headers */, D01778091F1F961D0044446D /* TGGameMediaAttachment.h in Headers */, + D07BCA191F2A9A2B00ED97AA /* TGMediaPickerScrubberHeaderView.h in Headers */, D01778551F1F961D0044446D /* TGLocalMessageMetaMediaAttachment.h in Headers */, + D07BC7391F2A2A7D00ED97AA /* PGPhotoEditorRawDataInput.h in Headers */, + D07BCA881F2B443700ED97AA /* TGMediaAssetsController.h in Headers */, D0177A091F2139980044446D /* POPSpringAnimationInternal.h in Headers */, D01779FC1F2139980044446D /* POPDecayAnimationInternal.h in Headers */, + D07BCBD91F2B72BD00ED97AA /* NSMutableArray+STKAudioPlayer.h in Headers */, D01778151F1F961D0044446D /* TGMessageEntityCode.h in Headers */, D01778251F1F961D0044446D /* TGMessageEntityUrl.h in Headers */, + D07BC8601F2A2DBD00ED97AA /* TGMenuSheetView.h in Headers */, + D07BCB221F2B646A00ED97AA /* TGTextField.h in Headers */, + D07BCAE31F2B502F00ED97AA /* TGImagePickerController.h in Headers */, + D07BC9031F2A380D00ED97AA /* TGPaintBuffers.h in Headers */, D0177A801F2205190044446D /* TGModernGalleryInterfaceView.h in Headers */, D01777F91F1F961D0044446D /* TGChannelAdminRights.h in Headers */, D01779961F21082E0044446D /* PSLMDBTable.h in Headers */, D01778051F1F961D0044446D /* TGWebDocument.h in Headers */, + D07BC76C1F2A2B3700ED97AA /* TGPhotoEditorBlurTypeButton.h in Headers */, + D07BC9091F2A380D00ED97AA /* TGPaintFaceDebugView.h in Headers */, D0177A0B1F2139980044446D /* POPVector.h in Headers */, + D07BC9431F2A3E4400ED97AA /* TGSuggestionContext.h in Headers */, + D07BC9011F2A380D00ED97AA /* TGPaintBrushPreview.h in Headers */, D0177AA71F22239A0044446D /* TGModernGalleryController.h in Headers */, + D07BC99D1F2A494000ED97AA /* TGStickerCollectionViewCell.h in Headers */, + D07BC7BE1F2A2BDD00ED97AA /* PGPhotoToolComposer.h in Headers */, + D07BC8141F2A2C0B00ED97AA /* PGPhotoFilterThumbnailManager.h in Headers */, + D07BCB6A1F2B6A5600ED97AA /* TGEmbedSoundCloudPlayerView.h in Headers */, + D07BC6D71F2A18FE00ED97AA /* UIControl+HitTestEdgeInsets.h in Headers */, + D07BCAFB1F2B517900ED97AA /* TGLegacyMediaPickerTipView.h in Headers */, + D07BCB5C1F2B6A5600ED97AA /* TGEmbedInstagramPlayerView.h in Headers */, + D07BCA9E1F2B443700ED97AA /* TGMediaAssetsVideoCell.h in Headers */, + D07BC70F1F2A25AE00ED97AA /* TGCameraPhotoPreviewController.h in Headers */, + D07BCA421F2A9C6600ED97AA /* TGModernMediaListItem.h in Headers */, + D07BCB1D1F2B646A00ED97AA /* TGPasscodePinView.h in Headers */, D01778E91F20CAE60044446D /* TGViewController.h in Headers */, D017781B1F1F961D0044446D /* TGMessageEntityItalic.h in Headers */, + D07BCBF11F2B72DC00ED97AA /* STKDataSourceWrapper.h in Headers */, + D07BCB701F2B6A5600ED97AA /* TGEmbedVinePlayerView.h in Headers */, + D07BC9131F2A380D00ED97AA /* TGPaintNeonBrush.h in Headers */, D0177A701F2201F00044446D /* TGModernGalleryDefaultHeaderView.h in Headers */, + D07BCB781F2B6DB900ED97AA /* TGEmbedCoubPlayerView.h in Headers */, + D07BC7271F2A2A5300ED97AA /* UICollectionView+Utils.h in Headers */, D01777661F1F8FE60044446D /* TGStringUtils.h in Headers */, + D07BCB3B1F2B65F100ED97AA /* TGRemoteWallpaperInfo.h in Headers */, + D07BCBC01F2B6F6300ED97AA /* CBDownloadOperationDelegate.h in Headers */, + D07BC9B21F2A4B6600ED97AA /* TGStickerAssociation.h in Headers */, D01778B21F1FFF810044446D /* TGLabel.h in Headers */, + D07BC8041F2A2C0B00ED97AA /* PGPhotoBlurPass.h in Headers */, + D07BC8411F2A2D7900ED97AA /* TGPhotoCaptionController.h in Headers */, D017785D1F1F961D0044446D /* TGActionMediaAttachment.h in Headers */, + D07BCA011F2A9A2B00ED97AA /* TGMediaPickerGalleryPhotoItemView.h in Headers */, + D07BC8081F2A2C0B00ED97AA /* PGPhotoEnhanceColorConversionFilter.h in Headers */, D0177A841F2218AB0044446D /* TGModernGalleryImageItemContainerView.h in Headers */, + D07BC76E1F2A2B3700ED97AA /* TGPhotoEditorBlurView.h in Headers */, + D07BC8481F2A2DA200ED97AA /* TGMenuSheetController.h in Headers */, + D07BC8061F2A2C0B00ED97AA /* PGPhotoCustomFilterPass.h in Headers */, + D07BCB601F2B6A5600ED97AA /* TGEmbedPIPPullArrowView.h in Headers */, + D07BCA431F2A9C6600ED97AA /* TGModernMediaListItemContentView.h in Headers */, D017784F1F1F961D0044446D /* TGContactMediaAttachment.h in Headers */, D0177A381F21F2520044446D /* TGFullscreenContainerView.h in Headers */, + D07BC8CF1F2A37EC00ED97AA /* TGPhotoPaintSettingsWrapperView.h in Headers */, D01778491F1F961D0044446D /* TGDocumentMediaAttachment.h in Headers */, D0177AD21F23D9810044446D /* ASWatcher.h in Headers */, + D07BC8831F2A367500ED97AA /* TGActivityIndicatorView.h in Headers */, + D07BC80E1F2A2C0B00ED97AA /* PGPhotoEnhancePass.h in Headers */, + D07BC9051F2A380D00ED97AA /* TGPaintCanvas.h in Headers */, + D07BC7411F2A2AC500ED97AA /* TGPhotoEditorButton.h in Headers */, + D07BC93B1F2A3C1F00ED97AA /* TGPhotoQualityController.h in Headers */, D0177AC51F23D92C0044446D /* ActionStage.h in Headers */, D0177A1C1F213BBE0044446D /* FloatConversion.h in Headers */, + D07BCBA81F2B6F6300ED97AA /* CBAssetDownloadManager.h in Headers */, D01779FD1F2139980044446D /* POPDefines.h in Headers */, + D07BC9BA1F2A705D00ED97AA /* TGPhotoFilterCell.h in Headers */, D01779E91F2139980044446D /* POPAnimationInternal.h in Headers */, + D07BC9371F2A3BD100ED97AA /* TGPhotoStickerEntityView.h in Headers */, + D07BCBC91F2B6F6300ED97AA /* CBPlayerView.h in Headers */, + D07BCB641F2B6A5600ED97AA /* TGEmbedPlayerScrubber.h in Headers */, + D07BCAF31F2B509200ED97AA /* TGAttachmentAssetCell.h in Headers */, + D07BC8951F2A375800ED97AA /* TGPhotoCropControl.h in Headers */, + D07BC73B1F2A2A7D00ED97AA /* PGPhotoEditorRawDataOutput.h in Headers */, D01778B61F1FFFE30044446D /* TGToolbarButton.h in Headers */, + D07BCBB71F2B6F6300ED97AA /* CBCoubNew.h in Headers */, + D07BC8CD1F2A37EC00ED97AA /* TGPhotoPaintSettingsView.h in Headers */, + D07BC8351F2A2D0C00ED97AA /* TGSecretTimerMenu.h in Headers */, + D07BC77A1F2A2B3700ED97AA /* TGPhotoEditorInterfaceAssets.h in Headers */, + D07BC8201F2A2C0B00ED97AA /* PGSaturationTool.h in Headers */, + D07BCA6D1F2B3CE700ED97AA /* TGCameraFocusCrosshairsControl.h in Headers */, + D07BC91F1F2A380D00ED97AA /* TGPaintShaderSet.h in Headers */, + D07BC6FF1F2A1A7700ED97AA /* TGMenuView.h in Headers */, + D07BCB1F1F2B646A00ED97AA /* TGPasscodeButtonView.h in Headers */, + D07BCA961F2B443700ED97AA /* TGMediaAssetsPhotoCell.h in Headers */, D01778441F1F961D0044446D /* TGDocumentAttributeAnimated.h in Headers */, + D07BCABB1F2B4E2600ED97AA /* TGTransitionLayout.h in Headers */, D01778591F1F961D0044446D /* TGImageMediaAttachment.h in Headers */, + D07BC9151F2A380D00ED97AA /* TGPaintPanGestureRecognizer.h in Headers */, D0177A281F2144700044446D /* TGPhotoEditorAnimation.h in Headers */, + D07BC8D11F2A37EC00ED97AA /* TGPhotoPaintSparseView.h in Headers */, D01779A41F210A120044446D /* TGMediaAssetLegacyImageSignals.h in Headers */, + D07BCBFB1F2B757700ED97AA /* TGEmbedPIPScrubber.h in Headers */, D0177A791F2204360044446D /* TGModernToolbarButton.h in Headers */, + D07BC9F51F2A9A2B00ED97AA /* TGMediaPickerGalleryGifItem.h in Headers */, D01778FF1F20D0040044446D /* TGColor.h in Headers */, D01778291F1F961D0044446D /* TGBotReplyMarkup.h in Headers */, + D07BC6F31F2A19A700ED97AA /* TGCameraModeControl.h in Headers */, + D07BCB5E1F2B6A5600ED97AA /* TGEmbedPIPButton.h in Headers */, D0177B161F2641B10044446D /* PGCameraCaptureSession.h in Headers */, D01777FB1F1F961D0044446D /* TGDatabaseMessageDraft.h in Headers */, ); @@ -1751,199 +4107,511 @@ buildActionMask = 2147483647; files = ( D017798F1F2108130044446D /* PSLMDBKeyValueStore.m in Sources */, + D07BC8291F2A2C0B00ED97AA /* PGVignetteTool.m in Sources */, D0177A081F2139980044446D /* POPSpringAnimation.mm in Sources */, + D07BCBA91F2B6F6300ED97AA /* CBAssetDownloadManager.m in Sources */, + D07BCBBE1F2B6F6300ED97AA /* CBCoubVideoSource.m in Sources */, + D07BCBAB1F2B6F6300ED97AA /* CBChunkDownloadOperation.m in Sources */, + D07BCACA1F2B4E6200ED97AA /* TGMediaAvatarMenuMixin.m in Sources */, D01778D81F2014E10044446D /* TGPopoverController.m in Sources */, + D07BC7141F2A269400ED97AA /* TGImageView.m in Sources */, + D07BC8D41F2A37EC00ED97AA /* TGPhotoPaintTextEntity.m in Sources */, + D07BC8131F2A2C0B00ED97AA /* PGPhotoFilterDefinition.m in Sources */, + D07BC86D1F2A2F3800ED97AA /* HPTextViewInternal.m in Sources */, D01778241F1F961D0044446D /* TGMessageEntityTextUrl.m in Sources */, + D07BC7B71F2A2BBE00ED97AA /* PGBlurTool.m in Sources */, + D07BCA991F2B443700ED97AA /* TGMediaAssetsPickerController.m in Sources */, D01778471F1F961D0044446D /* TGDocumentAttributeAudio.m in Sources */, D01778181F1F961D0044446D /* TGMessageEntityEmail.m in Sources */, + D07BC90C1F2A380D00ED97AA /* TGPaintFaceDetector.m in Sources */, + D07BC9F21F2A9A2B00ED97AA /* TGMediaPickerCell.m in Sources */, + D07BC8051F2A2C0B00ED97AA /* PGPhotoBlurPass.m in Sources */, + D07BCB3C1F2B65F100ED97AA /* TGRemoteWallpaperInfo.m in Sources */, + D07BC7691F2A2B3700ED97AA /* TGPhotoEditorBlurAreaView.m in Sources */, + D07BC99E1F2A494000ED97AA /* TGStickerCollectionViewCell.m in Sources */, D01779851F2107D70044446D /* mdb.c in Sources */, D0177B171F2641B10044446D /* PGCameraCaptureSession.m in Sources */, + D07BCA8B1F2B443700ED97AA /* TGMediaAssetsGifCell.m in Sources */, + D07BCB1E1F2B646A00ED97AA /* TGPasscodePinView.m in Sources */, + D07BCB1A1F2B646A00ED97AA /* TGPasscodeEntryKeyboardView.m in Sources */, D017784A1F1F961D0044446D /* TGDocumentMediaAttachment.m in Sources */, D0177A0C1F2139980044446D /* POPVector.mm in Sources */, D0177ADA1F23D9B80044446D /* SGraphListNode.m in Sources */, D01778B31F1FFF810044446D /* TGLabel.m in Sources */, + D07BCA6C1F2B3CE700ED97AA /* TGCameraController.m in Sources */, + D07BC8841F2A367500ED97AA /* TGActivityIndicatorView.m in Sources */, + D07BC94F1F2A3EA900ED97AA /* TGHashtagPanelCell.m in Sources */, D017782E1F1F961D0044446D /* TGBotReplyMarkupRow.m in Sources */, D01779271F20FE480044446D /* TGObserverProxy.m in Sources */, + D07BC8961F2A375800ED97AA /* TGPhotoCropControl.m in Sources */, D017795D1F2103440044446D /* PGPhotoEditorValues.m in Sources */, + D07BC8091F2A2C0B00ED97AA /* PGPhotoEnhanceColorConversionFilter.m in Sources */, D0177ADC1F23D9B80044446D /* SGraphNode.m in Sources */, D0177B211F2641B10044446D /* PGCameraShotMetadata.m in Sources */, D017794A1F20FFF60044446D /* TGMediaAssetMoment.m in Sources */, + D07BC9691F2A3F5C00ED97AA /* TGCache.m in Sources */, + D07BCBF81F2B72DC00ED97AA /* STKQueueEntry.m in Sources */, + D07BC81F1F2A2C0B00ED97AA /* PGPhotoSharpenPass.m in Sources */, + D07BC9591F2A3EBF00ED97AA /* TGModernConversationAssociatedInputPanel.m in Sources */, D01778561F1F961D0044446D /* TGLocalMessageMetaMediaAttachment.m in Sources */, + D07BCBB41F2B6F6300ED97AA /* CBCoubDownloadOperation.m in Sources */, D01778FC1F20CF6B0044446D /* TGBackdropView.m in Sources */, + D07BC95D1F2A3EF000ED97AA /* TGLetteredAvatarView.m in Sources */, D01778081F1F961D0044446D /* TGInvoiceMediaAttachment.m in Sources */, + D07BC76D1F2A2B3700ED97AA /* TGPhotoEditorBlurTypeButton.m in Sources */, + D07BC9921F2A480800ED97AA /* TGStickerKeyboardTabCell.m in Sources */, D01779541F2100520044446D /* TGMediaEditingContext.m in Sources */, + D07BC6F41F2A19A700ED97AA /* TGCameraModeControl.m in Sources */, D017782C1F1F961D0044446D /* TGBotReplyMarkupButton.m in Sources */, + D07BCAB81F2B4DE200ED97AA /* TGAttachmentCarouselItemView.m in Sources */, D01777771F1F92570044446D /* RMPhoneFormat.m in Sources */, + D07BCA221F2A9A5300ED97AA /* TGCheckButtonView.m in Sources */, D017781E1F1F961D0044446D /* TGMessageEntityMention.m in Sources */, + D07BCA2C1F2A9A9600ED97AA /* TGModernGalleryImageItemImageView.m in Sources */, + D07BC7A71F2A2B8900ED97AA /* GPUImageFilter.m in Sources */, D01779041F20D16B0044446D /* FreedomUIKit.m in Sources */, + D07BCAD01F2B4E9000ED97AA /* TGAttachmentMenuCell.m in Sources */, D01777FC1F1F961D0044446D /* TGDatabaseMessageDraft.m in Sources */, D01779441F20FFF60044446D /* TGMediaAssetFetchResult.m in Sources */, + D07BCB751F2B6A5600ED97AA /* TGEmbedYoutubePlayerView.m in Sources */, + D07BC8191F2A2C0B00ED97AA /* PGPhotoHistogram.m in Sources */, D017777F1F1F930B0044446D /* TGConversation.m in Sources */, D01777F51F1F961D0044446D /* TGTextCheckingResult.m in Sources */, D01778581F1F961D0044446D /* TGLocationMediaAttachment.m in Sources */, D017785C1F1F961D0044446D /* TGMediaAttachment.m in Sources */, + D07BC81D1F2A2C0B00ED97AA /* PGPhotoLookupFilterPass.m in Sources */, D0177AA81F22239A0044446D /* TGModernGalleryController.m in Sources */, + D07BC7001F2A1A7700ED97AA /* TGMenuView.m in Sources */, D01779931F2108130044446D /* PSLMDBKeyValueCursor.m in Sources */, + D07BCAE41F2B502F00ED97AA /* TGImagePickerController.mm in Sources */, + D07BC7791F2A2B3700ED97AA /* TGPhotoEditorHUDView.m in Sources */, + D07BC8171F2A2C0B00ED97AA /* PGPhotoGaussianBlurFilter.m in Sources */, D01778BB1F2009880044446D /* Freedom.mm in Sources */, D01778061F1F961D0044446D /* TGWebDocument.m in Sources */, D0177A351F21F1980044446D /* UIImage+TGMediaEditableItem.m in Sources */, D01778001F1F961D0044446D /* TGMessageHole.m in Sources */, + D07BC73C1F2A2A7D00ED97AA /* PGPhotoEditorRawDataOutput.m in Sources */, + D07BCBC21F2B6F6300ED97AA /* CBGenericDownloadOperation.m in Sources */, + D07BC8CE1F2A37EC00ED97AA /* TGPhotoPaintSettingsView.m in Sources */, + D07BCAA51F2B445E00ED97AA /* TGMediaGroupCell.m in Sources */, + D07BCAEC1F2B507600ED97AA /* TGAttachmentGifCell.m in Sources */, + D07BC8151F2A2C0B00ED97AA /* PGPhotoFilterThumbnailManager.m in Sources */, D01779591F21031F0044446D /* TGPhotoEditorUtils.m in Sources */, + D07BC7751F2A2B3700ED97AA /* TGPhotoEditorCurvesToolView.m in Sources */, D01778B71F1FFFE30044446D /* TGToolbarButton.m in Sources */, + D07BCAF01F2B507600ED97AA /* TGAttachmentPhotoCell.m in Sources */, D01777FA1F1F961D0044446D /* TGChannelAdminRights.m in Sources */, + D07BC8981F2A375800ED97AA /* TGPhotoCropController.m in Sources */, D01778041F1F961D0044446D /* TGAuthorSignatureMediaAttachment.m in Sources */, D0177A621F21FB250044446D /* TGModernGalleryScrollView.m in Sources */, + D07BC9161F2A380D00ED97AA /* TGPaintPanGestureRecognizer.m in Sources */, D0177B1F1F2641B10044446D /* PGCameraMovieWriter.m in Sources */, + D07BC9531F2A3EA900ED97AA /* TGModernConversationHashtagsAssociatedPanel.m in Sources */, D017794C1F20FFF60044446D /* TGMediaAssetMomentList.m in Sources */, + D07BC6D41F2A18B700ED97AA /* TGCameraMainView.m in Sources */, + D07BC8071F2A2C0B00ED97AA /* PGPhotoCustomFilterPass.m in Sources */, D01779421F20FFF60044446D /* TGMediaAsset.m in Sources */, + D07BC76B1F2A2B3700ED97AA /* TGPhotoEditorBlurToolView.m in Sources */, D0177AA41F2222990044446D /* TGKeyCommandController.m in Sources */, + D07BCA061F2A9A2B00ED97AA /* TGMediaPickerGalleryVideoItem.m in Sources */, + D07BCB1C1F2B646A00ED97AA /* TGPasscodePinDotView.m in Sources */, D01778411F1F961D0044446D /* TGDocumentAttributeSticker.m in Sources */, + D07BC6F81F2A19A700ED97AA /* TGCameraShutterButton.m in Sources */, D01779481F20FFF60044446D /* TGMediaAssetGroup.m in Sources */, D01779171F20F4500044446D /* TGStaticBackdropImageData.m in Sources */, + D07BCBDC1F2B72BD00ED97AA /* STKAudioPlayer.m in Sources */, + D07BC9241F2A380D00ED97AA /* TGPaintState.m in Sources */, D01778221F1F961D0044446D /* TGMessageEntityPre.m in Sources */, + D07BC7421F2A2AC500ED97AA /* TGPhotoEditorButton.m in Sources */, + D07BC7BD1F2A2BDD00ED97AA /* PGPhotoTool.m in Sources */, + D07BC6D81F2A18FE00ED97AA /* UIControl+HitTestEdgeInsets.m in Sources */, + D07BC6FC1F2A19A700ED97AA /* TGCameraZoomView.m in Sources */, + D07BC91E1F2A380D00ED97AA /* TGPaintShader.m in Sources */, + D07BC7201F2A29E400ED97AA /* TGPhotoToolbarView.m in Sources */, D01779FF1F2139980044446D /* POPGeometry.mm in Sources */, + D07BC8CC1F2A37EC00ED97AA /* TGPhotoPaintSelectionContainerView.m in Sources */, + D07BC80F1F2A2C0B00ED97AA /* PGPhotoEnhancePass.m in Sources */, D0177A031F2139980044446D /* POPMath.mm in Sources */, D0177A681F21FB9B0044446D /* TGModernGalleryItemView.m in Sources */, + D07BC6F01F2A19A700ED97AA /* TGCameraFlipButton.m in Sources */, + D07BCB711F2B6A5600ED97AA /* TGEmbedVinePlayerView.m in Sources */, + D07BCA0A1F2A9A2B00ED97AA /* TGMediaPickerGalleryVideoScrubber.m in Sources */, + D07BC9861F2A472900ED97AA /* TGPhotoStickersCollectionView.m in Sources */, D01778EC1F20CAE60044446D /* TGViewController+TGRecursiveEnumeration.m in Sources */, D01777561F1F8FE60044446D /* PSKeyValueCoder.m in Sources */, + D07BC83C1F2A2D0C00ED97AA /* TGSecretTimerValueControllerItemView.m in Sources */, + D07BCA081F2A9A2B00ED97AA /* TGMediaPickerGalleryVideoItemView.m in Sources */, + D07BC9221F2A380D00ED97AA /* TGPaintSlice.m in Sources */, + D07BC9BB1F2A705D00ED97AA /* TGPhotoFilterCell.m in Sources */, D0177A571F21F7F40044446D /* TGDoubleTapGestureRecognizer.m in Sources */, + D07BC7FF1F2A2C0B00ED97AA /* PGFadeTool.m in Sources */, D01778C31F200AF70044446D /* TGAnimationBlockDelegate.m in Sources */, + D07BC85F1F2A2DBD00ED97AA /* TGMenuSheetTitleItemView.m in Sources */, + D07BC6EC1F2A19A700ED97AA /* TGCameraFlashActiveView.m in Sources */, + D07BC8CA1F2A37EC00ED97AA /* TGPhotoPaintScrollView.m in Sources */, + D07BC9301F2A3BA600ED97AA /* TGPhotoEntitiesContainerView.m in Sources */, + D07BCA2A1F2A9A9600ED97AA /* TGModernGalleryImageItem.m in Sources */, D01777831F1F93250044446D /* TGImageInfo.mm in Sources */, D0177A511F21F7C90044446D /* TGModernGalleryZoomableScrollView.m in Sources */, + D07BC7B51F2A2BBE00ED97AA /* PGPhotoProcessPass.m in Sources */, + D07BC98A1F2A472A00ED97AA /* TGPhotoStickersSectionHeaderView.m in Sources */, D01779E81F2139980044446D /* POPAnimationExtras.mm in Sources */, + D07BC8111F2A2C0B00ED97AA /* PGPhotoFilter.m in Sources */, D017780E1F1F961D0044446D /* TGBotContextResultAttachment.m in Sources */, D01779611F2103680044446D /* TGPaintingData.m in Sources */, D017781A1F1F961D0044446D /* TGMessageEntityHashtag.m in Sources */, + D07BCA4C1F2A9DAB00ED97AA /* TGModernAnimatedImagePlayer.m in Sources */, D01779861F2107D70044446D /* midl.c in Sources */, + D07BC7F71F2A2C0B00ED97AA /* PGContrastTool.m in Sources */, + D07BC9061F2A380D00ED97AA /* TGPaintCanvas.m in Sources */, + D07BC8A61F2A37A500ED97AA /* TGPhotoAvatarCropController.m in Sources */, + D07BC9441F2A3E4400ED97AA /* TGSuggestionContext.m in Sources */, + D07BCA1E1F2A9A2B00ED97AA /* TGMediaPickerToolbarView.m in Sources */, + D07BCB021F2B546400ED97AA /* LegacyComponentsContext.m in Sources */, + D07BC9AB1F2A4A0700ED97AA /* TGStickerItemPreviewView.m in Sources */, D0177B1B1F2641B10044446D /* PGCameraMomentSegment.m in Sources */, + D07BC8731F2A2F6500ED97AA /* TGInputTextTag.m in Sources */, + D07BCB691F2B6A5600ED97AA /* TGEmbedPlayerView.m in Sources */, + D07BCB6D1F2B6A5600ED97AA /* TGEmbedVideoPlayerView.m in Sources */, + D07BC7F91F2A2C0B00ED97AA /* PGCurvesTool.m in Sources */, + D07BCBCC1F2B6F6300ED97AA /* CBTagNew.m in Sources */, D01779A71F210A120044446D /* TGMediaAssetModernImageSignals.m in Sources */, + D07BC89A1F2A375800ED97AA /* TGPhotoCropGridView.m in Sources */, + D07BC6D21F2A18B700ED97AA /* TGCameraMainTabletView.m in Sources */, + D07BC7AB1F2A2B8900ED97AA /* GPUImageFramebufferCache.m in Sources */, D0177A531F21F7C90044446D /* TGModernGalleryZoomableScrollViewSwipeGestureRecognizer.m in Sources */, D0177A191F213B9E0044446D /* TransformationMatrix.cpp in Sources */, + D07BC8031F2A2C0B00ED97AA /* PGHighlightsTool.m in Sources */, D0177A981F221DB60044446D /* TGModernGalleryContainerView.m in Sources */, + D07BC8611F2A2DBD00ED97AA /* TGMenuSheetView.m in Sources */, D017796E1F2103DB0044446D /* TGPhotoPaintStickerEntity.m in Sources */, + D07BC9F61F2A9A2B00ED97AA /* TGMediaPickerGalleryGifItem.m in Sources */, + D07BC7101F2A25AE00ED97AA /* TGCameraPhotoPreviewController.m in Sources */, + D07BC89C1F2A375800ED97AA /* TGPhotoCropRotationView.m in Sources */, D01779F91F2139980044446D /* POPCustomAnimation.mm in Sources */, D01778EE1F20CAE60044446D /* TGOverlayController.m in Sources */, D01779EC1F2139980044446D /* POPAnimationRuntime.mm in Sources */, D01777881F1F93550044446D /* TGMessage.mm in Sources */, + D07BC9961F2A481C00ED97AA /* TGStickerKeyboardTabSettingsCell.m in Sources */, D0177A141F213B440044446D /* NSValue+JNWAdditions.m in Sources */, D017782A1F1F961D0044446D /* TGBotReplyMarkup.m in Sources */, + D07BCA021F2A9A2B00ED97AA /* TGMediaPickerGalleryPhotoItemView.m in Sources */, D01778CC1F200BAC0044446D /* TGRTLScreenEdgePanGestureRecognizer.m in Sources */, D01779EE1F2139980044446D /* POPAnimationTracer.mm in Sources */, + D07BC7851F2A2B3700ED97AA /* TGPhotoEditorSliderView.m in Sources */, D01778EA1F20CAE60044446D /* TGViewController.mm in Sources */, D0177ACF1F23D9810044446D /* ASHandle.m in Sources */, + D07BCAC91F2B4E6200ED97AA /* TGAttachmentCameraView.m in Sources */, D0177A7A1F2204360044446D /* TGModernToolbarButton.m in Sources */, + D07BCA441F2A9C6600ED97AA /* TGModernMediaListItemContentView.m in Sources */, D0177A921F221BB10044446D /* TGModernGalleryTransitionView.m in Sources */, + D07BCA041F2A9A2B00ED97AA /* TGMediaPickerGallerySelectedItemsModel.m in Sources */, D017790F1F20F4370044446D /* UIImage+TG.m in Sources */, + D07BC6F61F2A19A700ED97AA /* TGCameraSegmentsView.m in Sources */, D017791B1F20F4A20044446D /* TGImageLuminanceMap.m in Sources */, + D07BCB6B1F2B6A5600ED97AA /* TGEmbedSoundCloudPlayerView.m in Sources */, + D07BCB631F2B6A5600ED97AA /* TGEmbedPlayerControls.m in Sources */, D01778321F1F961D0044446D /* TGInstantPage.m in Sources */, D017776B1F1F909E0044446D /* LegacyComponentsGlobals.m in Sources */, + D07BCB651F2B6A5600ED97AA /* TGEmbedPlayerScrubber.m in Sources */, D01778301F1F961D0044446D /* TGReplyMarkupAttachment.m in Sources */, + D07BC97A1F2A471000ED97AA /* TGStickerKeyboardTabPanel.m in Sources */, + D07BC78D1F2A2B3700ED97AA /* TGPhotoEditorToolButtonsView.m in Sources */, D01779FB1F2139980044446D /* POPDecayAnimation.mm in Sources */, + D07BCAC01F2B4E3900ED97AA /* UICollectionView+TGTransitioning.m in Sources */, + D07BCB6F1F2B6A5600ED97AA /* TGEmbedVimeoPlayerView.m in Sources */, + D07BC9551F2A3EA900ED97AA /* TGModernConversationMentionsAssociatedPanel.m in Sources */, + D07BCAF41F2B509200ED97AA /* TGAttachmentAssetCell.m in Sources */, D0177A941F221BB10044446D /* TGModernGalleryView.m in Sources */, D01778501F1F961D0044446D /* TGContactMediaAttachment.m in Sources */, + D07BCB5F1F2B6A5600ED97AA /* TGEmbedPIPButton.m in Sources */, D0177AF81F23DFC70044446D /* TGDataResource.m in Sources */, D017786A1F1F99180044446D /* NSInputStream+TL.m in Sources */, + D07BC7351F2A2A7D00ED97AA /* PGPhotoEditor.m in Sources */, + D07BCB3A1F2B65F100ED97AA /* TGCustomImageWallpaperInfo.m in Sources */, + D07BC9731F2A467D00ED97AA /* TGPhotoTextSettingsView.m in Sources */, D0177B191F2641B10044446D /* PGCameraDeviceAngleSampler.m in Sources */, + D07BC6D01F2A18B700ED97AA /* TGCameraMainPhoneView.m in Sources */, + D07BC8361F2A2D0C00ED97AA /* TGSecretTimerMenu.m in Sources */, + D07BCBB81F2B6F6300ED97AA /* CBCoubNew.m in Sources */, D0177B1D1F2641B10044446D /* PGCameraMomentSession.m in Sources */, + D07BC9751F2A467D00ED97AA /* TGPhotoBrushSettingsView.m in Sources */, + D07BC8C21F2A37EC00ED97AA /* TGPhotoPaintColorPicker.m in Sources */, + D07BC87E1F2A365000ED97AA /* TGProgressSpinnerView.m in Sources */, + D07BCB5D1F2B6A5600ED97AA /* TGEmbedInstagramPlayerView.m in Sources */, D0177B301F26430D0044446D /* TGCameraPreviewView.m in Sources */, + D07BC7811F2A2B3700ED97AA /* TGPhotoEditorPreviewView.m in Sources */, + D07BCA381F2A9B6A00ED97AA /* TGMediaAsset+TGMediaEditableItem.m in Sources */, + D07BC9BF1F2A722400ED97AA /* TGHistogramView.m in Sources */, + D07BCBDA1F2B72BD00ED97AA /* NSMutableArray+STKAudioPlayer.m in Sources */, + D07BCBBC1F2B6F6300ED97AA /* CBCoubPlayerContance.m in Sources */, + D07BCB291F2B652C00ED97AA /* TGImageBasedPasscodeBackground.m in Sources */, + D07BC92C1F2A3A3F00ED97AA /* matrix.m in Sources */, + D07BCA6E1F2B3CE700ED97AA /* TGCameraFocusCrosshairsControl.m in Sources */, + D07BC7AD1F2A2B8900ED97AA /* GPUImageOutput.m in Sources */, D0177AF21F23DF6D0044446D /* TGImageManager.m in Sources */, + D07BC8211F2A2C0B00ED97AA /* PGSaturationTool.m in Sources */, + D07BC89E1F2A375800ED97AA /* TGPhotoCropScrollView.m in Sources */, D0177A331F21F1980044446D /* AVURLAsset+TGMediaItem.m in Sources */, + D07BC7221F2A29E400ED97AA /* TGPhotoToolCell.m in Sources */, + D07BCA891F2B443700ED97AA /* TGMediaAssetsController.m in Sources */, + D07BCB731F2B6A5600ED97AA /* TGEmbedVKPlayerView.m in Sources */, + D07BCA0E1F2A9A2B00ED97AA /* TGMediaPickerGalleryVideoTrimView.m in Sources */, + D07BCA501F2A9DDD00ED97AA /* FLAnimatedImage.m in Sources */, + D07BC9881F2A472900ED97AA /* TGPhotoStickersSectionHeader.m in Sources */, D0177ADE1F23D9B80044446D /* SGraphObjectNode.m in Sources */, D017790B1F20F3F90044446D /* TGImageBlur.m in Sources */, + D07BC85D1F2A2DBD00ED97AA /* TGMenuSheetItemView.m in Sources */, + D07BC90E1F2A380D00ED97AA /* TGPainting.m in Sources */, D0177A441F21F62A0044446D /* TGMediaVideoConverter.m in Sources */, D01779A51F210A120044446D /* TGMediaAssetLegacyImageSignals.m in Sources */, D017777B1F1F927A0044446D /* NSObject+TGLock.m in Sources */, D01778281F1F961D0044446D /* TGMessageEntitiesAttachment.m in Sources */, + D07BC8801F2A365000ED97AA /* TGProgressWindow.m in Sources */, D01777581F1F8FE60044446D /* PSKeyValueDecoder.m in Sources */, + D07BC8771F2A2F7B00ED97AA /* TGWeakDelegate.m in Sources */, D01779761F2104320044446D /* TGVideoEditAdjustments.m in Sources */, D0177A721F2201F00044446D /* TGModernGalleryDefaultInterfaceView.m in Sources */, + D07BCAFC1F2B517900ED97AA /* TGLegacyMediaPickerTipView.m in Sources */, + D07BC7381F2A2A7D00ED97AA /* PGPhotoEditorPicture.m in Sources */, + D07BCBB21F2B6F6300ED97AA /* CBCoubAuthorVO.m in Sources */, + D07BC7181F2A29B700ED97AA /* TGPhotoEditorController.m in Sources */, + D07BC8251F2A2C0B00ED97AA /* PGSharpenTool.m in Sources */, D0177AF41F23DF6D0044446D /* TGImageManagerTask.m in Sources */, D017783F1F1F961D0044446D /* TGDocumentAttributeImageSize.m in Sources */, D01778E61F20CAE60044446D /* TGNavigationController.m in Sources */, + D07BC8C61F2A37EC00ED97AA /* TGPhotoPaintEntityView.m in Sources */, D0177A901F221BB10044446D /* TGModernGalleryModel.m in Sources */, + D07BC81B1F2A2C0B00ED97AA /* PGPhotoHistogramGenerator.m in Sources */, + D07BC7A91F2A2B8900ED97AA /* GPUImageFramebuffer.m in Sources */, + D07BC6EE1F2A19A700ED97AA /* TGCameraFlashControl.m in Sources */, D017797C1F21075C0044446D /* TGModernCache.m in Sources */, + D07BCBF21F2B72DC00ED97AA /* STKDataSourceWrapper.m in Sources */, D017783D1F1F961D0044446D /* TGDocumentAttributeFilename.m in Sources */, D017796C1F2103DB0044446D /* TGPhotoPaintEntity.m in Sources */, + D07BCBF41F2B72DC00ED97AA /* STKHTTPDataSource.m in Sources */, + D07BCABC1F2B4E2600ED97AA /* TGTransitionLayout.m in Sources */, + D07BCB791F2B6DB900ED97AA /* TGEmbedCoubPlayerView.m in Sources */, + D07BCB231F2B646A00ED97AA /* TGTextField.m in Sources */, D017780B1F1F961D0044446D /* TGViaUserAttachment.m in Sources */, + D07BCA8F1F2B443700ED97AA /* TGMediaAssetsMomentsCollectionView.m in Sources */, + D07BC8A01F2A375800ED97AA /* TGPhotoCropView.m in Sources */, D0177AF01F23DF6D0044446D /* TGImageDataSource.m in Sources */, D01778BF1F200A9A0044446D /* UIScrollView+TGHacks.m in Sources */, + D07BC9361F2A3BD100ED97AA /* TGPhotoTextEntityView.m in Sources */, + D07BC9001F2A380D00ED97AA /* TGPaintBrush.m in Sources */, D0177A051F2139980044446D /* POPPropertyAnimation.mm in Sources */, + D07BCBCE1F2B6F6300ED97AA /* CBVideoPlayer.m in Sources */, + D07BCB381F2B65F100ED97AA /* TGColorWallpaperInfo.m in Sources */, D0177A011F2139980044446D /* POPLayerExtras.mm in Sources */, D01777671F1F8FE60044446D /* TGStringUtils.mm in Sources */, D0177B151F2641B10044446D /* PGCamera.m in Sources */, + D07BCBF61F2B72DC00ED97AA /* STKLocalFileDataSource.m in Sources */, + D07BC7FB1F2A2C0B00ED97AA /* PGEnhanceTool.m in Sources */, + D07BC9511F2A3EA900ED97AA /* TGMentionPanelCell.m in Sources */, D01779501F2100280044446D /* TGMediaSelectionContext.m in Sources */, + D07BC9651F2A3F4000ED97AA /* TGRemoteImageView.m in Sources */, + D07BC9121F2A380D00ED97AA /* TGPaintInput.m in Sources */, D0177A491F21F7010044446D /* TGModernGalleryVideoView.m in Sources */, + D07BCBA71F2B6F6300ED97AA /* AVAsset+CBExtension.m in Sources */, D017783C1F1F961D0044446D /* TGStickerPackReference.m in Sources */, + D07BCB201F2B646A00ED97AA /* TGPasscodeButtonView.m in Sources */, + D07BC9261F2A380D00ED97AA /* TGPaintSwatch.m in Sources */, D017799D1F2108670044446D /* TGMemoryImageCache.m in Sources */, + D07BCA461F2A9C6600ED97AA /* TGModernMediaListItemView.m in Sources */, + D07BC77D1F2A2B3700ED97AA /* TGPhotoEditorItemController.m in Sources */, + D07BC6F21F2A19A700ED97AA /* TGCameraInterfaceAssets.m in Sources */, + D07BC8441F2A2D7A00ED97AA /* TGPhotoCaptionInputMixin.m in Sources */, D01779911F2108130044446D /* PSLMDBKeyValueReaderWriter.m in Sources */, + D07BCB181F2B646A00ED97AA /* TGPasscodeEntryController.m in Sources */, D0177A851F2218AB0044446D /* TGModernGalleryImageItemContainerView.m in Sources */, D017775A1F1F8FE60044446D /* PSKeyValueEncoder.m in Sources */, + D07BC8661F2A2F1300ED97AA /* TGMediaPickerCaptionInputPanel.m in Sources */, D0177AD11F23D9810044446D /* ASQueue.m in Sources */, + D07BC8D01F2A37EC00ED97AA /* TGPhotoPaintSettingsWrapperView.m in Sources */, + D07BC83A1F2A2D0C00ED97AA /* TGSecretTimerValueController.m in Sources */, + D07BC9611F2A3F0A00ED97AA /* TGGradientLabel.m in Sources */, + D07BC9101F2A380D00ED97AA /* TGPaintingWrapperView.m in Sources */, + D07BC82B1F2A2C0B00ED97AA /* PGWarmthTool.m in Sources */, D01778F81F20CDAC0044446D /* TGHacks.m in Sources */, + D07BC86C1F2A2F3800ED97AA /* HPGrowingTextView.m in Sources */, + D07BC80D1F2A2C0B00ED97AA /* PGPhotoEnhanceLUTGenerator.m in Sources */, + D07BC9A31F2A49C200ED97AA /* TGItemPreviewController.m in Sources */, D01779301F20FFAC0044446D /* TGMediaAssetsLegacyLibrary.m in Sources */, D01778021F1F961D0044446D /* TGMessageViewCountContentProperty.m in Sources */, + D07BCA951F2B443700ED97AA /* TGMediaAssetsMomentsSectionHeaderView.m in Sources */, D01779F11F2139980044446D /* POPAnimator.mm in Sources */, D01779F71F2139980044446D /* POPCGUtils.mm in Sources */, + D07BCBB01F2B6F6300ED97AA /* CBCoubAudioSource.m in Sources */, + D07BCA931F2B443700ED97AA /* TGMediaAssetsMomentsSectionHeader.m in Sources */, + D07BC9201F2A380D00ED97AA /* TGPaintShaderSet.m in Sources */, D01778261F1F961D0044446D /* TGMessageEntityUrl.m in Sources */, + D07BCBEC1F2B72DC00ED97AA /* STKAutoRecoveringHTTPDataSource.m in Sources */, + D07BCBEE1F2B72DC00ED97AA /* STKCoreFoundationDataSource.m in Sources */, + D07BCBFC1F2B757700ED97AA /* TGEmbedPIPScrubber.m in Sources */, + D07BC8231F2A2C0B00ED97AA /* PGShadowsTool.m in Sources */, D01779971F21082E0044446D /* PSLMDBTable.m in Sources */, + D07BCB361F2B65F100ED97AA /* TGBuiltinWallpaperInfo.m in Sources */, + D07BC90A1F2A380D00ED97AA /* TGPaintFaceDebugView.m in Sources */, D017785A1F1F961D0044446D /* TGImageMediaAttachment.m in Sources */, D017784C1F1F961D0044446D /* TGUnsupportedMediaAttachment.m in Sources */, D01779151F20F4500044446D /* TGStaticBackdropAreaData.m in Sources */, + D07BC91C1F2A380D00ED97AA /* TGPaintRender.m in Sources */, + D07BCA101F2A9A2B00ED97AA /* TGMediaPickerLayoutMetrics.m in Sources */, + D07BCB251F2B646A00ED97AA /* TGDefaultPasscodeBackground.m in Sources */, D0177A761F2202260044446D /* TGModernBackToolbarButton.m in Sources */, D01777611F1F8FE60044446D /* TGBotInfo.m in Sources */, + D07BC8591F2A2DBD00ED97AA /* TGMenuSheetCollectionView.m in Sources */, D01778521F1F961D0044446D /* TGVideoInfo.mm in Sources */, + D07BCA971F2B443700ED97AA /* TGMediaAssetsPhotoCell.m in Sources */, + D07BCBBA1F2B6F6300ED97AA /* CBCoubPlayer.m in Sources */, + D07BC9FA1F2A9A2B00ED97AA /* TGMediaPickerGalleryInterfaceView.m in Sources */, D01777651F1F8FE60044446D /* TGPluralization.m in Sources */, + D07BCAF81F2B50AA00ED97AA /* TGMediaAvatarEditorTransition.m in Sources */, D01779F41F2139980044446D /* POPBasicAnimation.mm in Sources */, + D07BC7831F2A2B3700ED97AA /* TGPhotoEditorRadialBlurView.m in Sources */, D01778AE1F1FFDB80044446D /* TGDateUtils.mm in Sources */, + D07BCA9B1F2B443700ED97AA /* TGMediaAssetsTipView.m in Sources */, + D07BC8011F2A2C0B00ED97AA /* PGGrainTool.m in Sources */, D01779E11F2139980044446D /* POPAnimatableProperty.mm in Sources */, + D07BC8381F2A2D0C00ED97AA /* TGSecretTimerPickerItemView.m in Sources */, + D07BC7771F2A2B3700ED97AA /* TGPhotoEditorGenericToolView.m in Sources */, D01777F81F1F961D0044446D /* TGChannelBannedRights.m in Sources */, D01778451F1F961D0044446D /* TGDocumentAttributeAnimated.m in Sources */, D01778141F1F961D0044446D /* TGMessageEntityBotCommand.m in Sources */, D01778541F1F961D0044446D /* TGVideoMediaAttachment.m in Sources */, + D07BC7A21F2A2B8900ED97AA /* GLProgram.m in Sources */, + D07BC9A71F2A49E300ED97AA /* TGItemPreviewView.m in Sources */, + D07BCB611F2B6A5600ED97AA /* TGEmbedPIPPullArrowView.m in Sources */, D01779341F20FFAC0044446D /* TGMediaAssetsModernLibrary.m in Sources */, D01778361F1F961D0044446D /* TGReplyMessageMediaAttachment.m in Sources */, + D07BC77F1F2A2B3700ED97AA /* TGPhotoEditorLinearBlurView.m in Sources */, + D07BC7FD1F2A2C0B00ED97AA /* PGExposureTool.m in Sources */, + D07BC77B1F2A2B3700ED97AA /* TGPhotoEditorInterfaceAssets.m in Sources */, + D07BC9B71F2A700900ED97AA /* TGPhotoMaskPosition.m in Sources */, + D07BC91A1F2A380D00ED97AA /* TGPaintRadialBrush.m in Sources */, + D07BC7891F2A2B3700ED97AA /* TGPhotoEditorTintSwatchView.m in Sources */, + D07BC8C01F2A37EC00ED97AA /* TGPhotoPaintActionsView.m in Sources */, + D07BCB161F2B646A00ED97AA /* TGPasswordEntryView.m in Sources */, + D07BC9FC1F2A9A2B00ED97AA /* TGMediaPickerGalleryItem.m in Sources */, + D07BC8C81F2A37EC00ED97AA /* TGPhotoPaintFont.m in Sources */, + D07BC9FE1F2A9A2B00ED97AA /* TGMediaPickerGalleryModel.m in Sources */, + D07BC96D1F2A43E300ED97AA /* TGPhotoStickersView.m in Sources */, + D07BCA0C1F2A9A2B00ED97AA /* TGMediaPickerGalleryVideoScrubberThumbnailView.m in Sources */, D0177AC61F23D92C0044446D /* ActionStage.mm in Sources */, + D07BC78B1F2A2B3700ED97AA /* TGPhotoEditorTintToolView.m in Sources */, + D07BC6FA1F2A19A700ED97AA /* TGCameraTimeCodeView.m in Sources */, + D07BC8C41F2A37EC00ED97AA /* TGPhotoPaintController.m in Sources */, + D07BC7A51F2A2B8900ED97AA /* GPUImageContext.m in Sources */, + D07BC9081F2A380D00ED97AA /* TGPaintEllipticalBrush.m in Sources */, D0177A291F2144700044446D /* TGPhotoEditorAnimation.m in Sources */, + D07BCBB61F2B6F6300ED97AA /* CBCoubLoopCompositionMaker.m in Sources */, + D07BC9401F2A3DB900ED97AA /* TGMessageImageViewOverlayView.m in Sources */, + D07BC80B1F2A2C0B00ED97AA /* PGPhotoEnhanceInterpolationFilter.m in Sources */, D01779651F2103910044446D /* TGPaintUtils.m in Sources */, D01779AB1F210A2C0044446D /* TGMediaAssetImageSignals.m in Sources */, + D07BC8A81F2A37A500ED97AA /* TGPhotoAvatarCropView.m in Sources */, + D07BC8491F2A2DA200ED97AA /* TGMenuSheetController.m in Sources */, D01778E81F20CAE60044446D /* TGNavigationBar.m in Sources */, D0177A861F2218AB0044446D /* TGModernGalleryZoomableItemView.m in Sources */, D01779321F20FFAC0044446D /* TGMediaAssetsLibrary.m in Sources */, + D07BC7711F2A2B3700ED97AA /* TGPhotoEditorCollectionView.m in Sources */, D01779E51F2139980044446D /* POPAnimationEvent.mm in Sources */, D01778F01F20CAE60044446D /* TGOverlayControllerWindow.m in Sources */, D01778341F1F961D0044446D /* TGWebPageMediaAttachment.m in Sources */, + D07BCA9D1F2B443700ED97AA /* TGMediaAssetsUtils.m in Sources */, D01778431F1F961D0044446D /* TGDocumentAttributeVideo.m in Sources */, D01778101F1F961D0044446D /* TGMessageEntity.m in Sources */, + D07BC7241F2A29E400ED97AA /* TGPhotoToolsController.m in Sources */, + D07BC7AF1F2A2B8900ED97AA /* GPUImageTwoInputFilter.m in Sources */, + D07BCA8D1F2B443700ED97AA /* TGMediaAssetsMomentsCollectionLayout.m in Sources */, + D07BC8D21F2A37EC00ED97AA /* TGPhotoPaintSparseView.m in Sources */, + D07BCA561F2A9E1600ED97AA /* TGDraggableCollectionView.m in Sources */, + D07BCA581F2A9E1600ED97AA /* TGDraggableCollectionViewFlowLayout.m in Sources */, D01779461F20FFF60044446D /* TGMediaAssetFetchResultChange.m in Sources */, + D07BCBC81F2B6F6300ED97AA /* CBPlayerLayerView.m in Sources */, + D07BC99A1F2A489C00ED97AA /* TGStickerPack.m in Sources */, + D07BC9F81F2A9A2B00ED97AA /* TGMediaPickerGalleryGifItemView.m in Sources */, + D07BCAE01F2B4F5E00ED97AA /* TGLegacyCameraController.m in Sources */, + D07BC7BF1F2A2BDD00ED97AA /* PGPhotoToolComposer.m in Sources */, + D07BCA181F2A9A2B00ED97AA /* TGMediaPickerPhotoStripView.m in Sources */, + D07BC9181F2A380D00ED97AA /* TGPaintPath.m in Sources */, + D07BCA9F1F2B443700ED97AA /* TGMediaAssetsVideoCell.m in Sources */, + D07BC9AF1F2A4A5100ED97AA /* TGItemMenuSheetPreviewView.m in Sources */, D0177B231F2641B10044446D /* PGCameraVolumeButtonHandler.m in Sources */, + D07BCADC1F2B4F2800ED97AA /* TGOverlayFormsheetWindow.m in Sources */, + D07BC73E1F2A2A7D00ED97AA /* PGPhotoEditorView.m in Sources */, + D07BCBF01F2B72DC00ED97AA /* STKDataSource.m in Sources */, D01778381F1F961D0044446D /* TGAudioMediaAttachment.m in Sources */, D017775F1F1F8FE60044446D /* TGBotComandInfo.m in Sources */, D01777FE1F1F961D0044446D /* TGMessageGroup.m in Sources */, D01778121F1F961D0044446D /* TGMessageEntityBold.m in Sources */, D01779001F20D0040044446D /* TGColor.m in Sources */, D01778201F1F961D0044446D /* TGMessageEntityMentionName.m in Sources */, + D07BCAAF1F2B45DA00ED97AA /* TGFileUtils.m in Sources */, + D07BC9021F2A380D00ED97AA /* TGPaintBrushPreview.m in Sources */, + D07BCA1A1F2A9A2B00ED97AA /* TGMediaPickerScrubberHeaderView.m in Sources */, D01778AA1F1FD0900044446D /* TGImageUtils.mm in Sources */, + D07BCADA1F2B4F2800ED97AA /* TGOverlayFormsheetController.m in Sources */, + D07BC8571F2A2DBD00ED97AA /* TGMenuSheetButtonItemView.m in Sources */, + D07BCA161F2A9A2B00ED97AA /* TGMediaPickerPhotoStripCell.m in Sources */, + D07BCBAD1F2B6F6300ED97AA /* CBConstance.m in Sources */, D017780A1F1F961D0044446D /* TGGameMediaAttachment.m in Sources */, D0177A7E1F2204640044446D /* TGModernButton.m in Sources */, + D07BC8271F2A2C0B00ED97AA /* PGTintTool.m in Sources */, + D07BC9F41F2A9A2B00ED97AA /* TGMediaPickerController.m in Sources */, D0177A121F213B440044446D /* JNWSpringAnimation.m in Sources */, D0177A9C1F22204A0044446D /* TGModernGalleryEmbeddedStickersHeaderView.m in Sources */, D017783A1F1F961D0044446D /* TGAudioWaveform.m in Sources */, + D07BCAA71F2B445E00ED97AA /* TGMediaGroupsController.m in Sources */, + D07BC9041F2A380D00ED97AA /* TGPaintBuffers.m in Sources */, + D07BCA141F2A9A2B00ED97AA /* TGMediaPickerPhotoCounterButton.m in Sources */, D01777631F1F8FE60044446D /* TGLocalization.m in Sources */, + D07BCBCA1F2B6F6300ED97AA /* CBPlayerView.m in Sources */, D0177AC81F23D92C0044446D /* ASActor.m in Sources */, D01777731F1F92420044446D /* TGPhoneUtils.m in Sources */, D017781C1F1F961D0044446D /* TGMessageEntityItalic.m in Sources */, + D07BC85B1F2A2DBD00ED97AA /* TGMenuSheetDimView.m in Sources */, + D07BC73A1F2A2A7D00ED97AA /* PGPhotoEditorRawDataInput.m in Sources */, D0177A391F21F2520044446D /* TGFullscreenContainerView.m in Sources */, + D07BC9841F2A472900ED97AA /* TGPhotoStickersCollectionLayout.m in Sources */, D01777691F1F8FE60044446D /* TGUser.m in Sources */, D017791F1F20F7090044446D /* UIDevice+PlatformInfo.m in Sources */, D01778A81F1FD0900044446D /* TGFont.mm in Sources */, D0177AA21F2222990044446D /* TGKeyCommand.m in Sources */, + D07BCAB31F2B460B00ED97AA /* TGGifConverter.m in Sources */, + D07BCAAB1F2B44C100ED97AA /* TGModernBarButton.m in Sources */, D01779E31F2139980044446D /* POPAnimation.mm in Sources */, D01779721F2103FD0044446D /* TGPaintUndoManager.m in Sources */, + D07BCA3C1F2A9BE600ED97AA /* TGModernGalleryVideoContentView.m in Sources */, + D07BC8941F2A375800ED97AA /* TGPhotoCropAreaView.m in Sources */, + D07BCBC61F2B6F6300ED97AA /* CBLibrary.m in Sources */, D017789E1F1FC99A0044446D /* LegacyComponentsInternal.m in Sources */, + D07BC9B31F2A4B6600ED97AA /* TGStickerAssociation.m in Sources */, + D07BC76F1F2A2B3700ED97AA /* TGPhotoEditorBlurView.m in Sources */, + D07BCAEE1F2B507600ED97AA /* TGAttachmentVideoCell.m in Sources */, + D07BCAC71F2B4E6200ED97AA /* TGAttachmentCameraCell.m in Sources */, D0177A3D1F21F2E50044446D /* TGTimerTarget.m in Sources */, D017784E1F1F961D0044446D /* TGForwardedMessageMediaAttachment.m in Sources */, + D07BCBD01F2B6F6300ED97AA /* NSDictionary+CBExtensions.m in Sources */, + D07BC7731F2A2B3700ED97AA /* TGPhotoEditorCurvesHistogramView.m in Sources */, + D07BCB3E1F2B65F100ED97AA /* TGWallpaperInfo.m in Sources */, + D07BCA1C1F2A9A2B00ED97AA /* TGMediaPickerSelectionGestureRecognizer.m in Sources */, + D07BC9141F2A380D00ED97AA /* TGPaintNeonBrush.m in Sources */, D01778161F1F961D0044446D /* TGMessageEntityCode.m in Sources */, + D07BC7281F2A2A5300ED97AA /* UICollectionView+Utils.m in Sources */, + D07BCB671F2B6A5600ED97AA /* TGEmbedPlayerState.m in Sources */, + D07BCA911F2B443700ED97AA /* TGMediaAssetsMomentsController.m in Sources */, + D07BCBC41F2B6F6300ED97AA /* CBJSONCoubMapper.m in Sources */, + D07BC7871F2A2B3700ED97AA /* TGPhotoEditorTabController.m in Sources */, + D07BCA121F2A9A2B00ED97AA /* TGMediaPickerModernGalleryMixin.m in Sources */, + D07BC8421F2A2D7900ED97AA /* TGPhotoCaptionController.m in Sources */, + D07BC93C1F2A3C1F00ED97AA /* TGPhotoQualityController.m in Sources */, + D07BCA001F2A9A2B00ED97AA /* TGMediaPickerGalleryPhotoItem.m in Sources */, D017785E1F1F961D0044446D /* TGActionMediaAttachment.m in Sources */, + D07BC9281F2A380D00ED97AA /* TGPaintTexture.m in Sources */, + D07BC9381F2A3BD100ED97AA /* TGPhotoStickerEntityView.m in Sources */, + D07BCA2E1F2A9A9600ED97AA /* TGModernGalleryImageItemView.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/LegacyComponents/AVAsset+CBExtension.h b/LegacyComponents/AVAsset+CBExtension.h new file mode 100755 index 0000000000..eb5cb5c3f4 --- /dev/null +++ b/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/LegacyComponents/AVAsset+CBExtension.m b/LegacyComponents/AVAsset+CBExtension.m new file mode 100755 index 0000000000..4ecf0fe334 --- /dev/null +++ b/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/LegacyComponents/CBAssetDownloadManager.h b/LegacyComponents/CBAssetDownloadManager.h new file mode 100755 index 0000000000..5b91844ea5 --- /dev/null +++ b/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/LegacyComponents/CBAssetDownloadManager.m b/LegacyComponents/CBAssetDownloadManager.m new file mode 100755 index 0000000000..36792d0480 --- /dev/null +++ b/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/LegacyComponents/CBChunkDownloadOperation.h b/LegacyComponents/CBChunkDownloadOperation.h new file mode 100755 index 0000000000..d7439ca0d3 --- /dev/null +++ b/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/LegacyComponents/CBChunkDownloadOperation.m b/LegacyComponents/CBChunkDownloadOperation.m new file mode 100755 index 0000000000..467bd77c95 --- /dev/null +++ b/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/LegacyComponents/CBConstance.h b/LegacyComponents/CBConstance.h new file mode 100755 index 0000000000..f448703234 --- /dev/null +++ b/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/LegacyComponents/CBConstance.m b/LegacyComponents/CBConstance.m new file mode 100755 index 0000000000..8528fb4e1c --- /dev/null +++ b/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/LegacyComponents/CBCoubAsset.h b/LegacyComponents/CBCoubAsset.h new file mode 100755 index 0000000000..bb921e8773 --- /dev/null +++ b/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/LegacyComponents/CBCoubAudioSource.h b/LegacyComponents/CBCoubAudioSource.h new file mode 100755 index 0000000000..a9161dab91 --- /dev/null +++ b/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/LegacyComponents/CBCoubAudioSource.m b/LegacyComponents/CBCoubAudioSource.m new file mode 100755 index 0000000000..be04846066 --- /dev/null +++ b/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/LegacyComponents/CBCoubAuthorVO.h b/LegacyComponents/CBCoubAuthorVO.h new file mode 100755 index 0000000000..2d1ca547ae --- /dev/null +++ b/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/LegacyComponents/CBCoubAuthorVO.m b/LegacyComponents/CBCoubAuthorVO.m new file mode 100755 index 0000000000..cd54782c86 --- /dev/null +++ b/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/LegacyComponents/CBCoubDownloadOperation.h b/LegacyComponents/CBCoubDownloadOperation.h new file mode 100755 index 0000000000..6fcc7eb0a5 --- /dev/null +++ b/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/LegacyComponents/CBCoubDownloadOperation.m b/LegacyComponents/CBCoubDownloadOperation.m new file mode 100755 index 0000000000..eadca4f671 --- /dev/null +++ b/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/LegacyComponents/CBCoubLoopCompositionMaker.h b/LegacyComponents/CBCoubLoopCompositionMaker.h new file mode 100755 index 0000000000..e45269c609 --- /dev/null +++ b/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/LegacyComponents/CBCoubLoopCompositionMaker.m b/LegacyComponents/CBCoubLoopCompositionMaker.m new file mode 100755 index 0000000000..73a30dd9d0 --- /dev/null +++ b/LegacyComponents/CBCoubLoopCompositionMaker.m @@ -0,0 +1,329 @@ +// +// 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(originalAudioTrack && CMTIME_COMPARE_INLINE(audioTrackTimeRange.duration, <, videoTrackTimeRange.duration)) + // { + // if(!gDigitalSilenceAsset) + // gDigitalSilenceAsset = [[AVURLAsset alloc] initWithURL:[[NSBundle mainBundle] URLForResource:@"silence2" withExtension:@"caf"] options:@{AVURLAssetPreferPreciseDurationAndTimingKey : @YES}]; + // silence = gDigitalSilenceAsset.anyAudioTrack; + // silenceTimeRange.duration = CMTimeSubtract(videoTrackTimeRange.duration, audioTrackTimeRange.duration); + // } + + 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 \ No newline at end of file diff --git a/LegacyComponents/CBCoubNew.h b/LegacyComponents/CBCoubNew.h new file mode 100755 index 0000000000..c6620636dc --- /dev/null +++ b/LegacyComponents/CBCoubNew.h @@ -0,0 +1,69 @@ +// +// 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) 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/LegacyComponents/CBCoubNew.m b/LegacyComponents/CBCoubNew.m new file mode 100755 index 0000000000..eed47b59a9 --- /dev/null +++ b/LegacyComponents/CBCoubNew.m @@ -0,0 +1,203 @@ +// +// 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.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 +{ + CBCoubNew *coub = [CBJSONCoubMapper coubFromJSONObject:attributes]; + + return coub; +} + +//- (NSURL *)coubWebViewURL +//{ +// return [NSURL URLWithString:[NSString stringWithFormat:@"%@/view/%@", kCBServerURL, self.permalink]]; +//} + +- (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/LegacyComponents/CBCoubPlayer.h b/LegacyComponents/CBCoubPlayer.h new file mode 100755 index 0000000000..b212174afe --- /dev/null +++ b/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/LegacyComponents/CBCoubPlayer.m b/LegacyComponents/CBCoubPlayer.m new file mode 100755 index 0000000000..4c2d059adc --- /dev/null +++ b/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/LegacyComponents/CBCoubPlayerContance.h b/LegacyComponents/CBCoubPlayerContance.h new file mode 100755 index 0000000000..935c8e4685 --- /dev/null +++ b/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/LegacyComponents/CBCoubPlayerContance.m b/LegacyComponents/CBCoubPlayerContance.m new file mode 100755 index 0000000000..d2850886ff --- /dev/null +++ b/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/LegacyComponents/CBCoubVideoSource.h b/LegacyComponents/CBCoubVideoSource.h new file mode 100755 index 0000000000..a07b1d4c4c --- /dev/null +++ b/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/LegacyComponents/CBCoubVideoSource.m b/LegacyComponents/CBCoubVideoSource.m new file mode 100755 index 0000000000..7226506f97 --- /dev/null +++ b/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/LegacyComponents/CBDownloadOperation.h b/LegacyComponents/CBDownloadOperation.h new file mode 100755 index 0000000000..fb98a3edfb --- /dev/null +++ b/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/LegacyComponents/CBDownloadOperationDelegate.h b/LegacyComponents/CBDownloadOperationDelegate.h new file mode 100755 index 0000000000..9639ef811b --- /dev/null +++ b/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/LegacyComponents/CBGenericDownloadOperation.h b/LegacyComponents/CBGenericDownloadOperation.h new file mode 100755 index 0000000000..6b55e97fd5 --- /dev/null +++ b/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/LegacyComponents/CBGenericDownloadOperation.m b/LegacyComponents/CBGenericDownloadOperation.m new file mode 100755 index 0000000000..a86dfc3b8c --- /dev/null +++ b/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/LegacyComponents/CBJSONCoubMapper.h b/LegacyComponents/CBJSONCoubMapper.h new file mode 100755 index 0000000000..6effbcbaec --- /dev/null +++ b/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/LegacyComponents/CBJSONCoubMapper.m b/LegacyComponents/CBJSONCoubMapper.m new file mode 100755 index 0000000000..04441ddb33 --- /dev/null +++ b/LegacyComponents/CBJSONCoubMapper.m @@ -0,0 +1,242 @@ +// +// 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]; + + NSString *remoteVideoLocation = nil; + NSDictionary *fileVersions = attributes[@"file_versions"]; + if(fileVersions) + { + remoteVideoLocation = fileVersions[@"iphone"][@"url"]; + } + if(!remoteVideoLocation) + remoteVideoLocation = attributes[@"file"]; + if(!remoteVideoLocation || [remoteVideoLocation isEqual:[NSNull null]]) + remoteVideoLocation = fileVersions[@"mobile"][@"gifv"]; + + if(remoteVideoLocation) + { + // + 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; + } + +// coub.remoteAudioLocationPattern = @"http://cdn1.akamai.coub.com/coub/simple/cw_audio/da1afb3df6f/734ed138b3577134af8bb/mp3_low_c%i_1400747442_out.mp3"; + + // 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/LegacyComponents/CBLibrary.h b/LegacyComponents/CBLibrary.h new file mode 100755 index 0000000000..bc708069dd --- /dev/null +++ b/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/LegacyComponents/CBLibrary.m b/LegacyComponents/CBLibrary.m new file mode 100755 index 0000000000..609349f176 --- /dev/null +++ b/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/LegacyComponents/CBPlayerLayerView.m b/LegacyComponents/CBPlayerLayerView.m new file mode 100755 index 0000000000..9e25cc8573 --- /dev/null +++ b/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/LegacyComponents/CBPlayerView.h b/LegacyComponents/CBPlayerView.h new file mode 100755 index 0000000000..06350aee73 --- /dev/null +++ b/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/LegacyComponents/CBPlayerView.m b/LegacyComponents/CBPlayerView.m new file mode 100755 index 0000000000..9bda935f06 --- /dev/null +++ b/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/LegacyComponents/CBTagNew.h b/LegacyComponents/CBTagNew.h new file mode 100755 index 0000000000..6052d00c11 --- /dev/null +++ b/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/LegacyComponents/CBTagNew.m b/LegacyComponents/CBTagNew.m new file mode 100755 index 0000000000..7229d24414 --- /dev/null +++ b/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/LegacyComponents/CBVideoPlayer.h b/LegacyComponents/CBVideoPlayer.h new file mode 100755 index 0000000000..7bc9cb7f0b --- /dev/null +++ b/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/LegacyComponents/CBVideoPlayer.m b/LegacyComponents/CBVideoPlayer.m new file mode 100755 index 0000000000..6fb54fc516 --- /dev/null +++ b/LegacyComponents/CBVideoPlayer.m @@ -0,0 +1,198 @@ +// +// 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/LegacyComponents/FLAnimatedImage.h b/LegacyComponents/FLAnimatedImage.h new file mode 100644 index 0000000000..657eeec99f --- /dev/null +++ b/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/LegacyComponents/FLAnimatedImage.m b/LegacyComponents/FLAnimatedImage.m new file mode 100644 index 0000000000..dd3014c232 --- /dev/null +++ b/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/LegacyComponents/GLProgram.h b/LegacyComponents/GLProgram.h new file mode 100755 index 0000000000..572d8ae9d0 --- /dev/null +++ b/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/LegacyComponents/GLProgram.m b/LegacyComponents/GLProgram.m new file mode 100755 index 0000000000..ab6320e85b --- /dev/null +++ b/LegacyComponents/GLProgram.m @@ -0,0 +1,273 @@ +// 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 "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 = [[NSBundle mainBundle] pathForResource:fShaderFilename ofType:@"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 = [[NSBundle mainBundle] pathForResource:vShaderFilename ofType:@"vsh"]; + NSString *vertexShaderString = [NSString stringWithContentsOfFile:vertShaderPathname encoding:NSUTF8StringEncoding error:nil]; + + NSString *fragShaderPathname = [[NSBundle mainBundle] pathForResource:fShaderFilename ofType:@"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/LegacyComponents/GPUImage.h b/LegacyComponents/GPUImage.h new file mode 100755 index 0000000000..7f2e0b82a2 --- /dev/null +++ b/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/LegacyComponents/GPUImageContext.h b/LegacyComponents/GPUImageContext.h new file mode 100755 index 0000000000..e70174c73f --- /dev/null +++ b/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/LegacyComponents/GPUImageContext.m b/LegacyComponents/GPUImageContext.m new file mode 100755 index 0000000000..941c37601c --- /dev/null +++ b/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/LegacyComponents/GPUImageFilter.h b/LegacyComponents/GPUImageFilter.h new file mode 100755 index 0000000000..bb8fa97096 --- /dev/null +++ b/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/LegacyComponents/GPUImageFilter.m b/LegacyComponents/GPUImageFilter.m new file mode 100755 index 0000000000..68b713fb1f --- /dev/null +++ b/LegacyComponents/GPUImageFilter.m @@ -0,0 +1,762 @@ +#import "GPUImageFilter.h" +#import + +// 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 = [[NSBundle mainBundle] pathForResource:fragmentShaderFilename ofType:@"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/LegacyComponents/GPUImageFramebuffer.h b/LegacyComponents/GPUImageFramebuffer.h new file mode 100755 index 0000000000..7c71618410 --- /dev/null +++ b/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/LegacyComponents/GPUImageFramebuffer.m b/LegacyComponents/GPUImageFramebuffer.m new file mode 100755 index 0000000000..ac289c817a --- /dev/null +++ b/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/LegacyComponents/GPUImageFramebufferCache.h b/LegacyComponents/GPUImageFramebufferCache.h new file mode 100755 index 0000000000..e56a345663 --- /dev/null +++ b/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/LegacyComponents/GPUImageFramebufferCache.m b/LegacyComponents/GPUImageFramebufferCache.m new file mode 100755 index 0000000000..8b9e7f1b30 --- /dev/null +++ b/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/LegacyComponents/GPUImageOutput.h b/LegacyComponents/GPUImageOutput.h new file mode 100755 index 0000000000..c8f3178004 --- /dev/null +++ b/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/LegacyComponents/GPUImageOutput.m b/LegacyComponents/GPUImageOutput.m new file mode 100755 index 0000000000..2d873f7243 --- /dev/null +++ b/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/LegacyComponents/GPUImageTwoInputFilter.h b/LegacyComponents/GPUImageTwoInputFilter.h new file mode 100755 index 0000000000..da3a1345e5 --- /dev/null +++ b/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/LegacyComponents/GPUImageTwoInputFilter.m b/LegacyComponents/GPUImageTwoInputFilter.m new file mode 100755 index 0000000000..41dea616f9 --- /dev/null +++ b/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/LegacyComponents/HPGrowingTextView.h b/LegacyComponents/HPGrowingTextView.h new file mode 100644 index 0000000000..c8bccf990a --- /dev/null +++ b/LegacyComponents/HPGrowingTextView.h @@ -0,0 +1,120 @@ +/* + * This is the source code of Telegram for iOS v. 1.1 + * It is licensed under GNU GPL v. 2 or later. + * You should have received a copy of the license in this archive (see LICENSE). + * + * Copyright Peter Iakovlev, 2013. + */ + +// +// 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) 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)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/LegacyComponents/HPGrowingTextView.m b/LegacyComponents/HPGrowingTextView.m new file mode 100644 index 0000000000..9772d236f1 --- /dev/null +++ b/LegacyComponents/HPGrowingTextView.m @@ -0,0 +1,897 @@ +#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(16)}; + } 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 { + NSMutableAttributedString *fixedFontString = [[NSMutableAttributedString alloc] initWithAttributedString:newText]; + [fixedFontString removeAttribute:NSFontAttributeName range:NSMakeRange(0, fixedFontString.length)]; + [fixedFontString addAttribute:NSFontAttributeName value:_intrinsicTextFont == nil ? [UIFont systemFontOfSize:16.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; +} + +- (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:@""]; + 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]; + 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:TGAccentColor() 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:TGAccentColor() 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/LegacyComponents/HPTextViewInternal.h b/LegacyComponents/HPTextViewInternal.h new file mode 100644 index 0000000000..7cc269f42b --- /dev/null +++ b/LegacyComponents/HPTextViewInternal.h @@ -0,0 +1,67 @@ +/* + * This is the source code of Telegram for iOS v. 1.1 + * It is licensed under GNU GPL v. 2 or later. + * You should have received a copy of the license in this archive (see LICENSE). + * + * Copyright Peter Iakovlev, 2013. + */ + +// +// 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/LegacyComponents/HPTextViewInternal.m b/LegacyComponents/HPTextViewInternal.m new file mode 100644 index 0000000000..390ae63f52 --- /dev/null +++ b/LegacyComponents/HPTextViewInternal.m @@ -0,0 +1,308 @@ +#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; + } + + //TGLog(@"canPerformAction %@", NSStringFromSelector(action)); + + 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/LegacyComponents/LegacyComponents.h b/LegacyComponents/LegacyComponents.h index 9dc6b82299..484bf8169e 100644 --- a/LegacyComponents/LegacyComponents.h +++ b/LegacyComponents/LegacyComponents.h @@ -31,6 +31,8 @@ FOUNDATION_EXPORT const unsigned char LegacyComponentsVersionString[]; #import #import #import +#import +#import #import #import @@ -63,6 +65,11 @@ FOUNDATION_EXPORT const unsigned char LegacyComponentsVersionString[]; #import #import +#import +#import +#import +#import + #import #import #import @@ -116,6 +123,9 @@ FOUNDATION_EXPORT const unsigned char LegacyComponentsVersionString[]; #import #import #import +#import +#import +#import #import #import @@ -140,6 +150,35 @@ 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 @@ -197,6 +236,7 @@ FOUNDATION_EXPORT const unsigned char LegacyComponentsVersionString[]; #import #import +#import #import #import @@ -213,3 +253,16 @@ FOUNDATION_EXPORT const unsigned char LegacyComponentsVersionString[]; #import #import #import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import diff --git a/LegacyComponents/LegacyComponentsContext.h b/LegacyComponents/LegacyComponentsContext.h index 15c74c5088..82ab6f1f06 100644 --- a/LegacyComponents/LegacyComponentsContext.h +++ b/LegacyComponents/LegacyComponentsContext.h @@ -1,8 +1,50 @@ #import #import +@class TGKeyCommandController; +@class SSignal; + +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 - (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)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; @end diff --git a/LegacyComponents/LegacyComponentsContext.m b/LegacyComponents/LegacyComponentsContext.m new file mode 100644 index 0000000000..4305eeb76d --- /dev/null +++ b/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/LegacyComponents/LegacyComponentsGlobals.h b/LegacyComponents/LegacyComponentsGlobals.h index fcae9484b6..d937f4eecf 100644 --- a/LegacyComponents/LegacyComponentsGlobals.h +++ b/LegacyComponents/LegacyComponentsGlobals.h @@ -2,11 +2,15 @@ #import #import +#import #import @class TGLocalization; @class UIViewController; +@class TGWallpaperInfo; +@class TGMemoryImageCache; +@class TGImageMediaAttachment; typedef enum { TGAudioSessionTypePlayVoice, @@ -28,6 +32,7 @@ typedef enum { - (UIWindow *)applicationKeyboardWindow; - (UIApplication *)applicationInstance; +- (UIInterfaceOrientation)applicationStatusBarOrientation; - (CGRect)statusBarFrame; - (bool)isStatusBarHidden; - (void)setStatusBarHidden:(BOOL)hidden withAnimation:(UIStatusBarAnimation)animation; @@ -35,17 +40,45 @@ typedef enum { - (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; + @end @interface LegacyComponentsGlobals : NSObject diff --git a/LegacyComponents/LegacyComponentsInternal.h b/LegacyComponents/LegacyComponentsInternal.h index cf81d6a534..ace04dbe2d 100644 --- a/LegacyComponents/LegacyComponentsInternal.h +++ b/LegacyComponents/LegacyComponentsInternal.h @@ -28,6 +28,9 @@ inline void TGDispatchAfter(double delay, dispatch_queue_t queue, dispatch_block dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)((delay) * NSEC_PER_SEC)), queue, 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]) diff --git a/LegacyComponents/LegacyComponentsInternal.m b/LegacyComponents/LegacyComponentsInternal.m index 8486d9f514..fab18244f7 100644 --- a/LegacyComponents/LegacyComponentsInternal.m +++ b/LegacyComponents/LegacyComponentsInternal.m @@ -4,6 +4,8 @@ #import +#import + TGLocalization *effectiveLocalization() { return [[LegacyComponentsGlobals provider] effectiveLocalization]; } @@ -108,3 +110,34 @@ NSString *TGEncodeText(NSString *string, int key) 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; +} diff --git a/LegacyComponents/LegacyHTTPRequestOperation.h b/LegacyComponents/LegacyHTTPRequestOperation.h new file mode 100644 index 0000000000..fab3b8accb --- /dev/null +++ b/LegacyComponents/LegacyHTTPRequestOperation.h @@ -0,0 +1,10 @@ +#import + +@protocol LegacyHTTPRequestOperation + +- (void)setOutputStream:(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/LegacyComponents/NSDictionary+CBExtensions.h b/LegacyComponents/NSDictionary+CBExtensions.h new file mode 100755 index 0000000000..285d40b3ba --- /dev/null +++ b/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/LegacyComponents/NSDictionary+CBExtensions.m b/LegacyComponents/NSDictionary+CBExtensions.m new file mode 100755 index 0000000000..affd8281b4 --- /dev/null +++ b/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/LegacyComponents/NSMutableArray+STKAudioPlayer.h b/LegacyComponents/NSMutableArray+STKAudioPlayer.h new file mode 100755 index 0000000000..12ca8396b4 --- /dev/null +++ b/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/LegacyComponents/NSMutableArray+STKAudioPlayer.m b/LegacyComponents/NSMutableArray+STKAudioPlayer.m new file mode 100755 index 0000000000..493868f45d --- /dev/null +++ b/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/LegacyComponents/PGBlurTool.h b/LegacyComponents/PGBlurTool.h new file mode 100644 index 0000000000..0bf2e80908 --- /dev/null +++ b/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/LegacyComponents/PGBlurTool.m b/LegacyComponents/PGBlurTool.m new file mode 100644 index 0000000000..cd9233928c --- /dev/null +++ b/LegacyComponents/PGBlurTool.m @@ -0,0 +1,223 @@ +#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"); +} + +- (UIImage *)image +{ + return [UIImage imageNamed:@"PhotoEditorBlurTool"]; +} + +- (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/LegacyComponents/PGContrastTool.h b/LegacyComponents/PGContrastTool.h new file mode 100644 index 0000000000..5be618be1e --- /dev/null +++ b/LegacyComponents/PGContrastTool.h @@ -0,0 +1,5 @@ +#import "PGPhotoTool.h" + +@interface PGContrastTool : PGPhotoTool + +@end diff --git a/LegacyComponents/PGContrastTool.m b/LegacyComponents/PGContrastTool.m new file mode 100644 index 0000000000..c0343866bc --- /dev/null +++ b/LegacyComponents/PGContrastTool.m @@ -0,0 +1,73 @@ +#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"); +} + +- (UIImage *)image +{ + return [UIImage imageNamed:@"PhotoEditorContrastTool"]; +} + +- (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/LegacyComponents/PGCurvesTool.h b/LegacyComponents/PGCurvesTool.h new file mode 100644 index 0000000000..9dec83abd4 --- /dev/null +++ b/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/LegacyComponents/PGCurvesTool.m b/LegacyComponents/PGCurvesTool.m new file mode 100644 index 0000000000..92258ea117 --- /dev/null +++ b/LegacyComponents/PGCurvesTool.m @@ -0,0 +1,364 @@ +#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"); +} + +- (UIImage *)image +{ + return [UIImage imageNamed:@"PhotoEditorCurvesTool"]; +} + +- (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/LegacyComponents/PGEnhanceTool.h b/LegacyComponents/PGEnhanceTool.h new file mode 100644 index 0000000000..6429846998 --- /dev/null +++ b/LegacyComponents/PGEnhanceTool.h @@ -0,0 +1,5 @@ +#import "PGPhotoTool.h" + +@interface PGEnhanceTool : PGPhotoTool + +@end diff --git a/LegacyComponents/PGEnhanceTool.m b/LegacyComponents/PGEnhanceTool.m new file mode 100644 index 0000000000..a7d8691c85 --- /dev/null +++ b/LegacyComponents/PGEnhanceTool.m @@ -0,0 +1,57 @@ +#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"); +} + +- (UIImage *)image +{ + return [UIImage imageNamed:@"PhotoEditorEnhanceTool"]; +} + +- (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/LegacyComponents/PGExposureTool.h b/LegacyComponents/PGExposureTool.h new file mode 100644 index 0000000000..0868856f15 --- /dev/null +++ b/LegacyComponents/PGExposureTool.h @@ -0,0 +1,5 @@ +#import "PGPhotoTool.h" + +@interface PGExposureTool : PGPhotoTool + +@end diff --git a/LegacyComponents/PGExposureTool.m b/LegacyComponents/PGExposureTool.m new file mode 100644 index 0000000000..8fd8800e7d --- /dev/null +++ b/LegacyComponents/PGExposureTool.m @@ -0,0 +1,84 @@ +#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"); +} + +- (UIImage *)image +{ + return [UIImage imageNamed:@"PhotoEditorExposureTool"]; +} + +- (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/LegacyComponents/PGFadeTool.h b/LegacyComponents/PGFadeTool.h new file mode 100644 index 0000000000..b3db968425 --- /dev/null +++ b/LegacyComponents/PGFadeTool.h @@ -0,0 +1,5 @@ +#import "PGPhotoTool.h" + +@interface PGFadeTool : PGPhotoTool + +@end diff --git a/LegacyComponents/PGFadeTool.m b/LegacyComponents/PGFadeTool.m new file mode 100644 index 0000000000..5121f0695f --- /dev/null +++ b/LegacyComponents/PGFadeTool.m @@ -0,0 +1,101 @@ +#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"); +} + +- (UIImage *)image +{ + return [UIImage imageNamed:@"PhotoEditorFadeTool"]; +} + +- (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/LegacyComponents/PGGrainTool.h b/LegacyComponents/PGGrainTool.h new file mode 100644 index 0000000000..36e3139db9 --- /dev/null +++ b/LegacyComponents/PGGrainTool.h @@ -0,0 +1,5 @@ +#import "PGPhotoTool.h" + +@interface PGGrainTool : PGPhotoTool + +@end diff --git a/LegacyComponents/PGGrainTool.m b/LegacyComponents/PGGrainTool.m new file mode 100644 index 0000000000..30b48fcdc5 --- /dev/null +++ b/LegacyComponents/PGGrainTool.m @@ -0,0 +1,163 @@ +#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"); +} + +- (UIImage *)image +{ + return [UIImage imageNamed:@"PhotoEditorGrainTool"]; +} + +- (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/LegacyComponents/PGHighlightsTool.h b/LegacyComponents/PGHighlightsTool.h new file mode 100644 index 0000000000..47f361beee --- /dev/null +++ b/LegacyComponents/PGHighlightsTool.h @@ -0,0 +1,5 @@ +#import "PGPhotoTool.h" + +@interface PGHighlightsTool : PGPhotoTool + +@end diff --git a/LegacyComponents/PGHighlightsTool.m b/LegacyComponents/PGHighlightsTool.m new file mode 100644 index 0000000000..6bdce97069 --- /dev/null +++ b/LegacyComponents/PGHighlightsTool.m @@ -0,0 +1,90 @@ +#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"); +} + +- (UIImage *)image +{ + return [UIImage imageNamed:@"PhotoEditorHighlightsTool"]; +} + +- (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/LegacyComponents/PGPhotoBlurPass.h b/LegacyComponents/PGPhotoBlurPass.h new file mode 100644 index 0000000000..44f7b13a2a --- /dev/null +++ b/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/LegacyComponents/PGPhotoBlurPass.m b/LegacyComponents/PGPhotoBlurPass.m new file mode 100644 index 0000000000..50e5ef9b7e --- /dev/null +++ b/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/LegacyComponents/PGPhotoCustomFilterPass.h b/LegacyComponents/PGPhotoCustomFilterPass.h new file mode 100644 index 0000000000..8a493a4b0b --- /dev/null +++ b/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/LegacyComponents/PGPhotoCustomFilterPass.m b/LegacyComponents/PGPhotoCustomFilterPass.m new file mode 100644 index 0000000000..b06b970779 --- /dev/null +++ b/LegacyComponents/PGPhotoCustomFilterPass.m @@ -0,0 +1,427 @@ +#import "PGPhotoCustomFilterPass.h" + +#import "PGPhotoEditorPicture.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 = [[NSBundle mainBundle] pathForResource:shaderFile ofType:@"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 = [[NSBundle mainBundle] pathForResource:name ofType: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/LegacyComponents/PGPhotoEditor.h b/LegacyComponents/PGPhotoEditor.h new file mode 100644 index 0000000000..1ab7a54342 --- /dev/null +++ b/LegacyComponents/PGPhotoEditor.h @@ -0,0 +1,48 @@ +#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; + +- (instancetype)initWithOriginalSize:(CGSize)originalSize adjustments:(id)adjustments forVideo:(bool)forVideo; + +- (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/LegacyComponents/PGPhotoEditor.m b/LegacyComponents/PGPhotoEditor.m new file mode 100644 index 0000000000..e428723bee --- /dev/null +++ b/LegacyComponents/PGPhotoEditor.m @@ -0,0 +1,397 @@ +#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 +{ + self = [super init]; + if (self != nil) + { + _queue = [[SQueue alloc] init]; + + _forVideo = forVideo; + + _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]; + } + 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/LegacyComponents/PGPhotoEditorItem.h b/LegacyComponents/PGPhotoEditorItem.h new file mode 100644 index 0000000000..777fe85af8 --- /dev/null +++ b/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/LegacyComponents/PGPhotoEditorPicture.h b/LegacyComponents/PGPhotoEditorPicture.h new file mode 100644 index 0000000000..6231fcc3ad --- /dev/null +++ b/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/LegacyComponents/PGPhotoEditorPicture.m b/LegacyComponents/PGPhotoEditorPicture.m new file mode 100644 index 0000000000..399acd45c3 --- /dev/null +++ b/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/LegacyComponents/PGPhotoEditorRawDataInput.h b/LegacyComponents/PGPhotoEditorRawDataInput.h new file mode 100644 index 0000000000..6c83a9d8e7 --- /dev/null +++ b/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/LegacyComponents/PGPhotoEditorRawDataInput.m b/LegacyComponents/PGPhotoEditorRawDataInput.m new file mode 100644 index 0000000000..c21806465f --- /dev/null +++ b/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/LegacyComponents/PGPhotoEditorRawDataOutput.h b/LegacyComponents/PGPhotoEditorRawDataOutput.h new file mode 100644 index 0000000000..5ebe76b771 --- /dev/null +++ b/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/LegacyComponents/PGPhotoEditorRawDataOutput.m b/LegacyComponents/PGPhotoEditorRawDataOutput.m new file mode 100644 index 0000000000..ebdb30d5d2 --- /dev/null +++ b/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/LegacyComponents/PGPhotoEditorView.h b/LegacyComponents/PGPhotoEditorView.h new file mode 100644 index 0000000000..e514b5c0fe --- /dev/null +++ b/LegacyComponents/PGPhotoEditorView.h @@ -0,0 +1,9 @@ +#import + +#import "GPUImageContext.h" + +@interface PGPhotoEditorView : UIView + +@property (nonatomic, assign) bool enabled; + +@end diff --git a/LegacyComponents/PGPhotoEditorView.m b/LegacyComponents/PGPhotoEditorView.m new file mode 100644 index 0000000000..4f00bf82ff --- /dev/null +++ b/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/LegacyComponents/PGPhotoEnhanceColorConversionFilter.h b/LegacyComponents/PGPhotoEnhanceColorConversionFilter.h new file mode 100644 index 0000000000..a4e2c94cce --- /dev/null +++ b/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/LegacyComponents/PGPhotoEnhanceColorConversionFilter.m b/LegacyComponents/PGPhotoEnhanceColorConversionFilter.m new file mode 100644 index 0000000000..2ae93f94c2 --- /dev/null +++ b/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/LegacyComponents/PGPhotoEnhanceInterpolationFilter.h b/LegacyComponents/PGPhotoEnhanceInterpolationFilter.h new file mode 100644 index 0000000000..46ca5dbcd3 --- /dev/null +++ b/LegacyComponents/PGPhotoEnhanceInterpolationFilter.h @@ -0,0 +1,7 @@ +#import "GPUImageTwoInputFilter.h" + +@interface PGPhotoEnhanceInterpolationFilter : GPUImageTwoInputFilter + +@property (nonatomic, assign) CGFloat intensity; + +@end diff --git a/LegacyComponents/PGPhotoEnhanceInterpolationFilter.m b/LegacyComponents/PGPhotoEnhanceInterpolationFilter.m new file mode 100644 index 0000000000..981cc2d8a6 --- /dev/null +++ b/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/LegacyComponents/PGPhotoEnhanceLUTGenerator.h b/LegacyComponents/PGPhotoEnhanceLUTGenerator.h new file mode 100644 index 0000000000..dbf59e8b00 --- /dev/null +++ b/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/LegacyComponents/PGPhotoEnhanceLUTGenerator.m b/LegacyComponents/PGPhotoEnhanceLUTGenerator.m new file mode 100644 index 0000000000..c87e7f58e2 --- /dev/null +++ b/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/LegacyComponents/PGPhotoEnhancePass.h b/LegacyComponents/PGPhotoEnhancePass.h new file mode 100644 index 0000000000..8af1c95299 --- /dev/null +++ b/LegacyComponents/PGPhotoEnhancePass.h @@ -0,0 +1,7 @@ +#import "PGPhotoProcessPass.h" + +@interface PGPhotoEnhancePass : PGPhotoProcessPass + +@property (nonatomic, assign) CGFloat intensity; + +@end diff --git a/LegacyComponents/PGPhotoEnhancePass.m b/LegacyComponents/PGPhotoEnhancePass.m new file mode 100644 index 0000000000..9ca59767ee --- /dev/null +++ b/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/LegacyComponents/PGPhotoFilter.h b/LegacyComponents/PGPhotoFilter.h new file mode 100644 index 0000000000..eddd041ec5 --- /dev/null +++ b/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/LegacyComponents/PGPhotoFilter.m b/LegacyComponents/PGPhotoFilter.m new file mode 100644 index 0000000000..f37c7bef8d --- /dev/null +++ b/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/LegacyComponents/PGPhotoFilterDefinition.h b/LegacyComponents/PGPhotoFilterDefinition.h new file mode 100644 index 0000000000..1c15ed0746 --- /dev/null +++ b/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/LegacyComponents/PGPhotoFilterDefinition.m b/LegacyComponents/PGPhotoFilterDefinition.m new file mode 100644 index 0000000000..f2f3b27995 --- /dev/null +++ b/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/LegacyComponents/PGPhotoFilterThumbnailManager.h b/LegacyComponents/PGPhotoFilterThumbnailManager.h new file mode 100644 index 0000000000..b215d589ce --- /dev/null +++ b/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/LegacyComponents/PGPhotoFilterThumbnailManager.m b/LegacyComponents/PGPhotoFilterThumbnailManager.m new file mode 100644 index 0000000000..c1594aa32e --- /dev/null +++ b/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/LegacyComponents/PGPhotoGaussianBlurFilter.h b/LegacyComponents/PGPhotoGaussianBlurFilter.h new file mode 100644 index 0000000000..e34f1cb089 --- /dev/null +++ b/LegacyComponents/PGPhotoGaussianBlurFilter.h @@ -0,0 +1,5 @@ +#import "GPUImageFilter.h" + +@interface PGPhotoGaussianBlurFilter : GPUImageFilter + +@end diff --git a/LegacyComponents/PGPhotoGaussianBlurFilter.m b/LegacyComponents/PGPhotoGaussianBlurFilter.m new file mode 100644 index 0000000000..575f2a01b3 --- /dev/null +++ b/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/LegacyComponents/PGPhotoHistogram.h b/LegacyComponents/PGPhotoHistogram.h new file mode 100644 index 0000000000..43e80eaaa7 --- /dev/null +++ b/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/LegacyComponents/PGPhotoHistogram.m b/LegacyComponents/PGPhotoHistogram.m new file mode 100644 index 0000000000..8be0ab82e5 --- /dev/null +++ b/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/LegacyComponents/PGPhotoHistogramGenerator.h b/LegacyComponents/PGPhotoHistogramGenerator.h new file mode 100644 index 0000000000..51df4ef99e --- /dev/null +++ b/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/LegacyComponents/PGPhotoHistogramGenerator.m b/LegacyComponents/PGPhotoHistogramGenerator.m new file mode 100644 index 0000000000..fc24d986b3 --- /dev/null +++ b/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/LegacyComponents/PGPhotoLookupFilterPass.h b/LegacyComponents/PGPhotoLookupFilterPass.h new file mode 100644 index 0000000000..c836be286e --- /dev/null +++ b/LegacyComponents/PGPhotoLookupFilterPass.h @@ -0,0 +1,7 @@ +#import "PGPhotoCustomFilterPass.h" + +@interface PGPhotoLookupFilterPass : PGPhotoCustomFilterPass + +- (instancetype)initWithLookupImage:(UIImage *)lookupImage; + +@end diff --git a/LegacyComponents/PGPhotoLookupFilterPass.m b/LegacyComponents/PGPhotoLookupFilterPass.m new file mode 100644 index 0000000000..439f24845b --- /dev/null +++ b/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/LegacyComponents/PGPhotoProcessPass.h b/LegacyComponents/PGPhotoProcessPass.h new file mode 100644 index 0000000000..4ed999347f --- /dev/null +++ b/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/LegacyComponents/PGPhotoProcessPass.m b/LegacyComponents/PGPhotoProcessPass.m new file mode 100644 index 0000000000..0fdf46835f --- /dev/null +++ b/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/LegacyComponents/PGPhotoSharpenPass.h b/LegacyComponents/PGPhotoSharpenPass.h new file mode 100644 index 0000000000..f33e500f8f --- /dev/null +++ b/LegacyComponents/PGPhotoSharpenPass.h @@ -0,0 +1,7 @@ +#import "PGPhotoProcessPass.h" + +@interface PGPhotoSharpenPass : PGPhotoProcessPass + +@property (nonatomic, assign) CGFloat sharpness; + +@end diff --git a/LegacyComponents/PGPhotoSharpenPass.m b/LegacyComponents/PGPhotoSharpenPass.m new file mode 100644 index 0000000000..453cb7f2e7 --- /dev/null +++ b/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/LegacyComponents/PGPhotoTool.h b/LegacyComponents/PGPhotoTool.h new file mode 100644 index 0000000000..55a7e8c9e8 --- /dev/null +++ b/LegacyComponents/PGPhotoTool.h @@ -0,0 +1,56 @@ +#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) UIImage *image; + +@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/LegacyComponents/PGPhotoTool.m b/LegacyComponents/PGPhotoTool.m new file mode 100644 index 0000000000..1e321d907c --- /dev/null +++ b/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/LegacyComponents/PGPhotoToolComposer.h b/LegacyComponents/PGPhotoToolComposer.h new file mode 100644 index 0000000000..fdf65a71a8 --- /dev/null +++ b/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/LegacyComponents/PGPhotoToolComposer.m b/LegacyComponents/PGPhotoToolComposer.m new file mode 100644 index 0000000000..e4cf7a3248 --- /dev/null +++ b/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/LegacyComponents/PGSaturationTool.h b/LegacyComponents/PGSaturationTool.h new file mode 100644 index 0000000000..943a80a4de --- /dev/null +++ b/LegacyComponents/PGSaturationTool.h @@ -0,0 +1,5 @@ +#import "PGPhotoTool.h" + +@interface PGSaturationTool : PGPhotoTool + +@end diff --git a/LegacyComponents/PGSaturationTool.m b/LegacyComponents/PGSaturationTool.m new file mode 100644 index 0000000000..87fc1c8490 --- /dev/null +++ b/LegacyComponents/PGSaturationTool.m @@ -0,0 +1,80 @@ +#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"); +} + +- (UIImage *)image +{ + return [UIImage imageNamed:@"PhotoEditorSaturationTool"]; +} + +- (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/LegacyComponents/PGShadowsTool.h b/LegacyComponents/PGShadowsTool.h new file mode 100644 index 0000000000..048891b698 --- /dev/null +++ b/LegacyComponents/PGShadowsTool.h @@ -0,0 +1,5 @@ +#import "PGPhotoTool.h" + +@interface PGShadowsTool : PGPhotoTool + +@end diff --git a/LegacyComponents/PGShadowsTool.m b/LegacyComponents/PGShadowsTool.m new file mode 100644 index 0000000000..aea61ee457 --- /dev/null +++ b/LegacyComponents/PGShadowsTool.m @@ -0,0 +1,65 @@ +#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"); +} + +- (UIImage *)image +{ + return [UIImage imageNamed:@"PhotoEditorShadowsTool"]; +} + +- (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/LegacyComponents/PGSharpenTool.h b/LegacyComponents/PGSharpenTool.h new file mode 100644 index 0000000000..d62b457841 --- /dev/null +++ b/LegacyComponents/PGSharpenTool.h @@ -0,0 +1,5 @@ +#import "PGPhotoTool.h" + +@interface PGSharpenTool : PGPhotoTool + +@end diff --git a/LegacyComponents/PGSharpenTool.m b/LegacyComponents/PGSharpenTool.m new file mode 100644 index 0000000000..641f8b089d --- /dev/null +++ b/LegacyComponents/PGSharpenTool.m @@ -0,0 +1,58 @@ +#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"); +} + +- (UIImage *)image +{ + return [UIImage imageNamed:@"PhotoEditorSharpenTool"]; +} + +- (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/LegacyComponents/PGTintTool.h b/LegacyComponents/PGTintTool.h new file mode 100644 index 0000000000..74e710aa38 --- /dev/null +++ b/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/LegacyComponents/PGTintTool.m b/LegacyComponents/PGTintTool.m new file mode 100644 index 0000000000..5c4ab532bf --- /dev/null +++ b/LegacyComponents/PGTintTool.m @@ -0,0 +1,239 @@ +#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"); +} + +- (UIImage *)image +{ + return [UIImage imageNamed:@"PhotoEditorTintTool"]; +} + +- (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/LegacyComponents/PGVignetteTool.h b/LegacyComponents/PGVignetteTool.h new file mode 100644 index 0000000000..d93512e9a5 --- /dev/null +++ b/LegacyComponents/PGVignetteTool.h @@ -0,0 +1,5 @@ +#import "PGPhotoTool.h" + +@interface PGVignetteTool : PGPhotoTool + +@end diff --git a/LegacyComponents/PGVignetteTool.m b/LegacyComponents/PGVignetteTool.m new file mode 100644 index 0000000000..a13709dae3 --- /dev/null +++ b/LegacyComponents/PGVignetteTool.m @@ -0,0 +1,80 @@ +#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"); +} + +- (UIImage *)image +{ + return [UIImage imageNamed:@"PhotoEditorVignetteTool"]; +} + +- (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/LegacyComponents/PGWarmthTool.h b/LegacyComponents/PGWarmthTool.h new file mode 100644 index 0000000000..7d9c49d0fb --- /dev/null +++ b/LegacyComponents/PGWarmthTool.h @@ -0,0 +1,5 @@ +#import "PGPhotoTool.h" + +@interface PGWarmthTool : PGPhotoTool + +@end diff --git a/LegacyComponents/PGWarmthTool.m b/LegacyComponents/PGWarmthTool.m new file mode 100644 index 0000000000..33e0ada382 --- /dev/null +++ b/LegacyComponents/PGWarmthTool.m @@ -0,0 +1,90 @@ +#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"); +} + +- (UIImage *)image +{ + return [UIImage imageNamed:@"PhotoEditorWarmthTool"]; +} + +- (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/LegacyComponents/STKAudioPlayer.h b/LegacyComponents/STKAudioPlayer.h new file mode 100755 index 0000000000..0ce6d53852 --- /dev/null +++ b/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/LegacyComponents/STKAudioPlayer.m b/LegacyComponents/STKAudioPlayer.m new file mode 100755 index 0000000000..a00a883d42 --- /dev/null +++ b/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/LegacyComponents/STKAutoRecoveringHTTPDataSource.h b/LegacyComponents/STKAutoRecoveringHTTPDataSource.h new file mode 100755 index 0000000000..c55895f3b6 --- /dev/null +++ b/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/LegacyComponents/STKAutoRecoveringHTTPDataSource.m b/LegacyComponents/STKAutoRecoveringHTTPDataSource.m new file mode 100755 index 0000000000..37fa5e5a93 --- /dev/null +++ b/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/LegacyComponents/STKCoreFoundationDataSource.h b/LegacyComponents/STKCoreFoundationDataSource.h new file mode 100755 index 0000000000..731de5d703 --- /dev/null +++ b/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/LegacyComponents/STKCoreFoundationDataSource.m b/LegacyComponents/STKCoreFoundationDataSource.m new file mode 100755 index 0000000000..1763acd57a --- /dev/null +++ b/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/LegacyComponents/STKDataSource.h b/LegacyComponents/STKDataSource.h new file mode 100755 index 0000000000..69c296196f --- /dev/null +++ b/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/LegacyComponents/STKDataSource.m b/LegacyComponents/STKDataSource.m new file mode 100755 index 0000000000..9abd8215a3 --- /dev/null +++ b/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/LegacyComponents/STKDataSourceWrapper.h b/LegacyComponents/STKDataSourceWrapper.h new file mode 100755 index 0000000000..4d3e035d5e --- /dev/null +++ b/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/LegacyComponents/STKDataSourceWrapper.m b/LegacyComponents/STKDataSourceWrapper.m new file mode 100755 index 0000000000..76e1634d91 --- /dev/null +++ b/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/LegacyComponents/STKHTTPDataSource.h b/LegacyComponents/STKHTTPDataSource.h new file mode 100755 index 0000000000..628a62f6d6 --- /dev/null +++ b/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/LegacyComponents/STKHTTPDataSource.m b/LegacyComponents/STKHTTPDataSource.m new file mode 100755 index 0000000000..8a5f4d3406 --- /dev/null +++ b/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/LegacyComponents/STKLocalFileDataSource.h b/LegacyComponents/STKLocalFileDataSource.h new file mode 100755 index 0000000000..777ee52f66 --- /dev/null +++ b/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/LegacyComponents/STKLocalFileDataSource.m b/LegacyComponents/STKLocalFileDataSource.m new file mode 100755 index 0000000000..2aded28dd9 --- /dev/null +++ b/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/LegacyComponents/STKQueueEntry.h b/LegacyComponents/STKQueueEntry.h new file mode 100755 index 0000000000..78e3784276 --- /dev/null +++ b/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/LegacyComponents/STKQueueEntry.m b/LegacyComponents/STKQueueEntry.m new file mode 100755 index 0000000000..2b6de04a3b --- /dev/null +++ b/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/LegacyComponents/TGActivityIndicatorView.h b/LegacyComponents/TGActivityIndicatorView.h new file mode 100644 index 0000000000..c66f8b2072 --- /dev/null +++ b/LegacyComponents/TGActivityIndicatorView.h @@ -0,0 +1,14 @@ +#import + +typedef enum { + TGActivityIndicatorViewStyleSmall = 0, + TGActivityIndicatorViewStyleLarge = 1, + TGActivityIndicatorViewStyleSmallWhite = 2 +} TGActivityIndicatorViewStyle; + +@interface TGActivityIndicatorView : UIImageView + +- (id)init; +- (id)initWithStyle:(TGActivityIndicatorViewStyle)style; + +@end diff --git a/LegacyComponents/TGActivityIndicatorView.m b/LegacyComponents/TGActivityIndicatorView.m new file mode 100644 index 0000000000..caf564c72b --- /dev/null +++ b/LegacyComponents/TGActivityIndicatorView.m @@ -0,0 +1,90 @@ +#import "TGActivityIndicatorView.h" + +@implementation TGActivityIndicatorView + ++ (NSArray *)animationFrames +{ + static NSArray *array = nil; + if (array == nil) + { + NSMutableArray *mutableArray = [[NSMutableArray alloc] init]; + for (int i = 1; i <= 24; i++) + { + UIImage *image = [UIImage imageNamed:[NSString stringWithFormat:@"grayProgress%d.png", i]]; + if (image != nil) + [mutableArray addObject:image]; + } + array = [[NSArray alloc] initWithArray:mutableArray]; + } + return array; +} + ++ (NSArray *)largeAnimationFrames +{ + static NSArray *array = nil; + if (array == nil) + { + NSMutableArray *mutableArray = [[NSMutableArray alloc] init]; + for (int i = 0; i <= 24; i++) + { + UIImage *image = [UIImage imageNamed:[NSString stringWithFormat:@"navbar_big_progress_%d.png", i]]; + if (image != nil) + [mutableArray addObject:image]; + } + array = [[NSArray alloc] initWithArray:mutableArray]; + } + return array; +} + ++ (NSArray *)smallWhiteAnimationFrames +{ + static NSArray *array = nil; + if (array == nil) + { + NSMutableArray *mutableArray = [[NSMutableArray alloc] init]; + + for (int i = 1; i <= 24; i++) + { + UIImage *image = [UIImage imageNamed:[[NSString alloc] initWithFormat:@"RProgress%d.png", i]]; + if (image != nil) + [mutableArray addObject:image]; + } + array = [[NSArray alloc] initWithArray:mutableArray]; + } + return array; +} + +- (id)initWithStyle:(TGActivityIndicatorViewStyle)style +{ + NSArray *frames = style == TGActivityIndicatorViewStyleSmall ? [TGActivityIndicatorView animationFrames] : (style == TGActivityIndicatorViewStyleLarge ?[TGActivityIndicatorView largeAnimationFrames] : [TGActivityIndicatorView smallWhiteAnimationFrames]); + self = [super initWithImage:[frames objectAtIndex:0]]; + if (self) + { + [self setAnimationImages:frames]; + } + return self; +} + +- (id)init +{ + NSArray *frames = [TGActivityIndicatorView animationFrames]; + self = [super initWithImage:[frames objectAtIndex:0]]; + if (self) + { + [self setAnimationImages:frames]; + } + return self; +} + +- (id)initWithFrame:(CGRect)__unused frame +{ + NSArray *frames = [TGActivityIndicatorView animationFrames]; + self = [super initWithImage:[frames objectAtIndex:0]]; + if (self) + { + [self setAnimationImages:frames]; + } + return self; +} + +@end diff --git a/LegacyComponents/TGAttachmentAssetCell.h b/LegacyComponents/TGAttachmentAssetCell.h new file mode 100644 index 0000000000..516e09e374 --- /dev/null +++ b/LegacyComponents/TGAttachmentAssetCell.h @@ -0,0 +1,29 @@ +#import "TGAttachmentMenuCell.h" +#import +#import + +@class TGMediaAsset; +@class TGMediaSelectionContext; +@class TGMediaEditingContext; + +@interface TGAttachmentAssetCell : TGAttachmentMenuCell +{ + TGCheckButtonView *_checkButton; + + UIImageView *_iconView; + UIImageView *_gradientView; +} + +@property (nonatomic, readonly) TGImageView *imageView; +- (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/LegacyComponents/TGAttachmentAssetCell.m b/LegacyComponents/TGAttachmentAssetCell.m new file mode 100644 index 0000000000..19b09cb75b --- /dev/null +++ b/LegacyComponents/TGAttachmentAssetCell.m @@ -0,0 +1,201 @@ +#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]; + + [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 (_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/LegacyComponents/TGAttachmentCameraCell.h b/LegacyComponents/TGAttachmentCameraCell.h new file mode 100644 index 0000000000..08524a26dc --- /dev/null +++ b/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/LegacyComponents/TGAttachmentCameraCell.m b/LegacyComponents/TGAttachmentCameraCell.m new file mode 100644 index 0000000000..49a6cc94da --- /dev/null +++ b/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/LegacyComponents/TGAttachmentCameraView.h b/LegacyComponents/TGAttachmentCameraView.h new file mode 100644 index 0000000000..2d059ca16a --- /dev/null +++ b/LegacyComponents/TGAttachmentCameraView.h @@ -0,0 +1,25 @@ +#import + +@class TGCameraPreviewView; + +@interface TGAttachmentCameraView : UIView + +@property (nonatomic, copy) void (^pressed)(void); + +- (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/LegacyComponents/TGAttachmentCameraView.m b/LegacyComponents/TGAttachmentCameraView.m new file mode 100644 index 0000000000..705df21317 --- /dev/null +++ b/LegacyComponents/TGAttachmentCameraView.m @@ -0,0 +1,232 @@ +#import "TGAttachmentCameraView.h" + +#import "LegacyComponentsInternal.h" + +#import +#import "TGAttachmentMenuCell.h" + +#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:[UIImage imageNamed:@"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]; + } + return self; +} + +- (void)dealloc +{ + TGCameraPreviewView *previewView = _previewView; + if (previewView.superview == _wrapperView && _camera != nil) + [self stopPreview]; + + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidChangeStatusBarOrientationNotification object:nil]; +} + +- (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/LegacyComponents/TGAttachmentCarouselItemView.h b/LegacyComponents/TGAttachmentCarouselItemView.h new file mode 100644 index 0000000000..3dab09bd3d --- /dev/null +++ b/LegacyComponents/TGAttachmentCarouselItemView.h @@ -0,0 +1,44 @@ +#import +#import + +#import + +@class TGMediaSelectionContext; +@class TGMediaEditingContext; +@class TGSuggestionContext; +@class TGViewController; +@class TGAttachmentCameraView; + +@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 inhibitDocumentCaptions; +@property (nonatomic) bool hasTimer; + +@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, strong) NSString *recipientName; + +- (instancetype)initWithContext:(id)context camera:(bool)hasCamera selfPortrait:(bool)selfPortrait forProfilePhoto:(bool)forProfilePhoto assetType:(TGMediaAssetType)assetType saveEditedPhotos:(bool)saveEditedPhotos; + +@end diff --git a/LegacyComponents/TGAttachmentCarouselItemView.m b/LegacyComponents/TGAttachmentCarouselItemView.m new file mode 100644 index 0000000000..3183685d8f --- /dev/null +++ b/LegacyComponents/TGAttachmentCarouselItemView.m @@ -0,0 +1,1159 @@ +#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; + +@implementation TGAttachmentCarouselCollectionView + +@end + +@interface TGAttachmentCarouselItemView () +{ + TGMediaAssetsLibrary *_assetsLibrary; + SMetaDisposable *_assetsDisposable; + TGMediaAssetFetchResult *_fetchResult; + + 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; +} +@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 +{ + self = [super initWithType:TGMenuSheetItemTypeDefault]; + if (self != nil) + { + _context = context; + _saveEditedPhotos = saveEditedPhotos; + + __weak TGAttachmentCarouselItemView *weakSelf = self; + _forProfilePhoto = forProfilePhoto; + + _assetsLibrary = [TGMediaAssetsLibrary libraryForAssetType:assetType]; + _assetsDisposable = [[SMetaDisposable alloc] init]; + + if (!forProfilePhoto) + { + _selectionContext = [[TGMediaSelectionContext alloc] init]; + [_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]; + }]]; + + _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]; + _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]; + [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) + strongSelf.sendPressed(nil, false); + }]; + [_sendMediaItemView setHidden:true animated:false]; + [self addSubview:_sendMediaItemView]; + + _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)setRemainingHeight:(CGFloat)remainingHeight +{ + _remainingHeight = remainingHeight; + [self setCondensed:_condensed]; +} + +- (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 (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 - 2 * TGMenuSheetButtonItemViewHeight; + return -(correction * progress); +} + +- (CGFloat)preferredHeightForWidth:(CGFloat)__unused width screenHeight:(CGFloat)screenHeight +{ + 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; + [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) + strongSelf.sendPressed(item.asset, false); + }; + + 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) hasTimer:self.hasTimer inhibitDocumentCaptions:_inhibitDocumentCaptions asFile:false 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) + { + TGPhotoEditorController *controller = [[TGPhotoEditorController alloc] initWithContext:_context item:asset intent: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] initWithParentController:_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]; + 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 = 3; + else + limit = 5; + } + + if (!(pivotIndex != NSNotFound && (indexPath.row < pivotIndex - limit || indexPath.row > pivotIndex + limit))) + { + cell.selectionContext = _selectionContext; + cell.editingContext = _editingContext; + + 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]]; + } + } + + 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 +{ + 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/LegacyComponents/TGAttachmentGifCell.h b/LegacyComponents/TGAttachmentGifCell.h new file mode 100644 index 0000000000..856ec84c95 --- /dev/null +++ b/LegacyComponents/TGAttachmentGifCell.h @@ -0,0 +1,9 @@ +#import "TGAttachmentPhotoCell.h" + +@interface TGAttachmentGifCell : TGAttachmentPhotoCell +{ + UILabel *_typeLabel; +} +@end + +extern NSString *const TGAttachmentGifCellIdentifier; diff --git a/LegacyComponents/TGAttachmentGifCell.m b/LegacyComponents/TGAttachmentGifCell.m new file mode 100644 index 0000000000..ce4d9d7dd3 --- /dev/null +++ b/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/LegacyComponents/TGAttachmentMenuCell.h b/LegacyComponents/TGAttachmentMenuCell.h new file mode 100644 index 0000000000..307fe61576 --- /dev/null +++ b/LegacyComponents/TGAttachmentMenuCell.h @@ -0,0 +1,10 @@ +#import + +@interface TGAttachmentMenuCell : UICollectionViewCell +{ + UIImageView *_cornersView; +} + +@end + +extern const CGFloat TGAttachmentMenuCellCornerRadius; diff --git a/LegacyComponents/TGAttachmentMenuCell.m b/LegacyComponents/TGAttachmentMenuCell.m new file mode 100644 index 0000000000..0a2d805084 --- /dev/null +++ b/LegacyComponents/TGAttachmentMenuCell.m @@ -0,0 +1,55 @@ +#import "TGAttachmentMenuCell.h" +#import + +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, [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 = self.bounds; + [self addSubview:_cornersView]; + } + } + return self; +} + +@end diff --git a/LegacyComponents/TGAttachmentPhotoCell.h b/LegacyComponents/TGAttachmentPhotoCell.h new file mode 100644 index 0000000000..fc611985a7 --- /dev/null +++ b/LegacyComponents/TGAttachmentPhotoCell.h @@ -0,0 +1,7 @@ +#import "TGAttachmentAssetCell.h" + +@interface TGAttachmentPhotoCell : TGAttachmentAssetCell + +@end + +extern NSString *const TGAttachmentPhotoCellIdentifier; diff --git a/LegacyComponents/TGAttachmentPhotoCell.m b/LegacyComponents/TGAttachmentPhotoCell.m new file mode 100644 index 0000000000..91e93af96f --- /dev/null +++ b/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/LegacyComponents/TGAttachmentVideoCell.h b/LegacyComponents/TGAttachmentVideoCell.h new file mode 100644 index 0000000000..2b14488f92 --- /dev/null +++ b/LegacyComponents/TGAttachmentVideoCell.h @@ -0,0 +1,9 @@ +#import "TGAttachmentAssetCell.h" + +@interface TGAttachmentVideoCell : TGAttachmentAssetCell +{ + UILabel *_durationLabel; +} +@end + +extern NSString *const TGAttachmentVideoCellIdentifier; diff --git a/LegacyComponents/TGAttachmentVideoCell.m b/LegacyComponents/TGAttachmentVideoCell.m new file mode 100644 index 0000000000..8dafd4dd08 --- /dev/null +++ b/LegacyComponents/TGAttachmentVideoCell.m @@ -0,0 +1,294 @@ +#import "TGAttachmentVideoCell.h" + +#import + +#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]; + } + 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 = [UIImage imageNamed:@"ModernMediaItemTimelapseIcon"]; + else if (asset.subtypes & TGMediaAssetSubtypeVideoHighFrameRate) + _iconView.image = [UIImage imageNamed:@"ModernMediaItemSloMoIcon"]; + else + _iconView.image = [UIImage imageNamed:@"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/LegacyComponents/TGBuiltinWallpaperInfo.h b/LegacyComponents/TGBuiltinWallpaperInfo.h new file mode 100644 index 0000000000..256c0cf625 --- /dev/null +++ b/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/LegacyComponents/TGBuiltinWallpaperInfo.m b/LegacyComponents/TGBuiltinWallpaperInfo.m new file mode 100644 index 0000000000..8914297b8e --- /dev/null +++ b/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/LegacyComponents/TGCache.h b/LegacyComponents/TGCache.h new file mode 100644 index 0000000000..df51b84d7f --- /dev/null +++ b/LegacyComponents/TGCache.h @@ -0,0 +1,55 @@ +#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; + +- (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/LegacyComponents/TGCache.m b/LegacyComponents/TGCache.m new file mode 100644 index 0000000000..3f119f3883 --- /dev/null +++ b/LegacyComponents/TGCache.m @@ -0,0 +1,818 @@ +#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 +{ + 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; + + NSString *cachesPath = [[LegacyComponentsGlobals provider] dataCachePath]; + + _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]; + //TGLog(@"evict %@", key); + } + + __block int currentCacheSize = 0; + [_memoryCache enumerateKeysAndObjectsUsingBlock:^(__unused NSString *key, TGCacheRecord *record, __unused BOOL *stop) + { + currentCacheSize += record.size; + }]; + + self.memoryCacheSize = currentCacheSize; + + //TGLog(@"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]; + } + //TGLog(@"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) + TGLog(@"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]; + //TGLog(@"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; + } + + //TGLog(@"Cache thumbnail: %@", url); + + //TGLog(@"_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 + { + //TGLog(@"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 +{ + //TGLog(@"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) + TGLog(@"Failed to move: %@", error); + }); +} + +- (void)moveToCache:(NSString *)fileUrl cacheUrl:(NSString *)cacheUrl +{ + TGLog(@"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; + [cacheFileManager moveItemAtPath:fileUrl toPath:[_diskCachePath stringByAppendingPathComponent:md5String(cacheUrl)] error:&error]; + if (error != nil) + TGLog(@"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++; + } + + TGLog(@"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]; + } + TGLog(@"Cache restored in %f ms", (CFAbsoluteTimeGetCurrent() - startTime) * 1000.0); +} + +@end diff --git a/LegacyComponents/TGCameraController.h b/LegacyComponents/TGCameraController.h new file mode 100644 index 0000000000..5d156ed583 --- /dev/null +++ b/LegacyComponents/TGCameraController.h @@ -0,0 +1,50 @@ +#import +#import +#import + +@class PGCamera; +@class TGCameraPreviewView; +@class TGSuggestionContext; +@class TGVideoEditAdjustments; + +typedef enum { + TGCameraControllerGenericIntent, + TGCameraControllerAvatarIntent, +} 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 inhibitDocumentCaptions; +@property (nonatomic, assign) bool hasTimer; +@property (nonatomic, strong) TGSuggestionContext *suggestionContext; +@property (nonatomic, assign) bool shortcut; + +@property (nonatomic, strong) NSString *recipientName; + +@property (nonatomic, copy) void(^finishedWithPhoto)(TGOverlayController *controller, UIImage *resultImage, NSString *caption, NSArray *stickers, NSNumber *timer); +@property (nonatomic, copy) void(^finishedWithVideo)(TGOverlayController *controller, NSURL *videoURL, UIImage *previewImage, NSTimeInterval duration, CGSize dimensions, TGVideoEditAdjustments *adjustments, NSString *caption, NSArray *stickers, NSNumber *timer); + +@property (nonatomic, copy) CGRect(^beginTransitionOut)(void); +@property (nonatomic, copy) void(^finishedTransitionOut)(void); + +- (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; + +- (void)beginTransitionInFromRect:(CGRect)rect; + +- (void)_dismissTransitionForResultController:(TGOverlayController *)resultController; + ++ (UIInterfaceOrientation)_interfaceOrientationForDeviceOrientation:(UIDeviceOrientation)orientation; + ++ (bool)useLegacyCamera; + +@end diff --git a/LegacyComponents/TGCameraController.m b/LegacyComponents/TGCameraController.m new file mode 100644 index 0000000000..24c6c66633 --- /dev/null +++ b/LegacyComponents/TGCameraController.m @@ -0,0 +1,1946 @@ +#import "TGCameraController.h" + +#import "LegacyComponentsInternal.h" + +#import + +#import + +#import +#import +#import + +#import +#import +#import +#import +#import + +#import +#import +#import +#import "TGCameraFocusCrosshairsControl.h" + +#import +#import +#import + +#import +#import +#import +#import +#import +#import + +#import +#import +#import +#import +#import +#import + +#import + +#import + +#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; + PGCameraMomentSession *_momentSession; + + 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; + + TGMediaEditingContext *_editingContext; + + NSTimer *_switchToVideoTimer; + NSTimer *_startRecordingTimer; + bool _recordingByShutterHold; + bool _stopRecordingOnRelease; + bool _shownMicrophoneAlert; + + id _context; + bool _saveEditedPhotos; + bool _saveCapturedMedia; +} +@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 init]; + if (self != nil) + { + _context = context; + if (previewView == nil) + _standalone = true; + _intent = intent; + _camera = camera; + _previewView = previewView; + + if (_intent == TGCameraControllerAvatarIntent) + _allowCaptions = false; + _saveEditedPhotos = saveEditedPhotos; + _saveCapturedMedia = saveCapturedMedia; + } + 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); + } + + _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) + { + __strong TGCameraController *strongSelf = weakSelf; + if (strongSelf == nil) + return true; + + if (strongSelf->_momentSession != nil && strongSelf->_momentSession.hasSegments) + { + + return false; + } + + return true; + }; + _interfaceView.cameraModeChanged = ^(PGCameraMode mode) + { + __strong TGCameraController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [strongSelf->_camera setCameraMode:mode]; + if (mode == PGCameraModeClip) + strongSelf->_momentSession = [[PGCameraMomentSession alloc] initWithCamera:strongSelf->_camera]; + else + strongSelf->_momentSession = nil; + }; + + _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) + return; + + void (^cancelBlock)(void) = ^ + { + [strongSelf beginTransitionOutWithVelocity:0.0f]; + }; + + if (strongSelf->_momentSession != nil && strongSelf->_momentSession.hasSegments) + { + [strongSelf->_interfaceView showMomentCaptureDismissWarningWithCompletion:^(bool dismiss) + { + if (dismiss) + cancelBlock(); + }]; + } + else + { + cancelBlock(); + } + }; + + _interfaceView.deleteSegmentButtonPressed = ^ + { + __strong TGCameraController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + if (!strongSelf->_momentSession.hasSegments) + return; + + if (!strongSelf->_previewingSegment) + { + [strongSelf previewLastSegment]; + } + else + { + strongSelf->_previewingSegment = false; + [strongSelf->_momentSession removeLastSegment]; + } + }; + + if (_intent == TGCameraControllerAvatarIntent) + [_interfaceView setHasModeControl:false]; + + [_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 (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 = ^(bool moment) + { + __strong TGCameraController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + strongSelf->_focusControl.ignoreAutofocusing = true; + + if (!moment) + [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 = ^(bool moment) + { + __strong TGCameraController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + strongSelf->_focusControl.ignoreAutofocusing = false; + + if (!moment) + [strongSelf->_interfaceView setFlashMode:PGCameraFlashModeOff]; + }; + + _camera.deviceAngleSampler.deviceOrientationChanged = ^(UIDeviceOrientation orientation) + { + __strong TGCameraController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [strongSelf handleDeviceOrientationChangedTo:orientation]; + }; +} + +#pragma mark - View Life Cycle + +- (void)viewWillAppear:(BOOL)animated +{ + [super viewWillAppear:animated]; + + [UIView animateWithDuration:0.3f animations:^ + { + [TGHacks 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:^ + { + [TGHacks 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)previewLastSegment +{ + PGCameraMomentSegment *segment = _momentSession.lastSegment; + + AVPlayer *player = [AVPlayer playerWithURL:segment.fileURL]; + + _segmentPreviewView = [[TGModernGalleryVideoView alloc] initWithFrame:_previewView.frame player:player]; + [_previewView.superview addSubview:_segmentPreviewView]; +} + +#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) + [strongSelf presentVideoResultControllerWithURL:outputURL dimensions:dimensions duration:duration completion:nil]; + 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) + [strongSelf presentVideoResultControllerWithURL:outputURL dimensions:dimensions duration:duration completion:nil]; + else + [strongSelf->_interfaceView setRecordingVideo:false animated:false]; + }]; + + _stopRecordingOnRelease = true; + } +} + +- (void)shutterPressed +{ + PGCameraMode cameraMode = _camera.cameraMode; + switch (cameraMode) + { + case PGCameraModePhoto: + { + if (_intent != TGCameraControllerAvatarIntent) + { + _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: + { + if (_momentSession == nil) + return; + + [_momentSession captureSegment]; + } + break; + + default: + break; + } +} + +- (void)shutterReleased +{ + [_switchToVideoTimer invalidate]; + _switchToVideoTimer = nil; + + [_startRecordingTimer invalidate]; + _startRecordingTimer = nil; + + PGCameraMode cameraMode = _camera.cameraMode; + if (cameraMode == PGCameraModePhoto || cameraMode == PGCameraModeSquare) + { + self.view.userInteractionEnabled = false; + + _buttonHandler.enabled = false; + [_buttonHandler ignoreEventsFor:1.5f andDisable:true]; + + _camera.disabled = true; + + [_camera takePhotoWithCompletion:^(UIImage *result, PGCameraShotMetadata *metadata) + { + TGDispatchOnMainThread(^ + { + [self presentPhotoResultControllerWithImage:result metadata:metadata completion:^ + { + self.view.userInteractionEnabled = true; + }]; + }); + }]; + } + else if (cameraMode == PGCameraModeVideo) + { + if (!_camera.isRecordingVideo) + { + [_buttonHandler ignoreEventsFor:1.0f andDisable:false]; + + __weak TGCameraController *weakSelf = self; + [_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) + [strongSelf presentVideoResultControllerWithURL:outputURL dimensions:dimensions duration:duration completion:nil]; + else + [strongSelf->_interfaceView setRecordingVideo:false animated:false]; + }]; + } + else if (_stopRecordingOnRelease) + { + _stopRecordingOnRelease = false; + + _camera.disabled = true; + + [_buttonHandler ignoreEventsFor:1.0f andDisable:true]; + [_camera stopVideoRecording]; + } + } + else if (cameraMode == PGCameraModeClip) + { + [_momentSession commitSegment]; + } +} + +#pragma mark - 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; + } + + __weak TGCameraController *weakSelf = self; + TGOverlayController *overlayController = nil; + + _focusControl.ignoreAutofocusing = true; + + switch (_intent) + { + case TGCameraControllerAvatarIntent: + { + TGPhotoEditorController *controller = [[TGPhotoEditorController alloc] initWithContext:_context item:image intent:(TGPhotoEditorControllerFromCameraIntent | TGPhotoEditorControllerAvatarIntent) 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); + + 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:_context image:image metadata:metadata recipientName:self.recipientName backButtonTitle:TGLocalized(@"Camera.Retake") doneButtonTitle:TGLocalized(@"Common.Next") saveCapturedMedia:_saveCapturedMedia saveEditedPhotos:_saveEditedPhotos] : [[TGCameraPhotoPreviewController alloc] initWithContext:_context 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 *stickers, NSNumber *timer) + { + __strong TGCameraController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + if (strongSelf.finishedWithPhoto != nil) + strongSelf.finishedWithPhoto(controller, resultImage, caption, stickers, timer); + + if (strongSelf->_shortcut) + return; + + __strong TGOverlayController *strongController = weakController; + if (strongController != nil) + [strongSelf _dismissTransitionForResultController:strongController]; + }; + + overlayController = controller; + } + break; + } + + if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPhone) + { + 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] initWithParentController: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 - Video Result + +- (void)presentVideoResultControllerWithURL:(NSURL *)url dimensions:(CGSize)dimensions duration:(NSTimeInterval)duration completion:(void (^)(void))completion +{ + TGMediaEditingContext *editingContext = [[TGMediaEditingContext alloc] init]; + _editingContext = editingContext; + + [[[LegacyComponentsGlobals provider] applicationInstance] setIdleTimerDisabled:false]; + + AVURLAsset *asset = [AVURLAsset assetWithURL:url]; + AVAssetImageGenerator *generator = [[AVAssetImageGenerator alloc] initWithAsset:asset]; + 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); + + __weak TGCameraController *weakSelf = self; + + TGMediaPickerGalleryVideoItem *videoItem = [[TGMediaPickerGalleryVideoItem alloc] initWithFileURL:url dimensions:dimensions duration:duration]; + videoItem.editingContext = _editingContext; + videoItem.immediateThumbnailImage = thumbnailImage; + + TGModernGalleryController *galleryController = [[TGModernGalleryController alloc] initWithContext:_context]; + galleryController.adjustsStatusBarVisibility = false; + galleryController.hasFadeOutTransition = true; + + TGMediaPickerGalleryModel *model = [[TGMediaPickerGalleryModel alloc] initWithContext:_context items:@[ videoItem ] focusItem:videoItem selectionContext:nil editingContext:_editingContext hasCaptions:self.allowCaptions hasTimer:self.hasTimer inhibitDocumentCaptions:self.inhibitDocumentCaptions hasSelectionPanel:false recipientName:self.recipientName]; + 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 = ^(__unused id item, NSString *caption) + { + __strong TGCameraController *strongSelf = weakSelf; + if (strongSelf != nil) + [strongSelf->_editingContext setCaption:caption forItem:videoItem.avAsset]; + }; + + model.interfaceView.hasSwipeGesture = false; + galleryController.model = model; + + __weak TGModernGalleryController *weakGalleryController = galleryController; + __weak TGMediaPickerGalleryModel *weakModel = model; + + model.interfaceView.donePressed = ^(__unused 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; + + TGMediaPickerGalleryVideoItemView *itemView = (TGMediaPickerGalleryVideoItemView *)[strongController itemViewForItem:strongController.currentItem]; + [itemView stop]; + [itemView setPlayButtonHidden:true animated:true]; + + TGVideoEditAdjustments *adjustments = (TGVideoEditAdjustments *)[strongSelf->_editingContext adjustmentsForItem:videoItem.avAsset]; + NSString *caption = [strongSelf->_editingContext captionForItem:videoItem.avAsset]; + NSNumber *timer = [strongSelf->_editingContext timerForItem:videoItem.avAsset]; + + SSignal *thumbnailSignal = [SSignal single:thumbnailImage]; + if (adjustments.trimStartValue > FLT_EPSILON) + { + thumbnailSignal = [TGMediaAssetImageSignals videoThumbnailForAVAsset:[AVURLAsset URLAssetWithURL:url options:nil] size:dimensions timestamp:CMTimeMakeWithSeconds(adjustments.trimStartValue, NSEC_PER_SEC)]; + } + + if ([adjustments cropAppliedForAvatar:false] || adjustments.hasPainting) + { + thumbnailSignal = [thumbnailSignal map:^UIImage *(UIImage *image) + { + 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, CGSizeMake(256, 256), image.size, true); + }]; + } + + [[thumbnailSignal deliverOn:[SQueue mainQueue]] startWithNext:^(UIImage *thumbnailImage) + { + if (strongSelf.finishedWithVideo != nil) + strongSelf.finishedWithVideo(strongController, url, thumbnailImage, duration, dimensions, adjustments, caption, adjustments.paintingData.stickers, timer); + }]; + + if (strongSelf->_shortcut) + return; + + [strongSelf _dismissTransitionForResultController:strongController]; + + if (strongSelf.shouldStoreCapturedAssets && timer == nil) + [strongSelf _saveVideoToCameraRollWithURL:url completion:nil]; + }; + + 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; + + [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) + { + [[[LegacyComponentsGlobals provider] applicationInstance] setIdleTimerDisabled:true]; + + [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]; + + if ([[NSFileManager defaultManager] fileExistsAtPath:url.path isDirectory:NULL]) + [[NSFileManager defaultManager] removeItemAtURL:url error:NULL]; + + [UIView animateWithDuration:0.3f delay:0.1f options:UIViewAnimationOptionCurveLinear animations:^ + { + strongSelf->_interfaceView.alpha = 1.0f; + } completion:nil]; + + return snapshotView; + } + return nil; + }; + + galleryController.completedTransitionOut = ^ + { + [snapshotView removeFromSuperview]; + + 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]; + + } + }; + + TGOverlayController *contentController = galleryController; + if (_shortcut) + { + contentController = [[TGOverlayController alloc] init]; + + TGNavigationController *navigationController = [TGNavigationController navigationControllerWithControllers:@[galleryController]]; + galleryController.navigationBarShouldBeHidden = true; + + [contentController addChildViewController:navigationController]; + [contentController.view addSubview:navigationController.view]; + } + + TGOverlayControllerWindow *controllerWindow = [[TGOverlayControllerWindow alloc] initWithParentController:self contentController:contentController]; + controllerWindow.hidden = false; + controllerWindow.windowLevel = self.view.window.windowLevel + 0.0001f; + galleryController.view.clipsToBounds = true; +} + +#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:^ + { + [TGHacks 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; + + [TGHacks setApplicationStatusBarAlpha:1.0f]; + + self.view.hidden = true; + + [UIView animateWithDuration:0.3f delay:0.0f options:(7 << 16) animations:^ + { + resultController.view.frame = CGRectOffset(resultController.view.frame, 0, resultController.view.frame.size.height); + } completion:^(__unused BOOL finished) + { + [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) + 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) + { + if (_camera.cameraMode == PGCameraModePhoto) + newMode = PGCameraModeSquare; + else if (_camera.cameraMode != PGCameraModeSquare) + newMode = PGCameraModePhoto; + } + else if (gestureRecognizer == _videoSwipeGestureRecognizer) + { + if (_camera.cameraMode == PGCameraModeSquare) + newMode = PGCameraModePhoto; + else + newMode = PGCameraModeVideo; + } +// if (gestureRecognizer == _photoSwipeGestureRecognizer) +// { +// if (_camera.cameraMode == PGCameraModePhoto) +// newMode = PGCameraModeClip; +// else if (_camera.cameraMode == PGCameraModeVideo) +// newMode = PGCameraModePhoto; +// } +// else if (gestureRecognizer == _videoSwipeGestureRecognizer) +// { +// if (_camera.cameraMode == PGCameraModeClip) +// newMode = PGCameraModePhoto; +// else if (_camera.cameraMode == PGCameraModePhoto) +// 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; + else if (gestureRecognizer == _photoSwipeGestureRecognizer || gestureRecognizer == _videoSwipeGestureRecognizer) + return _intent != TGCameraControllerAvatarIntent && !_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: + { + 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 >= 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; +} + +@end diff --git a/LegacyComponents/TGCameraFlashActiveView.h b/LegacyComponents/TGCameraFlashActiveView.h new file mode 100644 index 0000000000..516ab21996 --- /dev/null +++ b/LegacyComponents/TGCameraFlashActiveView.h @@ -0,0 +1,7 @@ +#import + +@interface TGCameraFlashActiveView : UIView + +- (void)setActive:(bool)active animated:(bool)animated; + +@end diff --git a/LegacyComponents/TGCameraFlashActiveView.m b/LegacyComponents/TGCameraFlashActiveView.m new file mode 100644 index 0000000000..4b3bb2035e --- /dev/null +++ b/LegacyComponents/TGCameraFlashActiveView.m @@ -0,0 +1,64 @@ +#import "TGCameraFlashActiveView.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 = [UIImage imageNamed:@"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/LegacyComponents/TGCameraFlashControl.h b/LegacyComponents/TGCameraFlashControl.h new file mode 100644 index 0000000000..b914fd8eef --- /dev/null +++ b/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/LegacyComponents/TGCameraFlashControl.m b/LegacyComponents/TGCameraFlashControl.m new file mode 100644 index 0000000000..6c960046a1 --- /dev/null +++ b/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:[UIImage imageNamed:@"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 = [UIImage imageNamed:@"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/LegacyComponents/TGCameraFlipButton.h b/LegacyComponents/TGCameraFlipButton.h new file mode 100644 index 0000000000..4bf127510f --- /dev/null +++ b/LegacyComponents/TGCameraFlipButton.h @@ -0,0 +1,7 @@ +#import + +@interface TGCameraFlipButton : TGModernButton + +- (void)setHidden:(bool)hidden animated:(bool)animated; + +@end diff --git a/LegacyComponents/TGCameraFlipButton.m b/LegacyComponents/TGCameraFlipButton.m new file mode 100644 index 0000000000..a50975840b --- /dev/null +++ b/LegacyComponents/TGCameraFlipButton.m @@ -0,0 +1,48 @@ +#import "TGCameraFlipButton.h" + +@implementation TGCameraFlipButton + +- (instancetype)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + if (self != nil) + { + self.exclusiveTouch = true; + [self setImage:[UIImage imageNamed:@"CameraLargeFlipButton"] 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/LegacyComponents/TGCameraFocusCrosshairsControl.h b/LegacyComponents/TGCameraFocusCrosshairsControl.h new file mode 100644 index 0000000000..1abc2ad409 --- /dev/null +++ b/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/LegacyComponents/TGCameraFocusCrosshairsControl.m b/LegacyComponents/TGCameraFocusCrosshairsControl.m new file mode 100644 index 0000000000..27f52528c4 --- /dev/null +++ b/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 = [UIImage imageNamed:@"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 = [UIImage imageNamed:@"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 = [UIImage imageNamed:@"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/LegacyComponents/TGCameraInterfaceAssets.h b/LegacyComponents/TGCameraInterfaceAssets.h new file mode 100644 index 0000000000..dce7efc7ab --- /dev/null +++ b/LegacyComponents/TGCameraInterfaceAssets.h @@ -0,0 +1,15 @@ +#import +#import + +@interface TGCameraInterfaceAssets : NSObject + ++ (UIColor *)normalColor; ++ (UIColor *)accentColor; ++ (UIColor *)redColor; + ++ (UIColor *)panelBackgroundColor; ++ (UIColor *)transparentPanelBackgroundColor; + ++ (UIFont *)normalFontOfSize:(CGFloat)size; + +@end diff --git a/LegacyComponents/TGCameraInterfaceAssets.m b/LegacyComponents/TGCameraInterfaceAssets.m new file mode 100644 index 0000000000..dfd745eaf2 --- /dev/null +++ b/LegacyComponents/TGCameraInterfaceAssets.m @@ -0,0 +1,37 @@ +#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]; +} + ++ (UIFont *)normalFontOfSize:(CGFloat)size +{ + return [UIFont fontWithName:@"DINAlternate-Bold" size:size]; +} + +@end diff --git a/LegacyComponents/TGCameraMainPhoneView.h b/LegacyComponents/TGCameraMainPhoneView.h new file mode 100644 index 0000000000..fd0f27557b --- /dev/null +++ b/LegacyComponents/TGCameraMainPhoneView.h @@ -0,0 +1,7 @@ +#import + +#import + +@interface TGCameraMainPhoneView : TGCameraMainView + +@end diff --git a/LegacyComponents/TGCameraMainPhoneView.m b/LegacyComponents/TGCameraMainPhoneView.m new file mode 100644 index 0000000000..109f5b6a55 --- /dev/null +++ b/LegacyComponents/TGCameraMainPhoneView.m @@ -0,0 +1,713 @@ +#import "TGCameraMainPhoneView.h" + +#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" + +@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 *_bottomPanelView; + UIView *_bottomPanelBackgroundView; + + UIView *_videoLandscapePanelView;; + + TGCameraFlashControl *_flashControl; + TGCameraFlashActiveView *_flashActiveView; + + CGFloat _topPanelHeight; + CGFloat _bottomPanelHeight; + CGFloat _modeControlHeight; + + 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]; + + CGSize screenSize = TGScreenSize(); + CGFloat widescreenWidth = MAX(screenSize.width, screenSize.height); + if (widescreenWidth >= 736.0f - FLT_EPSILON) + { + _topPanelHeight = 44.0f; + _bottomPanelHeight = 140.0f; + _modeControlHeight = 50.0f; + } + else if (widescreenWidth >= 667.0f - FLT_EPSILON) + { + _topPanelHeight = 44.0f; + _bottomPanelHeight = 123.0f; + _modeControlHeight = 42.0f; + } + else + { + _topPanelHeight = 40.0f; + _bottomPanelHeight = 101.0f; + _modeControlHeight = 31.0f; + } + + __weak TGCameraMainPhoneView *weakSelf = self; + + _topPanelView = [[TGCameraTopPanelView alloc] init]; + _topPanelView.backgroundColor = [TGCameraInterfaceAssets transparentPanelBackgroundColor]; + _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]; + + _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.alpha = 0.0f; + _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 - 66) / 2, 10, 66, 66)]; + [_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)]; + [_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]; + + _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 - 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]; + }; + + if (change) + { + changeBlock(); + } + else + { + [strongSelf showMomentCaptureDismissWarningWithCompletion:^(bool dismiss) + { + if (dismiss) + changeBlock(); + }]; + } + }; + + _segmentsView = [[TGCameraSegmentsView alloc] init]; + _segmentsView.hidden = true; + _segmentsView.deletePressed = ^ + { + __strong TGCameraMainPhoneView *strongSelf = weakSelf; + if (strongSelf != nil && strongSelf.deleteSegmentButtonPressed != nil) + strongSelf.deleteSegmentButtonPressed(); + }; + [self addSubview:_segmentsView]; + } + return self; +} + +- (void)dealloc +{ + [_actionHandle reset]; +} + +- (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:_segmentsView] || [view isDescendantOfView:_tooltipContainerView]) + 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 - Layout + +- (void)setInterfaceHiddenForVideoRecording:(bool)hidden animated:(bool)animated +{ + if (animated) + { + if (!hidden) + { + _modeControl.hidden = false; + _cancelButton.hidden = false; + _flashControl.hidden = false; + _flipButton.hidden = false; + _bottomPanelBackgroundView.hidden = false; + } + + [UIView animateWithDuration:0.25 + animations:^ + { + CGFloat alpha = hidden ? 0.0f : 1.0f; + _modeControl.alpha = alpha; + _cancelButton.alpha = alpha; + _flashControl.alpha = alpha; + _flipButton.alpha = alpha; + _bottomPanelBackgroundView.alpha = alpha; + } completion:^(BOOL finished) + { + if (finished) + { + _modeControl.hidden = hidden; + _cancelButton.hidden = hidden; + _flashControl.hidden = hidden; + _flipButton.hidden = hidden; + _bottomPanelBackgroundView.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; + _flipButton.alpha = alpha; + _bottomPanelBackgroundView.hidden = hidden; + _bottomPanelBackgroundView.alpha = alpha; + } +} + +- (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; + } + + _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]; + + _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 - 40) / 2, 21, 40); + } + break; + + case UIInterfaceOrientationLandscapeRight: + { + _flashActiveView.frame = CGRectMake(16, _topPanelHeight + (self.frame.size.height - _topPanelHeight - _bottomPanelHeight - 40) / 2, 21, 40); + } + break; + + default: + { + _flashActiveView.frame = CGRectMake((self.frame.size.width - 40) / 2, self.frame.size.height - _bottomPanelHeight - 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 +{ + CGRect segmentsContainerFrame = CGRectMake(0, CGRectGetMaxY(self.previewViewFrame), self.frame.size.width, self.frame.size.height - CGRectGetMaxY(self.previewViewFrame) - _bottomPanelView.frame.size.height); + + _segmentsView.frame = CGRectMake(0, segmentsContainerFrame.origin.y + (segmentsContainerFrame.size.height - 32) / 2 + 10, _bottomPanelView.frame.size.width, 32); +} + +- (void)layoutSubviews +{ + _topPanelView.frame = CGRectMake(0, 0, self.frame.size.width, _topPanelHeight); + [self _layoutTopPanelSubviewsForInterfaceOrientation:_interfaceOrientation]; + + _bottomPanelView.frame = CGRectMake(0, self.frame.size.height - _bottomPanelHeight, self.frame.size.width, _bottomPanelHeight); + _modeControl.frame = CGRectMake(0, 0, self.frame.size.width, _modeControlHeight); + _shutterButton.frame = CGRectMake((self.frame.size.width - 66) / 2, _modeControlHeight, _shutterButton.frame.size.width, _shutterButton.frame.size.height); + _cancelButton.frame = CGRectMake(0, _shutterButton.frame.origin.y + 11, _cancelButton.frame.size.width, _cancelButton.frame.size.height); + _doneButton.frame = CGRectMake(_bottomPanelView.frame.size.width - _doneButton.frame.size.width, _shutterButton.frame.origin.y + 11, _doneButton.frame.size.width, _doneButton.frame.size.height); + + _flipButton.frame = CGRectMake(self.frame.size.width - _flipButton.frame.size.width - 4.0f, 47.0f, _flipButton.frame.size.width, _flipButton.frame.size.height); + + if (!_displayedTooltip) + { + _displayedTooltip = true; + [self setupTooltip]; + } +} + +@end diff --git a/LegacyComponents/TGCameraMainTabletView.h b/LegacyComponents/TGCameraMainTabletView.h new file mode 100644 index 0000000000..ba2339132f --- /dev/null +++ b/LegacyComponents/TGCameraMainTabletView.h @@ -0,0 +1,7 @@ +#import + +#import + +@interface TGCameraMainTabletView : TGCameraMainView + +@end diff --git a/LegacyComponents/TGCameraMainTabletView.m b/LegacyComponents/TGCameraMainTabletView.m new file mode 100644 index 0000000000..9c1e1f784b --- /dev/null +++ b/LegacyComponents/TGCameraMainTabletView.m @@ -0,0 +1,190 @@ +#import "TGCameraMainTabletView.h" + +#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" + +const CGFloat TGCameraTabletPanelViewWidth = 102.0f; + +@interface TGCameraMainTabletView () +{ + UIView *_panelView; + UIView *_panelBackgroundView; +} +@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]; + + _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)]; + [_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]; + } + }; + } + return self; +} + +- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event +{ + UIView *view = [super hitTest:point withEvent:event]; + + if ([view isDescendantOfView:_panelView]) + return view; + + return nil; +} + +- (void)setInterfaceHiddenForVideoRecording:(bool)hidden animated:(bool)animated +{ + if (animated) + { + _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; + } completion:^(BOOL finished) + { + if (finished) + { + _modeControl.hidden = hidden; + _cancelButton.hidden = hidden; + _flipButton.hidden = hidden; + _panelBackgroundView.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; + } +} + +- (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); + _flipButton.frame = CGRectMake((_panelView.frame.size.width - _flipButton.frame.size.width) / 2, 0, + _flipButton.frame.size.width, _flipButton.frame.size.height); + + _cancelButton.frame = CGRectMake((_panelView.frame.size.width - _cancelButton.frame.size.width) / 2, _panelView.frame.size.height - _cancelButton.frame.size.height - 7, _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); +} + +@end diff --git a/LegacyComponents/TGCameraMainView.h b/LegacyComponents/TGCameraMainView.h new file mode 100644 index 0000000000..6ae38b358e --- /dev/null +++ b/LegacyComponents/TGCameraMainView.h @@ -0,0 +1,91 @@ +#import +#import +#import + +@class TGModernButton; +@class TGCameraShutterButton; +@class TGCameraModeControl; +@class TGCameraFlipButton; +@class TGCameraTimeCodeView; +@class TGCameraZoomView; +@class TGCameraSegmentsView; + +@interface TGCameraMainView : UIView +{ + UIInterfaceOrientation _interfaceOrientation; + + TGModernButton *_cancelButton; + TGModernButton *_doneButton; + TGCameraShutterButton *_shutterButton; + TGCameraModeControl *_modeControl; + + TGCameraFlipButton *_flipButton; + TGCameraTimeCodeView *_timecodeView; + + TGCameraSegmentsView *_segmentsView; + + TGCameraZoomView *_zoomView; +} + +@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 (^deleteSegmentButtonPressed)(void); + +@property (nonatomic, copy) NSTimeInterval(^requestedVideoRecordingDuration)(void); + +@property (nonatomic, assign) CGRect previewViewFrame; + +- (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; + +- (void)setStartedSegmentCapture; +- (void)setCurrentSegmentLength:(CGFloat)length; +- (void)setCommitSegmentCapture; +- (void)previewLastSegment; +- (void)removeLastSegment; + +- (void)showMomentCaptureDismissWarningWithCompletion:(void (^)(bool dismiss))completion; + +- (UIInterfaceOrientation)interfaceOrientation; +- (void)setInterfaceOrientation:(UIInterfaceOrientation)orientation animated:(bool)animated; + +- (void)layoutPreviewRelativeViews; + +@end diff --git a/LegacyComponents/TGCameraMainView.m b/LegacyComponents/TGCameraMainView.m new file mode 100644 index 0000000000..f4f2ff38ae --- /dev/null +++ b/LegacyComponents/TGCameraMainView.m @@ -0,0 +1,248 @@ +#import "TGCameraMainView.h" + +#import "LegacyComponentsInternal.h" + +#import + +#import "TGCameraShutterButton.h" +#import "TGCameraModeControl.h" +#import "TGCameraTimeCodeView.h" +#import "TGCameraZoomView.h" +#import "TGCameraSegmentsView.h" + +@implementation TGCameraMainView + +#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]; + [_segmentsView setHidden:true animated:true delay:0.0]; + } + break; + + case PGCameraModeVideo: + { + [_shutterButton setButtonMode:TGCameraShutterButtonVideoMode animated:true]; + [_timecodeView setHidden:false animated:true]; + [_segmentsView setHidden:true animated:true delay:0.0]; + } + break; + + case PGCameraModeClip: + { + [_shutterButton setButtonMode:TGCameraShutterButtonVideoMode animated:true]; + [_timecodeView setHidden:true animated:true]; + + } + break; + + default: + break; + } + + [_zoomView hideAnimated:true]; +} + +- (void)updateForCameraModeChangeAfterResize +{ + if (_modeControl.cameraMode == PGCameraModeClip) + [_segmentsView setHidden:false animated:true delay:0.1]; +} + +- (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 +{ + if (self.donePressed != nil) + self.donePressed(); +} + +- (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)setStartedSegmentCapture +{ + [_segmentsView startCurrentSegment]; +} + +- (void)setCurrentSegmentLength:(CGFloat)length +{ + [_segmentsView setCurrentSegment:length]; +} + +- (void)setCommitSegmentCapture +{ + [_segmentsView commitCurrentSegmentWithCompletion:nil]; +} + +- (void)previewLastSegment +{ + [_segmentsView highlightLastSegment]; +} + +- (void)removeLastSegment +{ + [_segmentsView removeLastSegment]; +} + +#pragma mark - + +- (void)showMomentCaptureDismissWarningWithCompletion:(void (^)(bool dismiss))completion +{ + if (completion != nil) + completion(true); +} + +- (void)layoutPreviewRelativeViews +{ + +} + +@end diff --git a/LegacyComponents/TGCameraModeControl.h b/LegacyComponents/TGCameraModeControl.h new file mode 100644 index 0000000000..3aa666a8b0 --- /dev/null +++ b/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/LegacyComponents/TGCameraModeControl.m b/LegacyComponents/TGCameraModeControl.m new file mode 100644 index 0000000000..ceaea53483 --- /dev/null +++ b/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/LegacyComponents/TGCameraPhotoPreviewController.h b/LegacyComponents/TGCameraPhotoPreviewController.h new file mode 100644 index 0000000000..2b90f54f53 --- /dev/null +++ b/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 *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/LegacyComponents/TGCameraPhotoPreviewController.m b/LegacyComponents/TGCameraPhotoPreviewController.m new file mode 100644 index 0000000000..75e121111d --- /dev/null +++ b/LegacyComponents/TGCameraPhotoPreviewController.m @@ -0,0 +1,1168 @@ +#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 "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 init]; + 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]); + + 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->_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 *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, 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) + { + __strong TGCameraPhotoPreviewController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [strongSelf->_editingContext setCaption:caption 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:[UIImage imageNamed:@"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]; + } +} + +- (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 +{ + [TGHacks 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) + { + __strong TGCameraPhotoPreviewController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [strongSelf reset]; + + [strongSelf->_editingContext setCaption:caption 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); + + 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; + } + + CGSize referenceSize = [self referenceViewSizeForOrientation:originalOrientation]; + + [_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, referenceSize.width, TGPhotoEditorToolbarSize); + + 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, referenceSize.height); + }]; + } + break; + + case UIInterfaceOrientationLandscapeRight: + { + [UIView performWithoutAnimation:^ + { + _landscapeToolbarView.frame = CGRectMake(screenEdges.right - TGPhotoEditorToolbarSize, screenEdges.top, TGPhotoEditorToolbarSize, 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/LegacyComponents/TGCameraSegmentsView.h b/LegacyComponents/TGCameraSegmentsView.h new file mode 100644 index 0000000000..b2f0c2ba73 --- /dev/null +++ b/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/LegacyComponents/TGCameraSegmentsView.m b/LegacyComponents/TGCameraSegmentsView.m new file mode 100644 index 0000000000..abfe5ad623 --- /dev/null +++ b/LegacyComponents/TGCameraSegmentsView.m @@ -0,0 +1,259 @@ +#import "TGCameraSegmentsView.h" + +#import + +#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:[[UIImage imageNamed:@"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:[UIImage imageNamed:@"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/LegacyComponents/TGCameraShutterButton.h b/LegacyComponents/TGCameraShutterButton.h new file mode 100644 index 0000000000..c69ded5aad --- /dev/null +++ b/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/LegacyComponents/TGCameraShutterButton.m b/LegacyComponents/TGCameraShutterButton.m new file mode 100644 index 0000000000..f5ca342b93 --- /dev/null +++ b/LegacyComponents/TGCameraShutterButton.m @@ -0,0 +1,216 @@ +#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]; + + 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]; + }]; + } + 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/LegacyComponents/TGCameraTimeCodeView.h b/LegacyComponents/TGCameraTimeCodeView.h new file mode 100644 index 0000000000..b5854a29ae --- /dev/null +++ b/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/LegacyComponents/TGCameraTimeCodeView.m b/LegacyComponents/TGCameraTimeCodeView.m new file mode 100644 index 0000000000..8a6f5ad331 --- /dev/null +++ b/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/LegacyComponents/TGCameraZoomView.h b/LegacyComponents/TGCameraZoomView.h new file mode 100644 index 0000000000..086b07fa96 --- /dev/null +++ b/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/LegacyComponents/TGCameraZoomView.m b/LegacyComponents/TGCameraZoomView.m new file mode 100644 index 0000000000..ed4febd19d --- /dev/null +++ b/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/LegacyComponents/TGCheckButtonView.h b/LegacyComponents/TGCheckButtonView.h new file mode 100644 index 0000000000..9c02ffa07c --- /dev/null +++ b/LegacyComponents/TGCheckButtonView.h @@ -0,0 +1,20 @@ +#import + +typedef enum +{ + TGCheckButtonStyleDefault, + TGCheckButtonStyleDefaultBlue, + TGCheckButtonStyleBar, + TGCheckButtonStyleMedia, + TGCheckButtonStyleGallery, + TGCheckButtonStyleShare +} TGCheckButtonStyle; + +@interface TGCheckButtonView : UIButton + +- (instancetype)initWithStyle:(TGCheckButtonStyle)style; + +- (void)setSelected:(bool)selected animated:(bool)animated; +- (void)setSelected:(bool)selected animated:(bool)animated bump:(bool)bump; + +@end diff --git a/LegacyComponents/TGCheckButtonView.m b/LegacyComponents/TGCheckButtonView.m new file mode 100644 index 0000000000..76e3da631f --- /dev/null +++ b/LegacyComponents/TGCheckButtonView.m @@ -0,0 +1,436 @@ +#import "TGCheckButtonView.h" + +@interface TGCheckButtonView () +{ + UIView *_wrapperView; + + CALayer *_checkBackground; + + TGCheckButtonStyle _style; + CGSize _size; + bool _borderOnTop; + UIImage *_fillImage; + + UIView *_checkView; + UIImageView *_checkFillView; + UIView *_checkShortFragment; + UIView *_checkLongFragment; +} +@end + +@implementation TGCheckButtonView + +static CGAffineTransform TGCheckButtonDefaultTransform; + +- (instancetype)initWithStyle:(TGCheckButtonStyle)style +{ + 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 NSMutableDictionary *backgroundImages; + static NSMutableDictionary *fillImages; + 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; + }); + + bool borderOnTop = false; + CGFloat insideInset = 0.0f; + switch (style) + { + case TGCheckButtonStyleGallery: + { + insideInset = 3.5f; + borderOnTop = true; + } + break; + + case TGCheckButtonStyleMedia: + { + insideInset = 4.0f; + borderOnTop = true; + } + break; + + case TGCheckButtonStyleBar: + { + insideInset = 2.0f; + } + break; + + case TGCheckButtonStyleShare: + { + 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: + { + 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); + CGContextSetLineWidth(context, 1.5f); + CGContextSetStrokeColorWithColor(context, [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); + + int32_t hex = 0xcacacf; + UIColor *color = [[UIColor alloc] initWithRed:(((hex >> 16) & 0xff) / 255.0f) green:(((hex >> 8) & 0xff) / 255.0f) blue:(((hex) & 0xff) / 255.0f) alpha:1.0f]; + + CGContextSetStrokeColorWithColor(context, color.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); + + int32_t hex = 0xcacacf; + UIColor *color = [[UIColor alloc] initWithRed:(((hex >> 16) & 0xff) / 255.0f) green:(((hex >> 8) & 0xff) / 255.0f) blue:(((hex) & 0xff) / 255.0f) alpha:1.0f]; + + CGContextSetStrokeColorWithColor(context, color.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: + { + CGRect rect = CGRectMake(0, 0, size.width, size.height); + UIGraphicsBeginImageContextWithOptions(rect.size, false, 0); + CGContextRef context = UIGraphicsGetCurrentContext(); + + int32_t hex = 0x007ee5; + UIColor *accentColor = [[UIColor alloc] initWithRed:(((hex >> 16) & 0xff) / 255.0f) green:(((hex >> 8) & 0xff) / 255.0f) blue:(((hex) & 0xff) / 255.0f) alpha:1.0f]; + + CGContextSetFillColorWithColor(context, accentColor.CGColor); + CGContextFillEllipseInRect(context, CGRectInset(rect, insideInset + 0.5f, insideInset + 0.5f)); + + CGContextSetLineWidth(context, 2.0f); + CGContextSetStrokeColorWithColor(context, [UIColor whiteColor].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(); + + int32_t hex = 0x29c519; + UIColor *hexColor = [[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 *accentColor = [[UIColor alloc] initWithRed:(((hex >> 16) & 0xff) / 255.0f) green:(((hex >> 8) & 0xff) / 255.0f) blue:(((hex) & 0xff) / 255.0f) alpha:1.0f]; + + UIColor *color = style == TGCheckButtonStyleDefaultBlue ? accentColor : hexColor; + CGContextSetFillColorWithColor(context, color.CGColor); + CGContextFillEllipseInRect(context, CGRectInset(rect, insideInset, insideInset)); + + fillImage = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + } + break; + } + + fillImages[@(style)] = fillImage; + } + + _style = style; + _size = size; + _borderOnTop = borderOnTop; + _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; + + if (_borderOnTop) + [_checkBackground removeFromSuperlayer]; + + _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 = [UIColor whiteColor]; + _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 = [UIColor whiteColor]; + _checkLongFragment.layer.anchorPoint = CGPointMake(0, 0.5f); + _checkLongFragment.frame = longFragmentFrame; + _checkLongFragment.transform = CGAffineTransformMakeScale(0.0f, 1.0f); + [_checkView addSubview:_checkLongFragment]; + + if (_borderOnTop) + [_wrapperView.layer addSublayer:_checkBackground]; +} + +- (void)setSelected:(bool)selected animated:(bool)animated +{ + [self setSelected:selected animated:animated bump:false]; +} + +- (void)setSelected:(bool)selected animated:(bool)animated bump:(bool)bump +{ + 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) + 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:nil]; + + _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:nil]; + } + else + { + CGFloat duration = 0.17f; + if (bump) + duration = 0.15f; + + [UIView animateWithDuration:duration animations:^ + { + _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; + }]; + } + } + else + { + _checkFillView.alpha = selected ? 1.0f : 0.0f; + _checkFillView.transform = selected ? CGAffineTransformIdentity : CGAffineTransformMakeScale(0.1f, 0.1f); + + _checkView.alpha = selected ? 1.0f : 0.0f; + _checkView.transform = TGCheckButtonDefaultTransform; + _checkShortFragment.transform = CGAffineTransformIdentity; + _checkLongFragment.transform = CGAffineTransformIdentity; + } + + super.selected = selected; +} + +#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 diff --git a/LegacyComponents/TGColorWallpaperInfo.h b/LegacyComponents/TGColorWallpaperInfo.h new file mode 100644 index 0000000000..74ef655fbc --- /dev/null +++ b/LegacyComponents/TGColorWallpaperInfo.h @@ -0,0 +1,8 @@ +#import + +@interface TGColorWallpaperInfo : TGWallpaperInfo + +- (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/LegacyComponents/TGColorWallpaperInfo.m b/LegacyComponents/TGColorWallpaperInfo.m new file mode 100644 index 0000000000..394e768b9f --- /dev/null +++ b/LegacyComponents/TGColorWallpaperInfo.m @@ -0,0 +1,132 @@ +#import "TGColorWallpaperInfo.h" + +#import "LegacyComponentsInternal.h" + +@interface TGColorWallpaperInfo () +{ + uint32_t _color; + 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/LegacyComponents/TGCustomImageWallpaperInfo.h b/LegacyComponents/TGCustomImageWallpaperInfo.h new file mode 100644 index 0000000000..5c73d2d755 --- /dev/null +++ b/LegacyComponents/TGCustomImageWallpaperInfo.h @@ -0,0 +1,7 @@ +#import + +@interface TGCustomImageWallpaperInfo : TGWallpaperInfo + +- (instancetype)initWithImage:(UIImage *)image; + +@end diff --git a/LegacyComponents/TGCustomImageWallpaperInfo.m b/LegacyComponents/TGCustomImageWallpaperInfo.m new file mode 100644 index 0000000000..d0d96f4adc --- /dev/null +++ b/LegacyComponents/TGCustomImageWallpaperInfo.m @@ -0,0 +1,83 @@ +/* + * This is the source code of Telegram for iOS v. 1.1 + * It is licensed under GNU GPL v. 2 or later. + * You should have received a copy of the license in this archive (see LICENSE). + * + * Copyright Peter Iakovlev, 2013. + */ + +#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/LegacyComponents/TGDefaultPasscodeBackground.h b/LegacyComponents/TGDefaultPasscodeBackground.h new file mode 100644 index 0000000000..dc45565175 --- /dev/null +++ b/LegacyComponents/TGDefaultPasscodeBackground.h @@ -0,0 +1,5 @@ +#import + +@interface TGDefaultPasscodeBackground : NSObject + +@end diff --git a/LegacyComponents/TGDefaultPasscodeBackground.m b/LegacyComponents/TGDefaultPasscodeBackground.m new file mode 100644 index 0000000000..e363b25aa2 --- /dev/null +++ b/LegacyComponents/TGDefaultPasscodeBackground.m @@ -0,0 +1,78 @@ +#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); + + _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/LegacyComponents/TGDraggableCollectionView.h b/LegacyComponents/TGDraggableCollectionView.h new file mode 100644 index 0000000000..e61f41d011 --- /dev/null +++ b/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/LegacyComponents/TGDraggableCollectionView.m b/LegacyComponents/TGDraggableCollectionView.m new file mode 100644 index 0000000000..81dcf9b0b7 --- /dev/null +++ b/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/LegacyComponents/TGDraggableCollectionViewFlowLayout.h b/LegacyComponents/TGDraggableCollectionViewFlowLayout.h new file mode 100644 index 0000000000..d380875c8a --- /dev/null +++ b/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/LegacyComponents/TGDraggableCollectionViewFlowLayout.m b/LegacyComponents/TGDraggableCollectionViewFlowLayout.m new file mode 100644 index 0000000000..ace72261a9 --- /dev/null +++ b/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/LegacyComponents/TGEmbedCoubPlayerView.h b/LegacyComponents/TGEmbedCoubPlayerView.h new file mode 100644 index 0000000000..24cc45274a --- /dev/null +++ b/LegacyComponents/TGEmbedCoubPlayerView.h @@ -0,0 +1,10 @@ +#import "TGEmbedPlayerView.h" + +@interface TGEmbedCoubPlayerView : TGEmbedPlayerView + ++ (NSString *)_coubVideoIdFromText:(NSString *)text; + ++ (NSDictionary *)coubJSONByPermalink:(NSString *)permalink; ++ (void)setCoubJSON:(NSDictionary *)json forPermalink:(NSString *)permalink; + +@end diff --git a/LegacyComponents/TGEmbedCoubPlayerView.m b/LegacyComponents/TGEmbedCoubPlayerView.m new file mode 100644 index 0000000000..5b651ca1c2 --- /dev/null +++ b/LegacyComponents/TGEmbedCoubPlayerView.m @@ -0,0 +1,413 @@ +#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 TGEmbedCoubPlayerView () +{ + NSString *_permalink; + + bool _started; + UIImage *_coverImage; + + CBPlayerView *_playerView; + CBCoubPlayer *_coubPlayer; + + SDisposableSet *_disposables; + + id _asset; +} +@end + +@implementation TGEmbedCoubPlayerView + +- (instancetype)initWithWebPageAttachment:(TGWebPageMediaAttachment *)webPage thumbnailSignal:(SSignal *)thumbnailSignal +{ + self = [super initWithWebPageAttachment:webPage thumbnailSignal:thumbnailSignal]; + if (self != nil) + { + _permalink = [TGEmbedCoubPlayerView _coubVideoIdFromText:webPage.embedUrl]; + _disposables = [[SDisposableSet alloc] init]; + + if (thumbnailSignal == nil) + { + 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]]; + } + } + + if (videoPath != nil && [[NSFileManager defaultManager] fileExistsAtPath:videoPath isDirectory:NULL]) + { + __weak TGEmbedCoubPlayerView *weakSelf = self; + [_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]; + }]]; + } + } + + self.controlsView.watermarkImage = [UIImage imageNamed:@"CoubWatermark"]; + self.controlsView.watermarkOffset = CGPointMake(12.0f, 12.0f); + } + return self; +} + +- (void)dealloc +{ + [_disposables dispose]; +} + +- (bool)supportsPIP +{ + return false; +} + +- (void)_watermarkAction +{ + [super _watermarkAction]; + + 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 = [[NSBundle mainBundle] pathForResource:@"DefaultPlayer" ofType:@"html"]; + + NSString *embedHTMLTemplate = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:&error]; + if (error != nil) + { + TGLog(@"[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; +} + +- (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 }; + }]; + }]; + + [_disposables add:[[dataSignal deliverOn:[SQueue mainQueue]] startWithNext:^(NSDictionary *data) + { + __strong TGEmbedCoubPlayerView *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + id coub = [CBCoubNew coubWithAttributes:data[@"json"]]; + 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)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 +{ + TGLog(@"[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 diff --git a/LegacyComponents/TGEmbedInstagramPlayerView.h b/LegacyComponents/TGEmbedInstagramPlayerView.h new file mode 100644 index 0000000000..e103032699 --- /dev/null +++ b/LegacyComponents/TGEmbedInstagramPlayerView.h @@ -0,0 +1,5 @@ +#import "TGEmbedPlayerView.h" + +@interface TGEmbedInstagramPlayerView : TGEmbedPlayerView + +@end diff --git a/LegacyComponents/TGEmbedInstagramPlayerView.m b/LegacyComponents/TGEmbedInstagramPlayerView.m new file mode 100644 index 0000000000..7dc8028675 --- /dev/null +++ b/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 +{ + self = [super initWithWebPageAttachment:webPage thumbnailSignal:thumbnailSignal]; + 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 = [[NSBundle mainBundle] pathForResource:@"InstagramPlayer" ofType:@"html"]; + + NSString *embedHTMLTemplate = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:&error]; + if (error != nil) + { + TGLog(@"[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/LegacyComponents/TGEmbedPIPButton.h b/LegacyComponents/TGEmbedPIPButton.h new file mode 100644 index 0000000000..d0bcb94075 --- /dev/null +++ b/LegacyComponents/TGEmbedPIPButton.h @@ -0,0 +1,9 @@ +#import + +@interface TGEmbedPIPButton : UIButton + +- (void)setIconImage:(UIImage *)iconImage; + +@end + +extern const CGSize TGEmbedPIPButtonSize; diff --git a/LegacyComponents/TGEmbedPIPButton.m b/LegacyComponents/TGEmbedPIPButton.m new file mode 100644 index 0000000000..482564985d --- /dev/null +++ b/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/LegacyComponents/TGEmbedPIPPullArrowView.h b/LegacyComponents/TGEmbedPIPPullArrowView.h new file mode 100644 index 0000000000..b0adb16366 --- /dev/null +++ b/LegacyComponents/TGEmbedPIPPullArrowView.h @@ -0,0 +1,7 @@ +#import + +@interface TGEmbedPIPPullArrowView : UIView + +- (void)setAngled:(bool)angled animated:(bool)animated; + +@end diff --git a/LegacyComponents/TGEmbedPIPPullArrowView.m b/LegacyComponents/TGEmbedPIPPullArrowView.m new file mode 100644 index 0000000000..fbd13c3c03 --- /dev/null +++ b/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/LegacyComponents/TGEmbedPIPScrubber.h b/LegacyComponents/TGEmbedPIPScrubber.h new file mode 100644 index 0000000000..ade95f7089 --- /dev/null +++ b/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/LegacyComponents/TGEmbedPIPScrubber.m b/LegacyComponents/TGEmbedPIPScrubber.m new file mode 100644 index 0000000000..ccc862cea3 --- /dev/null +++ b/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/LegacyComponents/TGEmbedPlayerControls.h b/LegacyComponents/TGEmbedPlayerControls.h new file mode 100644 index 0000000000..1e098c0979 --- /dev/null +++ b/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/LegacyComponents/TGEmbedPlayerControls.m b/LegacyComponents/TGEmbedPlayerControls.m new file mode 100644 index 0000000000..6082243b5b --- /dev/null +++ b/LegacyComponents/TGEmbedPlayerControls.m @@ -0,0 +1,684 @@ +#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; + 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 (TGEmbedPlayerControlsTypeFull) + { + if (iosMajorVersion() >= 8) + { + UIVisualEffectView *effectView = [[UIVisualEffectView alloc] initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleLight]]; + _backgroundView = effectView; + + UIView *whiteView = [[UIView alloc] initWithFrame:effectView.bounds]; + whiteView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + whiteView.backgroundColor = UIColorRGBA(0xffffff, 0.3f); + [_backgroundView addSubview:whiteView]; + } + else + { + _backgroundView = [[UIView alloc] initWithFrame:CGRectZero]; + } + [self addSubview:_backgroundView]; + + _pauseButton = [[TGModernButton alloc] initWithFrame:CGRectMake(0, 0, 38, TGEmbedPlayerControlsPanelHeight)]; + _pauseButton.exclusiveTouch = true; + [_pauseButton setImage:[UIImage imageNamed:@"EmbedVideoPauseIcon"] forState:UIControlStateNormal]; + [_pauseButton addTarget:self action:@selector(pauseButtonPressed) forControlEvents:UIControlEventTouchUpInside]; + [_backgroundView addSubview:_pauseButton]; + + _playButton = [[TGModernButton alloc] initWithFrame:CGRectMake(0, 0, 38, TGEmbedPlayerControlsPanelHeight)]; + _playButton.exclusiveTouch = true; + [_playButton setImage:[UIImage imageNamed:@"EmbedVideoPlayIcon"] forState:UIControlStateNormal]; + [_playButton addTarget:self action:@selector(playButtonPressed) forControlEvents:UIControlEventTouchUpInside]; + [_backgroundView 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; + [_backgroundView 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; + [_backgroundView addSubview:_remainingLabel]; + + _pictureInPictureButton = [[TGModernButton alloc] initWithFrame:CGRectMake(frame.size.width - 45.0f, 0, 45.0f, TGEmbedPlayerControlsPanelHeight)]; + _pictureInPictureButton.exclusiveTouch = true; + [_pictureInPictureButton setImage:[UIImage imageNamed:@"EmbedVideoPIPIcon"] forState:UIControlStateNormal]; + [_pictureInPictureButton addTarget:self action:@selector(pictureInPictureButtonPressed) forControlEvents:UIControlEventTouchUpInside]; + [_backgroundView 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)]; + [_backgroundView 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:[UIImage imageNamed:@"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; + } + + 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/LegacyComponents/TGEmbedPlayerScrubber.h b/LegacyComponents/TGEmbedPlayerScrubber.h new file mode 100644 index 0000000000..0d9487735a --- /dev/null +++ b/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/LegacyComponents/TGEmbedPlayerScrubber.m b/LegacyComponents/TGEmbedPlayerScrubber.m new file mode 100644 index 0000000000..70839b6147 --- /dev/null +++ b/LegacyComponents/TGEmbedPlayerScrubber.m @@ -0,0 +1,173 @@ +#import "TGEmbedPlayerScrubber.h" + +#import + +#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 = [[UIImage imageNamed:@"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([UIImage imageNamed:@"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/LegacyComponents/TGEmbedPlayerState.h b/LegacyComponents/TGEmbedPlayerState.h new file mode 100644 index 0000000000..7582e5f532 --- /dev/null +++ b/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; + +@end diff --git a/LegacyComponents/TGEmbedPlayerState.m b/LegacyComponents/TGEmbedPlayerState.m new file mode 100644 index 0000000000..99310fcaab --- /dev/null +++ b/LegacyComponents/TGEmbedPlayerState.m @@ -0,0 +1,27 @@ +#import "TGEmbedPlayerState.h" + +@implementation TGEmbedPlayerState + +@synthesize playing = _playing; +@synthesize duration = _duration; +@synthesize position = _position; +@synthesize downloadProgress = _downloadProgress; + ++ (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 +{ + TGEmbedPlayerState *state = [[TGEmbedPlayerState alloc] init]; + state->_playing = playing; + state->_duration = duration; + state->_position = position; + state->_downloadProgress = downloadProgress; + return state; +} + +@end diff --git a/LegacyComponents/TGEmbedPlayerView.h b/LegacyComponents/TGEmbedPlayerView.h new file mode 100644 index 0000000000..2328d9ba3c --- /dev/null +++ b/LegacyComponents/TGEmbedPlayerView.h @@ -0,0 +1,97 @@ +#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) CGRect initialFrame; + +@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; +- (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; + ++ (bool)_supportsWebPage:(TGWebPageMediaAttachment *)webPage; + ++ (Class)playerViewClassForWebPage:(TGWebPageMediaAttachment *)webPage onlySpecial:(bool)onlySpecial; + +@end diff --git a/LegacyComponents/TGEmbedPlayerView.m b/LegacyComponents/TGEmbedPlayerView.m new file mode 100644 index 0000000000..d80876e283 --- /dev/null +++ b/LegacyComponents/TGEmbedPlayerView.m @@ -0,0 +1,866 @@ +#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 "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; +} +@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 +{ + self = [super initWithFrame:CGRectZero]; + if (self != nil) + { + self.clipsToBounds = true; + + _statePipe = [[SPipe alloc] init]; + + _webPage = webPage; + _state = [TGEmbedPlayerState stateWithPlaying:false duration:0.0 position:0.0 downloadProgress:0.0f]; + + 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]; + + _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; + [wkWebView removeObserver:self forKeyPath:@"estimatedProgress"]; + }]; + + _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)_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]; + } +} + +- (CGFloat)_compensationEdges +{ + return 0.0f; +} + +- (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]; + + _dimWrapperView.hidden = false; + _dimWrapperView.alpha = 1.0f; + } + else + { + [self setLoadProgress:1.0f duration:0.2]; + + 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]; + else + [_overlayView setNone]; + } +} + +- (void)setLoadProgress:(CGFloat)value duration:(NSTimeInterval)duration +{ + [_overlayView setProgressAnimated:value duration:duration cancelEnabled:false]; +} + +- (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; + + 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]]; + }]; + + [_wkWebView addObserver:self forKeyPath:@"estimatedProgress" options:NSKeyValueObservingOptionNew context:NULL]; +} + +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { + if ([keyPath isEqualToString:@"estimatedProgress"] && object == _wkWebView) + return; + else + [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; +} + +- (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; + + 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 = [[NSBundle mainBundle] pathForResource:@"DefaultPlayer" ofType:@"html"]; + NSError *error = nil; + NSString *embedHTMLTemplate = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:&error]; + if (error != nil) + { + TGLog(@"[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 = [[NSBundle mainBundle] pathForResource:@"DefaultPlayerInject" ofType:@"js"]; + NSString *scriptText = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:&error]; + if (error != nil) + TGLog(@"[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 removeObserver:self forKeyPath:@"estimatedProgress"]; + [_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], + [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; +} + +@end diff --git a/LegacyComponents/TGEmbedSoundCloudPlayerView.h b/LegacyComponents/TGEmbedSoundCloudPlayerView.h new file mode 100644 index 0000000000..15d33b7e5c --- /dev/null +++ b/LegacyComponents/TGEmbedSoundCloudPlayerView.h @@ -0,0 +1,7 @@ +#import "TGEmbedPlayerView.h" + +@interface TGEmbedSoundCloudPlayerView : TGEmbedPlayerView + ++ (NSString *)_soundCloudIdFromText:(NSString *)text; + +@end diff --git a/LegacyComponents/TGEmbedSoundCloudPlayerView.m b/LegacyComponents/TGEmbedSoundCloudPlayerView.m new file mode 100644 index 0000000000..ecf8349e67 --- /dev/null +++ b/LegacyComponents/TGEmbedSoundCloudPlayerView.m @@ -0,0 +1,74 @@ +#import "TGEmbedSoundCloudPlayerView.h" + +@implementation TGEmbedSoundCloudPlayerView + +- (instancetype)initWithWebPageAttachment:(TGWebPageMediaAttachment *)webPage thumbnailSignal:(SSignal *)thumbnailSignal +{ + self = [super initWithWebPageAttachment:webPage thumbnailSignal:thumbnailSignal]; + if (self != nil) + { + + } + return self; +} + +- (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/LegacyComponents/TGEmbedVKPlayerView.h b/LegacyComponents/TGEmbedVKPlayerView.h new file mode 100644 index 0000000000..9e91fd7590 --- /dev/null +++ b/LegacyComponents/TGEmbedVKPlayerView.h @@ -0,0 +1,5 @@ +#import "TGEmbedPlayerView.h" + +@interface TGEmbedVKPlayerView : TGEmbedPlayerView + +@end diff --git a/LegacyComponents/TGEmbedVKPlayerView.m b/LegacyComponents/TGEmbedVKPlayerView.m new file mode 100644 index 0000000000..0369d2d967 --- /dev/null +++ b/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 +{ + self = [super initWithWebPageAttachment:webPage thumbnailSignal:thumbnailSignal]; + 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:@"