diff --git a/submodules/Camera/BUILD b/submodules/Camera/BUILD index eb09de4aa3..07377c40c1 100644 --- a/submodules/Camera/BUILD +++ b/submodules/Camera/BUILD @@ -57,6 +57,7 @@ swift_library( "//submodules/AsyncDisplayKit:AsyncDisplayKit", "//submodules/Display:Display", "//submodules/ImageBlur:ImageBlur", + "//submodules/TelegramCore:TelegramCore", ], visibility = [ "//visibility:public", diff --git a/submodules/Camera/Sources/Camera.swift b/submodules/Camera/Sources/Camera.swift index 0a469b2820..e57823a79a 100644 --- a/submodules/Camera/Sources/Camera.swift +++ b/submodules/Camera/Sources/Camera.swift @@ -40,14 +40,14 @@ private final class CameraContext { } } + private let previewSnapshotContext = CIContext() private var lastSnapshotTimestamp: Double = CACurrentMediaTime() private func savePreviewSnapshot(pixelBuffer: CVPixelBuffer) { Queue.concurrentDefaultQueue().async { - let ciContext = CIContext() var ciImage = CIImage(cvImageBuffer: pixelBuffer) - ciImage = ciImage.transformed(by: CGAffineTransform(scaleX: 0.33, y: 0.33)) - ciImage = ciImage.clampedToExtent() - if let cgImage = ciContext.createCGImage(ciImage, from: ciImage.extent) { + let size = ciImage.extent.size + ciImage = ciImage.clampedToExtent().applyingGaussianBlur(sigma: 40.0).cropped(to: CGRect(origin: .zero, size: size)) + if let cgImage = self.previewSnapshotContext.createCGImage(ciImage, from: ciImage.extent) { let uiImage = UIImage(cgImage: cgImage, scale: 1.0, orientation: .right) CameraSimplePreviewView.saveLastStateImage(uiImage) } @@ -67,7 +67,7 @@ private final class CameraContext { self.input.configure(for: self.session, device: self.device, audio: configuration.audio) self.output.configure(for: self.session, configuration: configuration) - self.device.configureDeviceFormat(maxDimensions: CMVideoDimensions(width: 1920, height: 1080), maxFramerate: 60) + self.device.configureDeviceFormat(maxDimensions: CMVideoDimensions(width: 1920, height: 1080), maxFramerate: self.preferredMaxFrameRate) self.output.configureVideoStabilization() } @@ -115,6 +115,15 @@ private final class CameraContext { } } + private var preferredMaxFrameRate: Double { + switch DeviceModel.current { + case .iPhone14ProMax, .iPhone13ProMax: + return 60.0 + default: + return 30.0 + } + } + func startCapture() { guard !self.session.isRunning else { return @@ -160,7 +169,7 @@ private final class CameraContext { self.changingPosition = true self.device.configure(for: self.session, position: targetPosition) self.input.configure(for: self.session, device: self.device, audio: self.initialConfiguration.audio) - self.device.configureDeviceFormat(maxDimensions: CMVideoDimensions(width: 1920, height: 1080), maxFramerate: 60) + self.device.configureDeviceFormat(maxDimensions: CMVideoDimensions(width: 1920, height: 1080), maxFramerate: self.preferredMaxFrameRate) self.output.configureVideoStabilization() self.queue.after(0.5) { self.changingPosition = false @@ -173,7 +182,7 @@ private final class CameraContext { self.input.invalidate(for: self.session) self.device.configure(for: self.session, position: position) self.input.configure(for: self.session, device: self.device, audio: self.initialConfiguration.audio) - self.device.configureDeviceFormat(maxDimensions: CMVideoDimensions(width: 1920, height: 1080), maxFramerate: 60) + self.device.configureDeviceFormat(maxDimensions: CMVideoDimensions(width: 1920, height: 1080), maxFramerate: self.preferredMaxFrameRate) self.output.configureVideoStabilization() } } diff --git a/submodules/Camera/Sources/CameraDevice.swift b/submodules/Camera/Sources/CameraDevice.swift index 182017f167..816ce061d9 100644 --- a/submodules/Camera/Sources/CameraDevice.swift +++ b/submodules/Camera/Sources/CameraDevice.swift @@ -1,6 +1,7 @@ import Foundation import AVFoundation import SwiftSignalKit +import TelegramCore private let defaultFPS: Double = 30.0 @@ -68,6 +69,14 @@ final class CameraDevice { if let bestFormat = candidates.last { device.activeFormat = bestFormat + + Logger.shared.log("Camera", "Available formats:") + for format in device.formats { + Logger.shared.log("Camera", format.description) + } + + Logger.shared.log("Camera", "Selected format:") + Logger.shared.log("Camera", bestFormat.description) } if let targetFPS = device.actualFPS(maxFramerate) { diff --git a/submodules/Camera/Sources/CameraMetrics.swift b/submodules/Camera/Sources/CameraMetrics.swift index cc7c0e20bf..9c296c8528 100644 --- a/submodules/Camera/Sources/CameraMetrics.swift +++ b/submodules/Camera/Sources/CameraMetrics.swift @@ -23,6 +23,8 @@ public extension Camera { self = .iPhone14ProMax case .unknown: self = .unknown + default: + self = .unknown } } @@ -70,6 +72,16 @@ enum DeviceModel: CaseIterable { case iPodTouch6 case iPodTouch7 + case iPhone12 + case iPhone12Mini + case iPhone12Pro + case iPhone12ProMax + + case iPhone13 + case iPhone13Mini + case iPhone13Pro + case iPhone13ProMax + case iPhone14 case iPhone14Plus case iPhone14Pro @@ -93,6 +105,22 @@ enum DeviceModel: CaseIterable { return "iPod7,1" case .iPodTouch7: return "iPod9,1" + case .iPhone12: + return "iPhone13,2" + case .iPhone12Mini: + return "iPhone13,1" + case .iPhone12Pro: + return "iPhone13,3" + case .iPhone12ProMax: + return "iPhone13,4" + case .iPhone13: + return "iPhone14,5" + case .iPhone13Mini: + return "iPhone14,4" + case .iPhone13Pro: + return "iPhone14,2" + case .iPhone13ProMax: + return "iPhone14,3" case .iPhone14: return "iPhone14,7" case .iPhone14Plus: @@ -122,6 +150,22 @@ enum DeviceModel: CaseIterable { return "iPod touch 6G" case .iPodTouch7: return "iPod touch 7G" + case .iPhone12: + return "iPhone 12" + case .iPhone12Mini: + return "iPhone 12 mini" + case .iPhone12Pro: + return "iPhone 12 Pro" + case .iPhone12ProMax: + return "iPhone 12 Pro Max" + case .iPhone13: + return "iPhone 13" + case .iPhone13Mini: + return "iPhone 13 mini" + case .iPhone13Pro: + return "iPhone 13 Pro" + case .iPhone13ProMax: + return "iPhone 13 Pro Max" case .iPhone14: return "iPhone 14" case .iPhone14Plus: diff --git a/submodules/Camera/Sources/CameraOutput.swift b/submodules/Camera/Sources/CameraOutput.swift index 5735944198..de12b7f7d6 100644 --- a/submodules/Camera/Sources/CameraOutput.swift +++ b/submodules/Camera/Sources/CameraOutput.swift @@ -65,7 +65,6 @@ final class CameraOutput: NSObject { super.init() self.videoOutput.alwaysDiscardsLateVideoFrames = false - //self.videoOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey: kCVPixelFormatType_32BGRA] as [String : Any] self.videoOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey: kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange] as [String : Any] self.faceLandmarksOutput.outputFaceObservations = { [weak self] observations in diff --git a/submodules/Camera/Sources/CameraPreviewView.swift b/submodules/Camera/Sources/CameraPreviewView.swift index 3ed8925847..3361f257be 100644 --- a/submodules/Camera/Sources/CameraPreviewView.swift +++ b/submodules/Camera/Sources/CameraPreviewView.swift @@ -21,7 +21,7 @@ public class CameraSimplePreviewView: UIView { static func saveLastStateImage(_ image: UIImage) { let imagePath = NSTemporaryDirectory() + "cameraImage.jpg" - if let blurredImage = blurredImage(image, radius: 60.0), let data = blurredImage.jpegData(compressionQuality: 0.85) { + if let data = image.jpegData(compressionQuality: 0.6) { try? data.write(to: URL(fileURLWithPath: imagePath)) } } diff --git a/submodules/DrawingUI/Sources/ColorPickerScreen.swift b/submodules/DrawingUI/Sources/ColorPickerScreen.swift index fe314eec12..5745ca248b 100644 --- a/submodules/DrawingUI/Sources/ColorPickerScreen.swift +++ b/submodules/DrawingUI/Sources/ColorPickerScreen.swift @@ -937,7 +937,7 @@ final class ColorSpectrumComponent: Component { } } -final class ColorSpectrumPickerView: UIView, UIGestureRecognizerDelegate { +public final class ColorSpectrumPickerView: UIView, UIGestureRecognizerDelegate { private var validSize: CGSize? private var selectedColor: DrawingColor? @@ -950,7 +950,7 @@ final class ColorSpectrumPickerView: UIView, UIGestureRecognizerDelegate { private var circleMaskView = UIView() private let maskCircle = SimpleShapeLayer() - var selected: (DrawingColor) -> Void = { _ in } + public var selected: (DrawingColor) -> Void = { _ in } private var bitmapData: UnsafeMutableRawPointer? @@ -1048,7 +1048,7 @@ final class ColorSpectrumPickerView: UIView, UIGestureRecognizerDelegate { private var animatingIn = false private var scheduledAnimateOut: (() -> Void)? - func animateIn() { + public func animateIn() { self.animatingIn = true Queue.mainQueue().after(0.15) { @@ -1107,7 +1107,7 @@ final class ColorSpectrumPickerView: UIView, UIGestureRecognizerDelegate { }) } - func updateLayout(size: CGSize, selectedColor: DrawingColor?) -> CGSize { + public func updateLayout(size: CGSize, selectedColor: DrawingColor?) -> CGSize { let previousSize = self.validSize let imageSize = size @@ -2413,10 +2413,10 @@ private final class ColorPickerSheetComponent: CombinedComponent { } } -class ColorPickerScreen: ViewControllerComponentContainer { +public final class ColorPickerScreen: ViewControllerComponentContainer { private var dismissed: () -> Void - init(context: AccountContext, initialColor: DrawingColor, updated: @escaping (DrawingColor) -> Void, openEyedropper: @escaping () -> Void, dismissed: @escaping () -> Void = {}) { + public init(context: AccountContext, initialColor: DrawingColor, updated: @escaping (DrawingColor) -> Void, openEyedropper: @escaping () -> Void, dismissed: @escaping () -> Void = {}) { self.dismissed = dismissed super.init(context: context, component: ColorPickerSheetComponent(context: context, initialColor: initialColor, updated: updated, openEyedropper: openEyedropper, dismissed: dismissed), navigationBarAppearance: .none) diff --git a/submodules/DrawingUI/Sources/DrawingEntitiesView.swift b/submodules/DrawingUI/Sources/DrawingEntitiesView.swift index 77ac1d044a..3b7e0f164a 100644 --- a/submodules/DrawingUI/Sources/DrawingEntitiesView.swift +++ b/submodules/DrawingUI/Sources/DrawingEntitiesView.swift @@ -54,7 +54,7 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView { public weak var selectionContainerView: DrawingSelectionContainerView? private var tapGestureRecognizer: UITapGestureRecognizer! - private(set) var selectedEntityView: DrawingEntityView? + public private(set) var selectedEntityView: DrawingEntityView? public var getEntityCenterPosition: () -> CGPoint = { return .zero } public var getEntityInitialRotation: () -> CGFloat = { return 0.0 } @@ -593,7 +593,7 @@ protocol DrawingEntityMediaView: DrawingEntityView { public class DrawingEntityView: UIView { let context: AccountContext - let entity: DrawingEntity + public let entity: DrawingEntity var isTracking = false public weak var selectionView: DrawingEntitySelectionView? @@ -645,7 +645,7 @@ public class DrawingEntityView: UIView { } - func update(animated: Bool = false) { + public func update(animated: Bool = false) { self.updateSelectionView() } diff --git a/submodules/DrawingUI/Sources/DrawingMediaEntity.swift b/submodules/DrawingUI/Sources/DrawingMediaEntity.swift index b78e163148..bc73d07eb8 100644 --- a/submodules/DrawingUI/Sources/DrawingMediaEntity.swift +++ b/submodules/DrawingUI/Sources/DrawingMediaEntity.swift @@ -89,7 +89,7 @@ public final class DrawingMediaEntityView: DrawingEntityView, DrawingEntityMedia } public var updated: (() -> Void)? - override func update(animated: Bool) { + public override func update(animated: Bool) { self.center = self.mediaEntity.position let size = self.mediaEntity.baseSize diff --git a/submodules/DrawingUI/Sources/DrawingScreen.swift b/submodules/DrawingUI/Sources/DrawingScreen.swift index 712a2b5077..8aaf79a8a2 100644 --- a/submodules/DrawingUI/Sources/DrawingScreen.swift +++ b/submodules/DrawingUI/Sources/DrawingScreen.swift @@ -1,6 +1,7 @@ import Foundation import UIKit import CoreServices +import AsyncDisplayKit import Display import ComponentFlow import LegacyComponents @@ -511,6 +512,7 @@ private final class DrawingScreenComponent: CombinedComponent { let insertText: ActionSlot let updateEntityView: ActionSlot<(UUID, Bool)> let endEditingTextEntityView: ActionSlot<(UUID, Bool)> + let entityViewForEntity: (DrawingEntity) -> DrawingEntityView? let apply: ActionSlot let dismiss: ActionSlot @@ -544,6 +546,7 @@ private final class DrawingScreenComponent: CombinedComponent { insertText: ActionSlot, updateEntityView: ActionSlot<(UUID, Bool)>, endEditingTextEntityView: ActionSlot<(UUID, Bool)>, + entityViewForEntity: @escaping (DrawingEntity) -> DrawingEntityView?, apply: ActionSlot, dismiss: ActionSlot, presentColorPicker: @escaping (DrawingColor) -> Void, @@ -575,6 +578,7 @@ private final class DrawingScreenComponent: CombinedComponent { self.insertText = insertText self.updateEntityView = updateEntityView self.endEditingTextEntityView = endEditingTextEntityView + self.entityViewForEntity = entityViewForEntity self.apply = apply self.dismiss = dismiss self.presentColorPicker = presentColorPicker @@ -652,6 +656,7 @@ private final class DrawingScreenComponent: CombinedComponent { private let insertText: ActionSlot private let updateEntityView: ActionSlot<(UUID, Bool)> private let endEditingTextEntityView: ActionSlot<(UUID, Bool)> + private let entityViewForEntity: (DrawingEntity) -> DrawingEntityView? private let present: (ViewController) -> Void var currentMode: Mode @@ -678,6 +683,7 @@ private final class DrawingScreenComponent: CombinedComponent { insertText: ActionSlot, updateEntityView: ActionSlot<(UUID, Bool)>, endEditingTextEntityView: ActionSlot<(UUID, Bool)>, + entityViewForEntity: @escaping (DrawingEntity) -> DrawingEntityView?, present: @escaping (ViewController) -> Void) { self.context = context @@ -692,6 +698,7 @@ private final class DrawingScreenComponent: CombinedComponent { self.insertText = insertText self.updateEntityView = updateEntityView self.endEditingTextEntityView = endEditingTextEntityView + self.entityViewForEntity = entityViewForEntity self.present = present self.currentMode = .drawing @@ -1015,7 +1022,23 @@ private final class DrawingScreenComponent: CombinedComponent { } func makeState() -> State { - return State(context: self.context, existingStickerPickerInputData: self.existingStickerPickerInputData, updateToolState: self.updateToolState, insertEntity: self.insertEntity, deselectEntity: self.deselectEntity, updateEntitiesPlayback: self.updateEntitiesPlayback, dismissEyedropper: self.dismissEyedropper, toggleWithEraser: self.toggleWithEraser, toggleWithPreviousTool: self.toggleWithPreviousTool, insertSticker: self.insertSticker, insertText: self.insertText, updateEntityView: self.updateEntityView, endEditingTextEntityView: self.endEditingTextEntityView, present: self.present) + return State( + context: self.context, + existingStickerPickerInputData: self.existingStickerPickerInputData, + updateToolState: self.updateToolState, + insertEntity: self.insertEntity, + deselectEntity: self.deselectEntity, + updateEntitiesPlayback: self.updateEntitiesPlayback, + dismissEyedropper: self.dismissEyedropper, + toggleWithEraser: self.toggleWithEraser, + toggleWithPreviousTool: self.toggleWithPreviousTool, + insertSticker: self.insertSticker, + insertText: self.insertText, + updateEntityView: self.updateEntityView, + endEditingTextEntityView: self.endEditingTextEntityView, + entityViewForEntity: self.entityViewForEntity, + present: self.present + ) } static var body: Body { @@ -1632,9 +1655,9 @@ private final class DrawingScreenComponent: CombinedComponent { var sizeSliderVisible = false var isEditingText = false var sizeValue: CGFloat? - if let textEntity = state.selectedEntity as? DrawingTextEntity, !"".isEmpty {//} let entityView = textEntity.currentEntityView as? DrawingTextEntityView { + if let textEntity = state.selectedEntity as? DrawingTextEntity, let entityView = component.entityViewForEntity(textEntity) as? DrawingTextEntityView { sizeSliderVisible = true - isEditingText = false//entityView.isEditing + isEditingText = entityView.isEditing sizeValue = textEntity.fontSize } else { if state.selectedEntity == nil || !(state.selectedEntity is DrawingStickerEntity) { @@ -2041,6 +2064,7 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U fileprivate final class Node: ViewControllerTracingNode { private weak var controller: DrawingScreen? private let context: AccountContext + private var interaction: DrawingToolsInteraction? private let updateState: ActionSlot private let updateColor: ActionSlot private let performAction: ActionSlot @@ -2064,10 +2088,7 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U private let dismiss: ActionSlot fileprivate let componentHost: ComponentView - - private let textEditAccessoryView: UIInputView - private let textEditAccessoryHost: ComponentView - + private var presentationData: PresentationData private let hapticFeedback = HapticFeedback() private var validLayout: (ContainerViewLayout, UIInterfaceOrientation?)? @@ -2098,68 +2119,67 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U } } self._drawingView?.requestedColorPicker = { [weak self] in - if let strongSelf = self { - if let _ = strongSelf.colorPickerScreen { - strongSelf.dismissColorPicker() + if let self, let interaction = self.interaction { + if let _ = interaction.colorPickerScreen { + interaction.dismissColorPicker() } else { - strongSelf.requestPresentColorPicker.invoke(Void()) + self.requestPresentColorPicker.invoke(Void()) } } } self._drawingView?.requestedEraserToggle = { [weak self] in - if let strongSelf = self { - strongSelf.toggleWithEraser.invoke(Void()) + if let self { + self.toggleWithEraser.invoke(Void()) } } self._drawingView?.requestedToolsToggle = { [weak self] in - if let strongSelf = self { - strongSelf.toggleWithPreviousTool.invoke(Void()) + if let self { + self.toggleWithPreviousTool.invoke(Void()) } } self.performAction.connect { [weak self] action in - if let strongSelf = self { - if action == .clear { - let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData.withUpdated(theme: defaultDarkColorPresentationTheme)) + if let self { + if case .clear = action { + let actionSheet = ActionSheetController(presentationData: self.presentationData.withUpdated(theme: defaultDarkColorPresentationTheme)) actionSheet.setItemGroups([ ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Paint_ClearConfirm, color: .destructive, action: { [weak actionSheet, weak self] in + ActionSheetButtonItem(title: self.presentationData.strings.Paint_ClearConfirm, color: .destructive, action: { [weak actionSheet, weak self] in actionSheet?.dismissAnimated() self?._drawingView?.performAction(action) }) ]), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in + ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ]) ]) - strongSelf.controller?.present(actionSheet, in: .window(.root)) + self.controller?.present(actionSheet, in: .window(.root)) } else { - strongSelf._drawingView?.performAction(action) + self._drawingView?.performAction(action) } } } self.updateToolState.connect { [weak self] state in - if let strongSelf = self { - strongSelf._drawingView?.updateToolState(state) + if let self { + self._drawingView?.updateToolState(state) } } self.previewBrushSize.connect { [weak self] size in - if let strongSelf = self { - strongSelf._drawingView?.setBrushSizePreview(size) + if let self { + self._drawingView?.setBrushSizePreview(size) } } self.dismissEyedropper.connect { [weak self] in - if let strongSelf = self { - strongSelf.dismissCurrentEyedropper() + if let self { + self.interaction?.dismissCurrentEyedropper() } } } return self._drawingView! } - private weak var currentMenuController: ContextMenuController? var _entitiesView: DrawingEntitiesView? var entitiesView: DrawingEntitiesView { if self._entitiesView == nil, let controller = self.controller { @@ -2206,76 +2226,9 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U strongSelf.updateSelectedEntity.invoke(entity) } } - self._entitiesView?.requestedMenuForEntityView = { [weak self] entityView, isTopmost in - guard let strongSelf = self else { - return - } - if strongSelf.currentMenuController != nil { - if let entityView = entityView as? DrawingTextEntityView { - entityView.beginEditing(accessoryView: strongSelf.textEditAccessoryView) - } - return - } - var actions: [ContextMenuAction] = [] - actions.append(ContextMenuAction(content: .text(title: strongSelf.presentationData.strings.Paint_Delete, accessibilityLabel: strongSelf.presentationData.strings.Paint_Delete), action: { [weak self, weak entityView] in - if let strongSelf = self, let entityView = entityView { - strongSelf.entitiesView.remove(uuid: entityView.entity.uuid, animated: true) - } - })) - if let entityView = entityView as? DrawingTextEntityView { - actions.append(ContextMenuAction(content: .text(title: strongSelf.presentationData.strings.Paint_Edit, accessibilityLabel: strongSelf.presentationData.strings.Paint_Edit), action: { [weak self, weak entityView] in - if let strongSelf = self, let entityView = entityView { - entityView.beginEditing(accessoryView: strongSelf.textEditAccessoryView) - strongSelf.entitiesView.selectEntity(entityView.entity) - } - })) - } - if !isTopmost { - actions.append(ContextMenuAction(content: .text(title: strongSelf.presentationData.strings.Paint_MoveForward, accessibilityLabel: strongSelf.presentationData.strings.Paint_MoveForward), action: { [weak self, weak entityView] in - if let strongSelf = self, let entityView = entityView { - strongSelf.entitiesView.bringToFront(uuid: entityView.entity.uuid) - } - })) - } - actions.append(ContextMenuAction(content: .text(title: strongSelf.presentationData.strings.Paint_Duplicate, accessibilityLabel: strongSelf.presentationData.strings.Paint_Duplicate), action: { [weak self, weak entityView] in - if let strongSelf = self, let entityView = entityView { - let newEntity = strongSelf.entitiesView.duplicate(entityView.entity) - strongSelf.entitiesView.selectEntity(newEntity) - } - })) - let entityFrame = entityView.convert(entityView.selectionBounds, to: strongSelf.view).offsetBy(dx: 0.0, dy: -6.0) - let controller = ContextMenuController(actions: actions) - strongSelf.currentMenuController = controller - strongSelf.controller?.present( - controller, - in: .window(.root), - with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self] in - if let strongSelf = self { - return (strongSelf, entityFrame, strongSelf, strongSelf.bounds.insetBy(dx: 0.0, dy: 160.0)) - } else { - return nil - } - }) - ) - } self.insertEntity.connect { [weak self] entity in - if let strongSelf = self, let entitiesView = strongSelf._entitiesView { - entitiesView.prepareNewEntity(entity) - entitiesView.add(entity) - entitiesView.selectEntity(entity) - - if let entityView = entitiesView.getView(for: entity.uuid) { - if let textEntityView = entityView as? DrawingTextEntityView { - textEntityView.beginEditing(accessoryView: strongSelf.textEditAccessoryView) - } else { - entityView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - entityView.layer.animateScale(from: 0.1, to: entity.scale, duration: 0.2) - - if let selectionView = entityView.selectionView { - selectionView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, delay: 0.2) - } - } - } + if let self, let interaction = self.interaction { + interaction.insertEntity(entity) } } self.deselectEntity.connect { [weak self] in @@ -2355,10 +2308,7 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U self.presentationData = self.context.sharedContext.currentPresentationData.with { $0 } self.componentHost = ComponentView() - - self.textEditAccessoryView = UIInputView(frame: CGRect(origin: .zero, size: CGSize(width: 100.0, height: 44.0)), inputViewStyle: .keyboard) - self.textEditAccessoryHost = ComponentView() - + super.init() self.apply.connect { [weak self] _ in @@ -2397,198 +2347,48 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U self.view.disablesInteractiveKeyboardGestureRecognizer = true self.view.disablesInteractiveTransitionGestureRecognizer = true - } - - private var currentEyedropperView: EyedropperView? - func presentEyedropper(retryLaterForVideo: Bool = true, dismissed: @escaping () -> Void) { - guard let controller = self.controller else { - return - } - self.entitiesView.pause() - - if controller.isVideo && retryLaterForVideo { - controller.updateVideoPlayback(false) - Queue.mainQueue().after(0.1) { - self.presentEyedropper(retryLaterForVideo: false, dismissed: dismissed) - } - return - } - - guard let currentImage = controller.getCurrentImage() else { - self.entitiesView.play() - controller.updateVideoPlayback(true) - return - } - - let sourceImage = generateImage(controller.drawingView.imageSize, contextGenerator: { size, context in - let bounds = CGRect(origin: .zero, size: size) - if let cgImage = currentImage.cgImage { - context.draw(cgImage, in: bounds) - } - if let cgImage = controller.drawingView.drawingImage?.cgImage { - context.draw(cgImage, in: bounds) - } - context.translateBy(x: size.width / 2.0, y: size.height / 2.0) - context.scaleBy(x: 1.0, y: -1.0) - context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) - controller.entitiesView.layer.render(in: context) - }, opaque: true, scale: 1.0) - guard let sourceImage = sourceImage else { - return - } - - let eyedropperView = EyedropperView(containerSize: controller.contentWrapperView.frame.size, drawingView: controller.drawingView, sourceImage: sourceImage) - eyedropperView.completed = { [weak self, weak controller] color in - if let strongSelf = self, let controller = controller { - strongSelf.updateColor.invoke(color) - controller.entitiesView.play() - controller.updateVideoPlayback(true) - dismissed() - } - } - eyedropperView.dismissed = { - controller.entitiesView.play() - controller.updateVideoPlayback(true) - } - eyedropperView.frame = controller.contentWrapperView.convert(controller.contentWrapperView.bounds, to: controller.view) - controller.view.addSubview(eyedropperView) - self.currentEyedropperView = eyedropperView - } - - func dismissCurrentEyedropper() { - if let currentEyedropperView = self.currentEyedropperView { - self.currentEyedropperView = nil - currentEyedropperView.dismiss() - } - } - - private weak var colorPickerScreen: ColorPickerScreen? - func presentColorPicker(initialColor: DrawingColor, dismissed: @escaping () -> Void = {}) { - self.dismissCurrentEyedropper() - self.dismissFontPicker() guard let controller = self.controller else { return } - self.hapticFeedback.impact(.medium) - var didDismiss = false - let colorController = ColorPickerScreen(context: self.context, initialColor: initialColor, updated: { [weak self] color in - self?.updateColor.invoke(color) - }, openEyedropper: { [weak self] in - self?.presentEyedropper(dismissed: dismissed) - }, dismissed: { - if !didDismiss { - didDismiss = true - dismissed() - } - }) - controller.present(colorController, in: .window(.root)) - self.colorPickerScreen = colorController - } - - func dismissColorPicker() { - if let colorPickerScreen = self.colorPickerScreen { - self.colorPickerScreen = nil - colorPickerScreen.dismiss() - } - } - - private var fastColorPickerView: ColorSpectrumPickerView? - func presentFastColorPicker(sourceView: UIView) { - self.dismissCurrentEyedropper() - self.dismissFontPicker() - - guard self.fastColorPickerView == nil, let superview = sourceView.superview else { - return - } - - self.hapticFeedback.impact(.medium) - - let size = CGSize(width: min(350.0, superview.frame.width - 8.0 - 24.0), height: 296.0) - - let fastColorPickerView = ColorSpectrumPickerView(frame: CGRect(origin: CGPoint(x: sourceView.frame.minX + 5.0, y: sourceView.frame.maxY - size.height - 6.0), size: size)) - fastColorPickerView.selected = { [weak self] color in - self?.updateColor.invoke(color) - } - let _ = fastColorPickerView.updateLayout(size: size, selectedColor: nil) - sourceView.superview?.addSubview(fastColorPickerView) - - fastColorPickerView.animateIn() - - self.fastColorPickerView = fastColorPickerView - } - - func updateFastColorPickerPan(_ point: CGPoint) { - guard let fastColorPickerView = self.fastColorPickerView else { - return - } - fastColorPickerView.handlePan(point: point) - } - - func dismissFastColorPicker() { - guard let fastColorPickerView = self.fastColorPickerView else { - return - } - self.fastColorPickerView = nil - fastColorPickerView.animateOut(completion: { [weak fastColorPickerView] in - fastColorPickerView?.removeFromSuperview() - }) - } - - private weak var currentFontPicker: ContextController? - func presentFontPicker(sourceView: UIView) { - guard !self.dismissFontPicker(), let validLayout = self.validLayout?.0 else { - return - } - - if let entityView = self.entitiesView.selectedEntityView as? DrawingTextEntityView { - entityView.textChanged = { [weak self] in - self?.dismissFontPicker() - } - } - - let fonts: [DrawingTextFont] = [ - .sanFrancisco, - .other("AmericanTypewriter", "Typewriter"), - .other("AvenirNext-DemiBoldItalic", "Avenir Next"), - .other("CourierNewPS-BoldMT", "Courier New"), - .other("Noteworthy-Bold", "Noteworthy"), - .other("Georgia-Bold", "Georgia"), - .other("Papyrus", "Papyrus"), - .other("SnellRoundhand-Bold", "Snell Roundhand") - ] - - var items: [ContextMenuItem] = [] - for font in fonts { - items.append(.action(ContextMenuActionItem(text: font.title, textFont: .custom(font: font.uiFont(size: 17.0), height: 42.0, verticalOffset: font.title == "Noteworthy" ? -6.0 : nil), icon: { _ in return nil }, animationName: nil, action: { [weak self] f in - f.dismissWithResult(.default) - guard let strongSelf = self, let entityView = strongSelf.entitiesView.selectedEntityView as? DrawingTextEntityView, let textEntity = entityView.entity as? DrawingTextEntity else { - return + self.interaction = DrawingToolsInteraction( + context: self.context, + drawingView: self.drawingView, + entitiesView: self.entitiesView, + selectionContainerView: self.selectionContainerView, + isVideo: controller.isVideo, + updateSelectedEntity: { [weak self] entity in + if let self { + self.updateSelectedEntity.invoke(entity) } - textEntity.font = font.font - entityView.update() - - if let (layout, orientation) = strongSelf.validLayout { - strongSelf.containerLayoutUpdated(layout: layout, orientation: orientation, forceUpdate: true, transition: .easeInOut(duration: 0.2)) + }, + updateVideoPlayback: { [weak controller] isPlaying in + if let controller { + controller.updateVideoPlayback(isPlaying) } - }))) - } - - let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkPresentationTheme) - let contextController = ContextController(account: self.context.account, presentationData: presentationData, source: .reference(ReferenceContentSource(sourceView: sourceView, contentArea: CGRect(origin: .zero, size: CGSize(width: validLayout.size.width, height: validLayout.size.height - (validLayout.inputHeight ?? 0.0))), customPosition: CGPoint(x: 0.0, y: 1.0))), items: .single(ContextController.Items(content: .list(items)))) - self.controller?.present(contextController, in: .window(.root)) - self.currentFontPicker = contextController - contextController.view.disablesInteractiveKeyboardGestureRecognizer = true - } - - @discardableResult - func dismissFontPicker() -> Bool { - if let currentFontPicker = self.currentFontPicker { - self.currentFontPicker = nil - currentFontPicker.dismiss() - return true - } - return false + }, + updateColor: { [weak self] color in + if let self { + self.updateColor.invoke(color) + } + }, + getCurrentImage: { [weak controller] in + return controller?.getCurrentImage() + }, + getControllerNode: { [weak self] in + return self + }, + present: { [weak self] c, i, a in + if let self { + self.controller?.present(c, in: i, with: a) + } + }, + addSubview: { [weak self] view in + if let self { + self.view.addSubview(view) + } + } + ) } func animateIn() { @@ -2776,22 +2576,29 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U insertText: self.insertText, updateEntityView: self.updateEntityView, endEditingTextEntityView: self.endEditingTextEntityView, + entityViewForEntity: { [weak self] entity in + if let self, let entityView = self.entitiesView.getView(for: entity.uuid) { + return entityView + } else { + return nil + } + }, apply: self.apply, dismiss: self.dismiss, presentColorPicker: { [weak self] initialColor in - self?.presentColorPicker(initialColor: initialColor) + self?.interaction?.presentColorPicker(initialColor: initialColor) }, presentFastColorPicker: { [weak self] sourceView in - self?.presentFastColorPicker(sourceView: sourceView) + self?.interaction?.presentFastColorPicker(sourceView: sourceView) }, updateFastColorPickerPan: { [weak self] point in - self?.updateFastColorPickerPan(point) + self?.interaction?.updateFastColorPickerPan(point) }, dismissFastColorPicker: { [weak self] in - self?.dismissFastColorPicker() + self?.interaction?.dismissFastColorPicker() }, presentFontPicker: { [weak self] sourceView in - self?.presentFontPicker(sourceView: sourceView) + self?.interaction?.presentFontPicker(sourceView: sourceView) } ) ), @@ -2815,190 +2622,7 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U } } - if let entityView = self.entitiesView.selectedEntityView as? DrawingTextEntityView, let textEntity = entityView.entity as? DrawingTextEntity { - var isFirstTime = true - if let componentView = self.textEditAccessoryHost.view, componentView.superview != nil { - isFirstTime = false - } - UIView.performWithoutAnimation { - let accessorySize = self.textEditAccessoryHost.update( - transition: isFirstTime ? .immediate : .easeInOut(duration: 0.2), - component: AnyComponent( - TextSettingsComponent( - color: textEntity.color, - style: DrawingTextStyle(style: textEntity.style), - animation: DrawingTextAnimation(animation: textEntity.animation), - alignment: DrawingTextAlignment(alignment: textEntity.alignment), - font: DrawingTextFont(font: textEntity.font), - isEmojiKeyboard: entityView.textView.inputView != nil, - tag: nil, - fontTag: fontTag, - presentColorPicker: { [weak self] in - guard let strongSelf = self, let entityView = strongSelf.entitiesView.selectedEntityView as? DrawingTextEntityView, let textEntity = entityView.entity as? DrawingTextEntity else { - return - } - entityView.suspendEditing() - self?.presentColorPicker(initialColor: textEntity.color, dismissed: { - entityView.resumeEditing() - }) - }, - presentFastColorPicker: { [weak self] buttonTag in - if let buttonView = self?.textEditAccessoryHost.findTaggedView(tag: buttonTag) { - self?.presentFastColorPicker(sourceView: buttonView) - } - }, - updateFastColorPickerPan: { [weak self] point in - self?.updateFastColorPickerPan(point) - }, - dismissFastColorPicker: { [weak self] in - self?.dismissFastColorPicker() - }, - toggleStyle: { [weak self] in - self?.dismissFontPicker() - guard let strongSelf = self, let entityView = strongSelf.entitiesView.selectedEntityView as? DrawingTextEntityView, let textEntity = entityView.entity as? DrawingTextEntity else { - return - } - var nextStyle: DrawingTextEntity.Style - switch textEntity.style { - case .regular: - nextStyle = .filled - case .filled: - nextStyle = .semi - case .semi: - nextStyle = .stroke - case .stroke: - nextStyle = .regular - } - textEntity.style = nextStyle - entityView.update() - - if let (layout, orientation) = strongSelf.validLayout { - strongSelf.containerLayoutUpdated(layout: layout, orientation: orientation, transition: .immediate) - } - }, - toggleAnimation: { [weak self] in - self?.dismissFontPicker() - guard let strongSelf = self, let entityView = strongSelf.entitiesView.selectedEntityView as? DrawingTextEntityView, let textEntity = entityView.entity as? DrawingTextEntity else { - return - } - var nextAnimation: DrawingTextEntity.Animation - switch textEntity.animation { - case .none: - nextAnimation = .typing - case .typing: - nextAnimation = .wiggle - case .wiggle: - nextAnimation = .zoomIn - case .zoomIn: - nextAnimation = .none - } - textEntity.animation = nextAnimation - entityView.update() - - if let (layout, orientation) = strongSelf.validLayout { - strongSelf.containerLayoutUpdated(layout: layout, orientation: orientation, transition: .immediate) - } - }, - toggleAlignment: { [weak self] in - self?.dismissFontPicker() - guard let strongSelf = self, let entityView = strongSelf.entitiesView.selectedEntityView as? DrawingTextEntityView, let textEntity = entityView.entity as? DrawingTextEntity else { - return - } - var nextAlignment: DrawingTextEntity.Alignment - switch textEntity.alignment { - case .left: - nextAlignment = .center - case .center: - nextAlignment = .right - case .right: - nextAlignment = .left - } - textEntity.alignment = nextAlignment - entityView.update() - - if let (layout, orientation) = strongSelf.validLayout { - strongSelf.containerLayoutUpdated(layout: layout, orientation: orientation, transition: .immediate) - } - }, - presentFontPicker: { [weak self] in - if let buttonView = self?.textEditAccessoryHost.findTaggedView(tag: fontTag) { - self?.presentFontPicker(sourceView: buttonView) - } - }, - toggleKeyboard: { [weak self] in - guard let strongSelf = self else { - return - } - strongSelf.dismissFontPicker() - strongSelf.toggleInputMode() - } - ) - ), - environment: {}, - forceUpdate: true, - containerSize: CGSize(width: layout.size.width, height: 44.0) - ) - if let componentView = self.textEditAccessoryHost.view { - if componentView.superview == nil { - self.textEditAccessoryView.addSubview(componentView) - } - - self.textEditAccessoryView.frame = CGRect(origin: .zero, size: accessorySize) - componentView.frame = CGRect(origin: .zero, size: accessorySize) - } - } - } - } - - private func toggleInputMode() { - guard let entityView = self.entitiesView.selectedEntityView as? DrawingTextEntityView else { - return - } - - let textView = entityView.textView - var shouldHaveInputView = false - if textView.isFirstResponder { - if textView.inputView == nil { - shouldHaveInputView = true - } - } else { - shouldHaveInputView = true - } - - if shouldHaveInputView { - let inputView = EntityInputView( - context: self.context, - isDark: true, - areCustomEmojiEnabled: true, - hideBackground: true, - forceHasPremium: true - ) - inputView.insertText = { [weak entityView] text in - entityView?.insertText(text) - } - inputView.deleteBackwards = { [weak textView] in - textView?.deleteBackward() - } - inputView.switchToKeyboard = { [weak self] in - guard let strongSelf = self else { - return - } - strongSelf.toggleInputMode() - } - textView.inputView = inputView - } else { - textView.inputView = nil - } - - if textView.isFirstResponder { - textView.reloadInputViews() - } else { - textView.becomeFirstResponder() - } - - if let (layout, orientation) = self.validLayout { - self.containerLayoutUpdated(layout: layout, orientation: orientation, animateOut: false, transition: .immediate) - } + self.interaction?.containerLayoutUpdated(layout: layout, transition: transition) } } @@ -3178,35 +2802,12 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U self.node.animateOut(completion: { completion() }) - - Queue.mainQueue().after(0.4) { - self.node.isHidden = true - } +// +// Queue.mainQueue().after(0.4) { +// self.node.isHidden = true +// } } - - public override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) { - super.dismiss(animated: flag, completion: completion) - - self.node._drawingView?.entitiesView = nil - self.node._entitiesView?.drawingView = nil - self.node._entitiesView?.entityAdded = { _ in } - self.node._entitiesView?.entityRemoved = { _ in } - self.node._drawingView?.getFullImage = { return nil } - self.node._entitiesView?.selectionContainerView = nil - self.node._entitiesView?.selectionChanged = { _ in } - self.node._entitiesView?.requestedMenuForEntityView = { _, _ in } - self.node._drawingView = nil - self.node._entitiesView = nil - } - - public func presentStickerSelection() { - self.node.insertSticker.invoke(Void()) - } - - public func addTextEntity() { - self.node.insertText.invoke(Void()) - } - + private var orientation: UIInterfaceOrientation? override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) @@ -3271,3 +2872,539 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U //self.chatDisplayNode.updateDropInteraction(isActive: false) } } + +public final class DrawingToolsInteraction { + private let context: AccountContext + private let drawingView: DrawingView + private let entitiesView: DrawingEntitiesView + private let selectionContainerView: DrawingSelectionContainerView + private let isVideo: Bool + private let updateSelectedEntity: (DrawingEntity?) -> Void + private let updateVideoPlayback: (Bool) -> Void + private let updateColor: (DrawingColor) -> Void + + private let getCurrentImage: () -> UIImage? + private let getControllerNode: () -> ASDisplayNode? + private let present: (ViewController, PresentationContextType, Any?) -> Void + private let addSubview: (UIView) -> Void + + private let textEditAccessoryView: UIInputView + private let textEditAccessoryHost: ComponentView + + private var currentEyedropperView: EyedropperView? + private weak var currentMenuController: ContextMenuController? + + private let hapticFeedback = HapticFeedback() + + private var isActive = false + private var validLayout: ContainerViewLayout? + + public init( + context: AccountContext, + drawingView: DrawingView, + entitiesView: DrawingEntitiesView, + selectionContainerView: DrawingSelectionContainerView, + isVideo: Bool, + updateSelectedEntity: @escaping (DrawingEntity?) -> Void, + updateVideoPlayback: @escaping (Bool) -> Void, + updateColor: @escaping (DrawingColor) -> Void, + getCurrentImage: @escaping () -> UIImage?, + getControllerNode: @escaping () -> ASDisplayNode?, + present: @escaping (ViewController, PresentationContextType, Any?) -> Void, + addSubview: @escaping (UIView) -> Void + ) { + self.context = context + self.drawingView = drawingView + self.entitiesView = entitiesView + self.selectionContainerView = selectionContainerView + self.isVideo = isVideo + self.updateSelectedEntity = updateSelectedEntity + self.updateVideoPlayback = updateVideoPlayback + self.updateColor = updateColor + self.getCurrentImage = getCurrentImage + self.getControllerNode = getControllerNode + self.present = present + self.addSubview = addSubview + + self.textEditAccessoryView = UIInputView(frame: CGRect(origin: .zero, size: CGSize(width: 100.0, height: 44.0)), inputViewStyle: .keyboard) + self.textEditAccessoryHost = ComponentView() + + self.activate() + } + + func activate() { + self.isActive = true + + self.entitiesView.selectionContainerView = self.selectionContainerView + self.entitiesView.selectionChanged = { [weak self] entity in + if let self { + self.updateSelectedEntity(entity) + } + } + + self.entitiesView.requestedMenuForEntityView = { [weak self] entityView, isTopmost in + guard let self, let node = self.getControllerNode() else { + return + } + if self.currentMenuController != nil { + if let entityView = entityView as? DrawingTextEntityView { + entityView.beginEditing(accessoryView: self.textEditAccessoryView) + } + return + } + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkPresentationTheme) + var actions: [ContextMenuAction] = [] + actions.append(ContextMenuAction(content: .text(title: presentationData.strings.Paint_Delete, accessibilityLabel: presentationData.strings.Paint_Delete), action: { [weak self, weak entityView] in + if let self, let entityView { + self.entitiesView.remove(uuid: entityView.entity.uuid, animated: true) + } + })) + if let entityView = entityView as? DrawingTextEntityView { + actions.append(ContextMenuAction(content: .text(title: presentationData.strings.Paint_Edit, accessibilityLabel: presentationData.strings.Paint_Edit), action: { [weak self, weak entityView] in + if let self, let entityView { + entityView.beginEditing(accessoryView: self.textEditAccessoryView) + self.entitiesView.selectEntity(entityView.entity) + } + })) + } + if !isTopmost { + actions.append(ContextMenuAction(content: .text(title: presentationData.strings.Paint_MoveForward, accessibilityLabel: presentationData.strings.Paint_MoveForward), action: { [weak self, weak entityView] in + if let self, let entityView { + self.entitiesView.bringToFront(uuid: entityView.entity.uuid) + } + })) + } + actions.append(ContextMenuAction(content: .text(title: presentationData.strings.Paint_Duplicate, accessibilityLabel: presentationData.strings.Paint_Duplicate), action: { [weak self, weak entityView] in + if let self, let entityView { + let newEntity = self.entitiesView.duplicate(entityView.entity) + self.entitiesView.selectEntity(newEntity) + } + })) + let entityFrame = entityView.convert(entityView.selectionBounds, to: node.view).offsetBy(dx: 0.0, dy: -6.0) + let controller = ContextMenuController(actions: actions) + let bounds = node.bounds.insetBy(dx: 0.0, dy: 160.0) + self.present( + controller, + .window(.root), + ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak node] in + if let node { + return (node, entityFrame, node, bounds) + } else { + return nil + } + }) + ) + self.currentMenuController = controller + } + } + + public func deactivate() { + self.isActive = false + } + + public func insertEntity(_ entity: DrawingEntity) { + self.entitiesView.prepareNewEntity(entity) + self.entitiesView.add(entity) + self.entitiesView.selectEntity(entity) + + if let entityView = self.entitiesView.getView(for: entity.uuid) { + if let textEntityView = entityView as? DrawingTextEntityView { + textEntityView.beginEditing(accessoryView: self.textEditAccessoryView) + } else { + entityView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + entityView.layer.animateScale(from: 0.1, to: entity.scale, duration: 0.2) + + if let selectionView = entityView.selectionView { + selectionView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, delay: 0.2) + } + } + } + } + + func presentEyedropper(retryLaterForVideo: Bool = true, dismissed: @escaping () -> Void) { +// self.entitiesView.pause() +// +// if self.isVideo && retryLaterForVideo { +// self.updateVideoPlayback(false) +// Queue.mainQueue().after(0.1) { +// self.presentEyedropper(retryLaterForVideo: false, dismissed: dismissed) +// } +// return +// } +// +// guard let currentImage = self.getCurrentImage() else { +// self.entitiesView.play() +// self.updateVideoPlayback(true) +// return +// } +// +// let sourceImage = generateImage(self.drawingView.imageSize, contextGenerator: { size, context in +// let bounds = CGRect(origin: .zero, size: size) +// if let cgImage = currentImage.cgImage { +// context.draw(cgImage, in: bounds) +// } +// if let cgImage = self.drawingView.drawingImage?.cgImage { +// context.draw(cgImage, in: bounds) +// } +// context.translateBy(x: size.width / 2.0, y: size.height / 2.0) +// context.scaleBy(x: 1.0, y: -1.0) +// context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) +// self.entitiesView.layer.render(in: context) +// }, opaque: true, scale: 1.0) +// guard let sourceImage = sourceImage else { +// return +// } +// +// let eyedropperView = EyedropperView(containerSize: controller.contentWrapperView.frame.size, drawingView: self.drawingView, sourceImage: sourceImage) +// eyedropperView.completed = { [weak self] color in +// if let self { +// self.updateColor(color) +// self.entitiesView.play() +// self.updateVideoPlayback(true) +// +// dismissed() +// } +// } +// eyedropperView.dismissed = { [weak self] in +// if let self { +// self.entitiesView.play() +// self.updateVideoPlayback(true) +// } +// } +// eyedropperView.frame = controller.contentWrapperView.convert(controller.contentWrapperView.bounds, to: controller.view) +// self.addSubview(eyedropperView) +// self.currentEyedropperView = eyedropperView + } + + func dismissCurrentEyedropper() { + if let currentEyedropperView = self.currentEyedropperView { + self.currentEyedropperView = nil + currentEyedropperView.dismiss() + } + } + + weak var colorPickerScreen: ColorPickerScreen? + func presentColorPicker(initialColor: DrawingColor, dismissed: @escaping () -> Void = {}) { + self.dismissCurrentEyedropper() + self.dismissFontPicker() + + self.hapticFeedback.impact(.medium) + var didDismiss = false + let colorController = ColorPickerScreen(context: self.context, initialColor: initialColor, updated: { [weak self] color in + if let self { + self.updateColor(color) + } + }, openEyedropper: { [weak self] in + if let self { + self.presentEyedropper(dismissed: dismissed) + } + }, dismissed: { + if !didDismiss { + didDismiss = true + dismissed() + } + }) + self.present(colorController, .window(.root), nil) + self.colorPickerScreen = colorController + } + + func dismissColorPicker() { + if let colorPickerScreen = self.colorPickerScreen { + self.colorPickerScreen = nil + colorPickerScreen.dismiss() + } + } + + private var fastColorPickerView: ColorSpectrumPickerView? + func presentFastColorPicker(sourceView: UIView) { + self.dismissCurrentEyedropper() + self.dismissFontPicker() + + guard self.fastColorPickerView == nil, let superview = sourceView.superview else { + return + } + + self.hapticFeedback.impact(.medium) + + let size = CGSize(width: min(350.0, superview.frame.width - 8.0 - 24.0), height: 296.0) + + let fastColorPickerView = ColorSpectrumPickerView(frame: CGRect(origin: CGPoint(x: sourceView.frame.minX + 5.0, y: sourceView.frame.maxY - size.height - 6.0), size: size)) + fastColorPickerView.selected = { [weak self] color in + if let self { + self.updateColor(color) + } + } + let _ = fastColorPickerView.updateLayout(size: size, selectedColor: nil) + sourceView.superview?.addSubview(fastColorPickerView) + + fastColorPickerView.animateIn() + + self.fastColorPickerView = fastColorPickerView + } + + func updateFastColorPickerPan(_ point: CGPoint) { + guard let fastColorPickerView = self.fastColorPickerView else { + return + } + fastColorPickerView.handlePan(point: point) + } + + func dismissFastColorPicker() { + guard let fastColorPickerView = self.fastColorPickerView else { + return + } + self.fastColorPickerView = nil + fastColorPickerView.animateOut(completion: { [weak fastColorPickerView] in + fastColorPickerView?.removeFromSuperview() + }) + } + + private weak var currentFontPicker: ContextController? + func presentFontPicker(sourceView: UIView) { + guard !self.dismissFontPicker(), let validLayout = self.validLayout else { + return + } + + if let entityView = self.entitiesView.selectedEntityView as? DrawingTextEntityView { + entityView.textChanged = { [weak self] in + self?.dismissFontPicker() + } + } + + let fonts: [DrawingTextFont] = [ + .sanFrancisco, + .other("AmericanTypewriter", "Typewriter"), + .other("AvenirNext-DemiBoldItalic", "Avenir Next"), + .other("CourierNewPS-BoldMT", "Courier New"), + .other("Noteworthy-Bold", "Noteworthy"), + .other("Georgia-Bold", "Georgia"), + .other("Papyrus", "Papyrus"), + .other("SnellRoundhand-Bold", "Snell Roundhand") + ] + + var items: [ContextMenuItem] = [] + for font in fonts { + items.append(.action(ContextMenuActionItem(text: font.title, textFont: .custom(font: font.uiFont(size: 17.0), height: 42.0, verticalOffset: font.title == "Noteworthy" ? -6.0 : nil), icon: { _ in return nil }, animationName: nil, action: { [weak self] f in + f.dismissWithResult(.default) + guard let strongSelf = self, let entityView = strongSelf.entitiesView.selectedEntityView as? DrawingTextEntityView, let textEntity = entityView.entity as? DrawingTextEntity else { + return + } + textEntity.font = font.font + entityView.update() + + if let layout = strongSelf.validLayout { + strongSelf.containerLayoutUpdated(layout: layout, transition: .easeInOut(duration: 0.2)) + } + }))) + } + + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkPresentationTheme) + let contextController = ContextController(account: self.context.account, presentationData: presentationData, source: .reference(ReferenceContentSource(sourceView: sourceView, contentArea: CGRect(origin: .zero, size: CGSize(width: validLayout.size.width, height: validLayout.size.height - (validLayout.inputHeight ?? 0.0))), customPosition: CGPoint(x: 0.0, y: 1.0))), items: .single(ContextController.Items(content: .list(items)))) + self.present(contextController, .window(.root), nil) + self.currentFontPicker = contextController + contextController.view.disablesInteractiveKeyboardGestureRecognizer = true + } + + @discardableResult + func dismissFontPicker() -> Bool { + if let currentFontPicker = self.currentFontPicker { + self.currentFontPicker = nil + currentFontPicker.dismiss() + return true + } + return false + } + + private func toggleInputMode() { + guard let entityView = self.entitiesView.selectedEntityView as? DrawingTextEntityView else { + return + } + + let textView = entityView.textView + var shouldHaveInputView = false + if textView.isFirstResponder { + if textView.inputView == nil { + shouldHaveInputView = true + } + } else { + shouldHaveInputView = true + } + + if shouldHaveInputView { + let inputView = EntityInputView( + context: self.context, + isDark: true, + areCustomEmojiEnabled: true, + hideBackground: true, + forceHasPremium: true + ) + inputView.insertText = { [weak entityView] text in + entityView?.insertText(text) + } + inputView.deleteBackwards = { [weak textView] in + textView?.deleteBackward() + } + inputView.switchToKeyboard = { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.toggleInputMode() + } + textView.inputView = inputView + } else { + textView.inputView = nil + } + + if textView.isFirstResponder { + textView.reloadInputViews() + } else { + textView.becomeFirstResponder() + } + + if let layout = self.validLayout { + self.containerLayoutUpdated(layout: layout, transition: .immediate) + } + } + + public func containerLayoutUpdated(layout: ContainerViewLayout, transition: Transition) { + self.validLayout = layout + + guard self.isActive else { + return + } + + if let entityView = self.entitiesView.selectedEntityView as? DrawingTextEntityView, let textEntity = entityView.entity as? DrawingTextEntity { + var isFirstTime = true + if let componentView = self.textEditAccessoryHost.view, componentView.superview != nil { + isFirstTime = false + } + UIView.performWithoutAnimation { + let accessorySize = self.textEditAccessoryHost.update( + transition: isFirstTime ? .immediate : .easeInOut(duration: 0.2), + component: AnyComponent( + TextSettingsComponent( + color: textEntity.color, + style: DrawingTextStyle(style: textEntity.style), + animation: DrawingTextAnimation(animation: textEntity.animation), + alignment: DrawingTextAlignment(alignment: textEntity.alignment), + font: DrawingTextFont(font: textEntity.font), + isEmojiKeyboard: entityView.textView.inputView != nil, + tag: nil, + fontTag: fontTag, + presentColorPicker: { [weak self] in + guard let strongSelf = self, let entityView = strongSelf.entitiesView.selectedEntityView as? DrawingTextEntityView, let textEntity = entityView.entity as? DrawingTextEntity else { + return + } + entityView.suspendEditing() + self?.presentColorPicker(initialColor: textEntity.color, dismissed: { + entityView.resumeEditing() + }) + }, + presentFastColorPicker: { [weak self] buttonTag in + if let buttonView = self?.textEditAccessoryHost.findTaggedView(tag: buttonTag) { + self?.presentFastColorPicker(sourceView: buttonView) + } + }, + updateFastColorPickerPan: { [weak self] point in + self?.updateFastColorPickerPan(point) + }, + dismissFastColorPicker: { [weak self] in + self?.dismissFastColorPicker() + }, + toggleStyle: { [weak self] in + self?.dismissFontPicker() + guard let strongSelf = self, let entityView = strongSelf.entitiesView.selectedEntityView as? DrawingTextEntityView, let textEntity = entityView.entity as? DrawingTextEntity else { + return + } + var nextStyle: DrawingTextEntity.Style + switch textEntity.style { + case .regular: + nextStyle = .filled + case .filled: + nextStyle = .semi + case .semi: + nextStyle = .stroke + case .stroke: + nextStyle = .regular + } + textEntity.style = nextStyle + entityView.update() + + if let layout = strongSelf.validLayout { + strongSelf.containerLayoutUpdated(layout: layout, transition: .immediate) + } + }, + toggleAnimation: { [weak self] in + self?.dismissFontPicker() + guard let strongSelf = self, let entityView = strongSelf.entitiesView.selectedEntityView as? DrawingTextEntityView, let textEntity = entityView.entity as? DrawingTextEntity else { + return + } + var nextAnimation: DrawingTextEntity.Animation + switch textEntity.animation { + case .none: + nextAnimation = .typing + case .typing: + nextAnimation = .wiggle + case .wiggle: + nextAnimation = .zoomIn + case .zoomIn: + nextAnimation = .none + } + textEntity.animation = nextAnimation + entityView.update() + + if let layout = strongSelf.validLayout { + strongSelf.containerLayoutUpdated(layout: layout, transition: .immediate) + } + }, + toggleAlignment: { [weak self] in + self?.dismissFontPicker() + guard let strongSelf = self, let entityView = strongSelf.entitiesView.selectedEntityView as? DrawingTextEntityView, let textEntity = entityView.entity as? DrawingTextEntity else { + return + } + var nextAlignment: DrawingTextEntity.Alignment + switch textEntity.alignment { + case .left: + nextAlignment = .center + case .center: + nextAlignment = .right + case .right: + nextAlignment = .left + } + textEntity.alignment = nextAlignment + entityView.update() + + if let layout = strongSelf.validLayout { + strongSelf.containerLayoutUpdated(layout: layout, transition: .immediate) + } + }, + presentFontPicker: { [weak self] in + if let buttonView = self?.textEditAccessoryHost.findTaggedView(tag: fontTag) { + self?.presentFontPicker(sourceView: buttonView) + } + }, + toggleKeyboard: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.dismissFontPicker() + strongSelf.toggleInputMode() + } + ) + ), + environment: {}, + forceUpdate: true, + containerSize: CGSize(width: layout.size.width, height: 44.0) + ) + if let componentView = self.textEditAccessoryHost.view { + if componentView.superview == nil { + self.textEditAccessoryView.addSubview(componentView) + } + + self.textEditAccessoryView.frame = CGRect(origin: .zero, size: accessorySize) + componentView.frame = CGRect(origin: .zero, size: accessorySize) + } + } + } + } +} diff --git a/submodules/DrawingUI/Sources/EyedropperView.swift b/submodules/DrawingUI/Sources/EyedropperView.swift index e38470da8c..62ebd7e774 100644 --- a/submodules/DrawingUI/Sources/EyedropperView.swift +++ b/submodules/DrawingUI/Sources/EyedropperView.swift @@ -38,7 +38,7 @@ private func generateGridImage(size: CGSize, light: Bool) -> UIImage? { }) } -final class EyedropperView: UIView { +public final class EyedropperView: UIView { private weak var drawingView: DrawingView? private let containerView: UIView diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift index 306e91861c..d58748b840 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift @@ -872,7 +872,7 @@ public class CameraScreen: ViewController { } else { if translation.x < -10.0 { - let transitionFraction = 1.0 - abs(translation.x) / self.frame.width + let transitionFraction = 1.0 - max(0.0, translation.x * -1.0) / self.frame.width controller.updateTransitionProgress(transitionFraction, transition: .immediate) } else if translation.y < -10.0 { controller.presentGallery() @@ -882,7 +882,7 @@ public class CameraScreen: ViewController { } case .ended: let velocity = gestureRecognizer.velocity(in: self.view) - let transitionFraction = 1.0 - abs(translation.x) / self.frame.width + let transitionFraction = 1.0 - max(0.0, translation.x * -1.0) / self.frame.width controller.completeWithTransitionProgress(transitionFraction, velocity: abs(velocity.x), dismissing: true) default: break @@ -982,26 +982,28 @@ public class CameraScreen: ViewController { } func resumeCameraCapture() { - if let snapshot = self.simplePreviewView?.snapshotView(afterScreenUpdates: false) { - self.simplePreviewView?.addSubview(snapshot) - self.previewSnapshotView = snapshot - } - self.simplePreviewView?.isEnabled = true - self.camera.startCapture() - - if #available(iOS 13.0, *), let isPreviewing = self.simplePreviewView?.isPreviewing { - let _ = (isPreviewing - |> filter { - $0 + if self.simplePreviewView?.isEnabled == false { + if let snapshot = self.simplePreviewView?.snapshotView(afterScreenUpdates: false) { + self.simplePreviewView?.addSubview(snapshot) + self.previewSnapshotView = snapshot } - |> take(1)).start(next: { [weak self] _ in - if let self { + self.simplePreviewView?.isEnabled = true + self.camera.startCapture() + + if #available(iOS 13.0, *), let isPreviewing = self.simplePreviewView?.isPreviewing { + let _ = (isPreviewing + |> filter { + $0 + } + |> take(1)).start(next: { [weak self] _ in + if let self { + self.previewBlurPromise.set(false) + } + }) + } else { + Queue.mainQueue().after(1.0) { self.previewBlurPromise.set(false) } - }) - } else { - Queue.mainQueue().after(1.0) { - self.previewBlurPromise.set(false) } } } @@ -1344,7 +1346,7 @@ public class CameraScreen: ViewController { private var isTransitioning = false public func updateTransitionProgress(_ transitionFraction: CGFloat, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void = {}) { self.isTransitioning = true - let offsetX = (1.0 - transitionFraction) * self.node.frame.width * -1.0 + let offsetX = floorToScreenPixels((1.0 - transitionFraction) * self.node.frame.width * -1.0) transition.updateTransform(layer: self.node.backgroundView.layer, transform: CGAffineTransform(translationX: offsetX, y: 0.0)) transition.updateTransform(layer: self.node.containerView.layer, transform: CGAffineTransform(translationX: offsetX, y: 0.0)) let scale = max(0.8, min(1.0, 0.8 + 0.2 * transitionFraction)) @@ -1359,7 +1361,7 @@ public class CameraScreen: ViewController { self.statusBar.updateStatusBarStyle(transitionFraction > 0.45 ? .White : .Ignore, animated: true) if let navigationController = self.navigationController as? NavigationController { - let offsetX = transitionFraction * self.node.frame.width + let offsetX = floorToScreenPixels(transitionFraction * self.node.frame.width) navigationController.updateRootContainerTransitionOffset(offsetX, transition: transition) } } @@ -1367,7 +1369,7 @@ public class CameraScreen: ViewController { public func completeWithTransitionProgress(_ transitionFraction: CGFloat, velocity: CGFloat, dismissing: Bool) { self.isTransitioning = false if dismissing { - if transitionFraction < 0.7 || velocity > 1000.0 { + if transitionFraction < 0.7 || velocity < -1000.0 { self.requestDismiss(animated: true, interactive: true) } else { self.updateTransitionProgress(1.0, transition: .animated(duration: 0.4, curve: .spring), completion: { [weak self] in diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift index a61e8ad0d7..74692b08db 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift @@ -464,6 +464,10 @@ public final class MediaEditor { } } + public func play() { + self.player?.play() + } + public func stop() { self.player?.pause() } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposer.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposer.swift index ccc6a9e539..e3d1fd400d 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposer.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposer.swift @@ -85,46 +85,35 @@ final class MediaEditorComposer { self.renderChain.update(values: self.values) } - func processSampleBuffer(_ sampleBuffer: CMSampleBuffer, pool: CVPixelBufferPool?, completion: @escaping (CVPixelBuffer?) -> Void) { - guard let textureCache = self.textureCache, let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer), let pool = pool else { + func processSampleBuffer(_ sampleBuffer: CMSampleBuffer, pool: CVPixelBufferPool?, textureRotation: TextureRotation, completion: @escaping (CVPixelBuffer?) -> Void) { + guard let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer), let pool = pool else { completion(nil) return } let time = CMSampleBufferGetPresentationTimeStamp(sampleBuffer) - let width = CVPixelBufferGetWidth(imageBuffer) - let height = CVPixelBufferGetHeight(imageBuffer) - let format: MTLPixelFormat = .bgra8Unorm - var textureRef : CVMetalTexture? - let status = CVMetalTextureCacheCreateTextureFromImage(nil, textureCache, imageBuffer, nil, format, width, height, 0, &textureRef) - var texture: MTLTexture? - if status == kCVReturnSuccess { - texture = CVMetalTextureGetTexture(textureRef!) - } - if let texture { - self.renderer.consumeTexture(texture) - self.renderer.renderFrame() + self.renderer.consumeVideoPixelBuffer(imageBuffer, rotation: textureRotation) + self.renderer.renderFrame() + + if let finalTexture = self.renderer.finalTexture, var ciImage = CIImage(mtlTexture: finalTexture, options: [.colorSpace: self.colorSpace]) { + ciImage = ciImage.transformed(by: CGAffineTransformMakeScale(1.0, -1.0).translatedBy(x: 0.0, y: -ciImage.extent.height)) - if let finalTexture = self.renderer.finalTexture, var ciImage = CIImage(mtlTexture: finalTexture, options: [.colorSpace: self.colorSpace]) { - ciImage = ciImage.transformed(by: CGAffineTransformMakeScale(1.0, -1.0).translatedBy(x: 0.0, y: -ciImage.extent.height)) - - var pixelBuffer: CVPixelBuffer? - CVPixelBufferPoolCreatePixelBuffer(kCFAllocatorDefault, pool, &pixelBuffer) - - if let pixelBuffer { - processImage(inputImage: ciImage, time: time, completion: { compositedImage in - if var compositedImage { - let scale = self.outputDimensions.width / self.dimensions.width - compositedImage = compositedImage.transformed(by: CGAffineTransform(scaleX: scale, y: scale)) - - self.ciContext?.render(compositedImage, to: pixelBuffer) - completion(pixelBuffer) - } else { - completion(nil) - } - }) - return - } + var pixelBuffer: CVPixelBuffer? + CVPixelBufferPoolCreatePixelBuffer(kCFAllocatorDefault, pool, &pixelBuffer) + + if let pixelBuffer { + processImage(inputImage: ciImage, time: time, completion: { compositedImage in + if var compositedImage { + let scale = self.outputDimensions.width / self.dimensions.width + compositedImage = compositedImage.transformed(by: CGAffineTransform(scaleX: scale, y: scale)) + + self.ciContext?.render(compositedImage, to: pixelBuffer) + completion(pixelBuffer) + } else { + completion(nil) + } + }) + return } } completion(nil) diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorPreviewView.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorPreviewView.swift index c3c2de83ae..1e8cc11c16 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorPreviewView.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorPreviewView.swift @@ -50,7 +50,7 @@ public final class MediaEditorPreviewView: MTKView, MTKViewDelegate, RenderTarge } func scheduleFrame() { - Queue.mainQueue().async { + Queue.mainQueue().justDispatch { self.draw() } } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoExport.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoExport.swift index 0337bed811..2e5ca44e09 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoExport.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoExport.swift @@ -247,10 +247,7 @@ public final class MediaEditorVideoExport { private let subject: Subject private let configuration: Configuration private let outputPath: String - - private var previousSampleTime: CMTime = .zero - private var processedPixelBuffer: CVPixelBuffer? - + private var reader: AVAssetReader? private var videoOutput: AVAssetReaderOutput? @@ -260,6 +257,7 @@ public final class MediaEditorVideoExport { private var writer: MediaEditorVideoExportWriter? private var composer: MediaEditorComposer? + private var textureRotation: TextureRotation = .rotate0Degrees private let duration = ValuePromise() private let pauseDispatchGroup = DispatchGroup() @@ -320,16 +318,23 @@ public final class MediaEditorVideoExport { return } + self.textureRotation = textureRotatonForAVAsset(asset) + writer.setup(configuration: self.configuration, outputPath: self.outputPath) let videoTracks = asset.tracks(withMediaType: .video) if (videoTracks.count > 0) { - let outputSettings: [String : Any] var sourceFrameRate: Float = 0.0 + let outputSettings: [String: Any] = [ + kCVPixelBufferPixelFormatTypeKey as String: [kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange], + AVVideoColorPropertiesKey: [ + AVVideoColorPrimariesKey: AVVideoColorPrimaries_ITU_R_709_2, + AVVideoTransferFunctionKey: AVVideoTransferFunction_ITU_R_709_2, + AVVideoYCbCrMatrixKey: AVVideoYCbCrMatrix_ITU_R_709_2 + ] + ] if let videoTrack = videoTracks.first, videoTrack.preferredTransform.isIdentity && !self.configuration.values.requiresComposing { - outputSettings = [kCVPixelBufferPixelFormatTypeKey as String: [kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange]] } else { - outputSettings = [kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA] self.setupComposer() } let videoOutput = AVAssetReaderTrackOutput(track: videoTracks.first!, outputSettings: outputSettings) @@ -516,7 +521,7 @@ public final class MediaEditorVideoExport { if let buffer = output.copyNextSampleBuffer() { if let composer = self.composer { let timestamp = CMSampleBufferGetPresentationTimeStamp(buffer) - composer.processSampleBuffer(buffer, pool: writer.pixelBufferPool, completion: { pixelBuffer in + composer.processSampleBuffer(buffer, pool: writer.pixelBufferPool, textureRotation: self.textureRotation, completion: { pixelBuffer in if let pixelBuffer { if !writer.appendPixelBuffer(pixelBuffer, at: timestamp) { writer.markVideoAsFinished() diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/VideoTextureSource.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/VideoTextureSource.swift index 33c009a189..f8f2e18558 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/VideoTextureSource.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/VideoTextureSource.swift @@ -3,6 +3,28 @@ import AVFoundation import Metal import MetalKit +func textureRotatonForAVAsset(_ asset: AVAsset) -> TextureRotation { + for track in asset.tracks { + if track.mediaType == .video { + let t = track.preferredTransform + if t.a == -1.0 && t.d == -1.0 { + return .rotate180Degrees + } else if t.a == 1.0 && t.d == 1.0 { + return .rotate0Degrees + } else if t.b == -1.0 && t.c == 1.0 { + return .rotate270Degrees + } else if t.a == -1.0 && t.d == 1.0 { + return .rotate270Degrees + } else if t.a == 1.0 && t.d == -1.0 { + return .rotate180Degrees + } else { + return .rotate90Degrees + } + } + } + return .rotate0Degrees +} + final class VideoTextureSource: NSObject, TextureSource, AVPlayerItemOutputPullDelegate { private let player: AVPlayer private var playerItem: AVPlayerItem? @@ -80,23 +102,10 @@ final class VideoTextureSource: NSObject, TextureSource, AVPlayerItemOutputPullD for track in playerItem.asset.tracks { if track.mediaType == .video { hasVideoTrack = true - - let t = track.preferredTransform - if t.a == -1.0 && t.d == -1.0 { - self.textureRotation = .rotate180Degrees - } else if t.a == 1.0 && t.d == 1.0 { - self.textureRotation = .rotate0Degrees - } else if t.b == -1.0 && t.c == 1.0 { - self.textureRotation = .rotate270Degrees - } else if t.a == -1.0 && t.d == 1.0 { - self.textureRotation = .rotate270Degrees - } else if t.a == 1.0 && t.d == -1.0 { - self.textureRotation = .rotate180Degrees - } else { - self.textureRotation = .rotate90Degrees - } + break } } + self.textureRotation = textureRotatonForAVAsset(playerItem.asset) if !hasVideoTrack { assertionFailure("No video track found.") return diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 4a3866b39e..85ab785831 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -496,7 +496,7 @@ final class MediaEditorScreenComponent: Component { containerSize: CGSize(width: 40.0, height: 40.0) ) let drawButtonFrame = CGRect( - origin: CGPoint(x: floorToScreenPixels(availableSize.width / 4.0 - 3.0 - drawButtonSize.width / 2.0), y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset), + origin: CGPoint(x: floorToScreenPixels(availableSize.width / 4.0 - 3.0 - drawButtonSize.width / 2.0), y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset + 1.0), size: drawButtonSize ) if let drawButtonView = self.drawButton.view { @@ -521,7 +521,7 @@ final class MediaEditorScreenComponent: Component { containerSize: CGSize(width: 40.0, height: 40.0) ) let textButtonFrame = CGRect( - origin: CGPoint(x: floorToScreenPixels(availableSize.width / 2.5 + 5.0 - textButtonSize.width / 2.0), y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset), + origin: CGPoint(x: floorToScreenPixels(availableSize.width / 2.5 + 5.0 - textButtonSize.width / 2.0), y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset + 1.0), size: textButtonSize ) if let textButtonView = self.textButton.view { @@ -546,7 +546,7 @@ final class MediaEditorScreenComponent: Component { containerSize: CGSize(width: 40.0, height: 40.0) ) let stickerButtonFrame = CGRect( - origin: CGPoint(x: floorToScreenPixels(availableSize.width - availableSize.width / 2.5 - 5.0 - stickerButtonSize.width / 2.0), y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset), + origin: CGPoint(x: floorToScreenPixels(availableSize.width - availableSize.width / 2.5 - 5.0 - stickerButtonSize.width / 2.0), y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset + 1.0), size: stickerButtonSize ) if let stickerButtonView = self.stickerButton.view { @@ -571,7 +571,7 @@ final class MediaEditorScreenComponent: Component { containerSize: CGSize(width: 40.0, height: 40.0) ) let toolsButtonFrame = CGRect( - origin: CGPoint(x: floorToScreenPixels(availableSize.width / 4.0 * 3.0 + 3.0 - toolsButtonSize.width / 2.0), y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset), + origin: CGPoint(x: floorToScreenPixels(availableSize.width / 4.0 * 3.0 + 3.0 - toolsButtonSize.width / 2.0), y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset + 1.0), size: toolsButtonSize ) if let toolsButtonView = self.toolsButton.view { @@ -592,24 +592,19 @@ final class MediaEditorScreenComponent: Component { context: component.context, duration: playerState.duration, startPosition: playerState.timeRange?.lowerBound ?? 0.0, - endPosition: playerState.timeRange?.upperBound ?? playerState.duration, + endPosition: playerState.timeRange?.upperBound ?? min(playerState.duration, storyMaxVideoDuration), position: playerState.position, + maxDuration: storyMaxVideoDuration, frames: playerState.frames, framesUpdateTimestamp: playerState.framesUpdateTimestamp, - startPositionUpdated: { [weak mediaEditor] position, done in + trimUpdated: { [weak mediaEditor] start, end, updatedEnd, done in if let mediaEditor { - mediaEditor.setVideoTrimStart(position) - mediaEditor.seek(position, andPlay: done) - } - }, - endPositionUpdated: { [weak mediaEditor] position, done in - if let mediaEditor { - mediaEditor.setVideoTrimEnd(position) + mediaEditor.setVideoTrimStart(start) + mediaEditor.setVideoTrimEnd(end) if done { - let start = mediaEditor.values.videoTrimRange?.lowerBound ?? 0.0 mediaEditor.seek(start, andPlay: true) } else { - mediaEditor.seek(position, andPlay: false) + mediaEditor.seek(updatedEnd ? end : start, andPlay: false) } } }, @@ -730,7 +725,7 @@ final class MediaEditorScreenComponent: Component { } case let .message(peerIds, _): if peerIds.count == 1 { - privacyText = "User Test" + privacyText = "1 Recipient" } else { privacyText = "\(peerIds.count) Recipients" } @@ -871,6 +866,7 @@ final class MediaEditorScreenComponent: Component { } private let storyDimensions = CGSize(width: 1080.0, height: 1920.0) +private let storyMaxVideoDuration: Double = 60.0 public enum MediaEditorResultPrivacy: Equatable { case story(privacy: EngineStoryPrivacy, archive: Bool) @@ -928,6 +924,7 @@ public final class MediaEditorScreen: ViewController { fileprivate final class Node: ViewControllerTracingNode, UIGestureRecognizerDelegate { private weak var controller: MediaEditorScreen? private let context: AccountContext + private var interaction: DrawingToolsInteraction? private let initializationTimestamp = CACurrentMediaTime() fileprivate var subject: MediaEditorScreen.Subject? @@ -1162,6 +1159,51 @@ public final class MediaEditorScreen: ViewController { let rotateGestureRecognizer = UIRotationGestureRecognizer(target: self, action: #selector(self.handleRotate(_:))) rotateGestureRecognizer.delegate = self self.previewContainerView.addGestureRecognizer(rotateGestureRecognizer) + + let tapGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.handleTap(_:))) + self.previewContainerView.addGestureRecognizer(tapGestureRecognizer) + + self.interaction = DrawingToolsInteraction( + context: self.context, + drawingView: self.drawingView, + entitiesView: self.entitiesView, + selectionContainerView: self.selectionContainerView, + isVideo: false, + updateSelectedEntity: { _ in + + }, + updateVideoPlayback: { [weak self] isPlaying in + if let self, let mediaEditor = self.mediaEditor { + if isPlaying { + mediaEditor.play() + } else { + mediaEditor.stop() + } + } + }, + updateColor: { [weak self] color in + if let self, let selectedEntityView = self.entitiesView.selectedEntityView { + selectedEntityView.entity.color = color + selectedEntityView.update(animated: false) + } + }, + getCurrentImage: { + return nil + }, + getControllerNode: { [weak self] in + return self + }, + present: { [weak self] c, i, a in + if let self { + self.controller?.present(c, in: i, with: a) + } + }, + addSubview: { [weak self] view in + if let self { + self.view.addSubview(view) + } + } + ) } @objc func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { @@ -1180,6 +1222,12 @@ public final class MediaEditorScreen: ViewController { self.entitiesView.handleRotate(gestureRecognizer) } + @objc func handleTap(_ gestureRecognizer: UITapGestureRecognizer) { + if self.entitiesView.hasSelection { + self.entitiesView.selectEntity(nil) + } + } + func animateIn() { if let transitionIn = self.controller?.transitionIn { switch transitionIn { @@ -1435,21 +1483,6 @@ public final class MediaEditorScreen: ViewController { } } - private func insertDrawingEntity(_ entity: DrawingEntity) { - self.entitiesView.prepareNewEntity(entity) - self.entitiesView.add(entity) - self.entitiesView.selectEntity(entity) - - if let entityView = entitiesView.getView(for: entity.uuid) { - entityView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - entityView.layer.animateScale(from: 0.1, to: entity.scale, duration: 0.2) - - if let selectionView = entityView.selectionView { - selectionView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, delay: 0.2) - } - } - } - private var drawingScreen: DrawingScreen? func containerLayoutUpdated(layout: ContainerViewLayout, forceUpdate: Bool = false, animateOut: Bool = false, transition: Transition) { guard let controller = self.controller else { @@ -1492,71 +1525,65 @@ public final class MediaEditorScreen: ViewController { privacy: controller.state.privacy, openDrawing: { [weak self] mode in if let self { + if self.entitiesView.hasSelection { + self.entitiesView.selectEntity(nil) + } switch mode { case .sticker: let controller = StickerPickerScreen(context: self.context, inputData: self.stickerPickerInputData.get()) controller.completion = { [weak self] file in if let self, let file { let stickerEntity = DrawingStickerEntity(content: .file(file)) - self.insertDrawingEntity(stickerEntity) + self.interaction?.insertEntity(stickerEntity) } } self.controller?.present(controller, in: .current) return case .text: - break - default: - break - } - - let controller = DrawingScreen(context: self.context, sourceHint: .storyEditor, size: self.previewContainerView.frame.size, originalSize: storyDimensions, isVideo: false, isAvatar: false, drawingView: self.drawingView, entitiesView: self.entitiesView, selectionContainerView: self.selectionContainerView, existingStickerPickerInputData: self.stickerPickerInputData) - self.drawingScreen = controller - self.drawingView.isUserInteractionEnabled = true - - controller.requestDismiss = { [weak controller, weak self] in - self?.drawingScreen = nil - controller?.animateOut({ - controller?.dismiss() - }) - self?.drawingView.isUserInteractionEnabled = false - self?.animateInFromTool() + let textEntity = DrawingTextEntity(text: NSAttributedString(), style: .regular, animation: .none, font: .sanFrancisco, alignment: .center, fontSize: 1.0, color: DrawingColor(color: .white)) + self.interaction?.insertEntity(textEntity) + return + case .drawing: + let controller = DrawingScreen(context: self.context, sourceHint: .storyEditor, size: self.previewContainerView.frame.size, originalSize: storyDimensions, isVideo: false, isAvatar: false, drawingView: self.drawingView, entitiesView: self.entitiesView, selectionContainerView: self.selectionContainerView, existingStickerPickerInputData: self.stickerPickerInputData) + self.drawingScreen = controller + self.drawingView.isUserInteractionEnabled = true - self?.entitiesView.selectEntity(nil) - } - controller.requestApply = { [weak controller, weak self] in - self?.drawingScreen = nil - controller?.animateOut({ - controller?.dismiss() - }) - self?.drawingView.isUserInteractionEnabled = false - self?.animateInFromTool() - - if let result = controller?.generateDrawingResultData() { - self?.mediaEditor?.setDrawingAndEntities(data: result.data, image: result.drawingImage, entities: result.entities) - } else { - self?.mediaEditor?.setDrawingAndEntities(data: nil, image: nil, entities: []) + controller.requestDismiss = { [weak controller, weak self] in + self?.drawingScreen = nil + controller?.animateOut({ + controller?.dismiss() + }) + self?.drawingView.isUserInteractionEnabled = false + self?.animateInFromTool() + + self?.entitiesView.selectEntity(nil) } - - self?.entitiesView.selectEntity(nil) + controller.requestApply = { [weak controller, weak self] in + self?.drawingScreen = nil + controller?.animateOut({ + controller?.dismiss() + }) + self?.drawingView.isUserInteractionEnabled = false + self?.animateInFromTool() + + if let result = controller?.generateDrawingResultData() { + self?.mediaEditor?.setDrawingAndEntities(data: result.data, image: result.drawingImage, entities: result.entities) + } else { + self?.mediaEditor?.setDrawingAndEntities(data: nil, image: nil, entities: []) + } + + self?.entitiesView.selectEntity(nil) + } + self.controller?.present(controller, in: .current) + self.animateOutToTool() } - self.controller?.present(controller, in: .current) - - switch mode { - case .sticker: - controller.presentStickerSelection() - case .text: - Queue.mainQueue().after(0.05, { - controller.addTextEntity() - }) - default: - break - } - - self.animateOutToTool() } }, openTools: { [weak self] in if let self, let mediaEditor = self.mediaEditor { + if self.entitiesView.hasSelection { + self.entitiesView.selectEntity(nil) + } let controller = MediaToolsScreen(context: self.context, mediaEditor: mediaEditor) controller.dismissed = { [weak self] in if let self { @@ -1605,6 +1632,8 @@ public final class MediaEditorScreen: ViewController { transition.setFrame(view: self.selectionContainerView, frame: CGRect(origin: .zero, size: previewFrame.size)) + self.interaction?.containerLayoutUpdated(layout: layout, transition: transition) + if isFirstTime { self.animateIn() } @@ -1968,7 +1997,10 @@ public final class MediaEditorScreen: ViewController { } mediaEditor.stop() - + + let codableEntities = self.node.entitiesView.entities.filter { !($0 is DrawingMediaEntity) }.compactMap({ CodableDrawingEntity(entity: $0) }) + mediaEditor.setDrawingAndEntities(data: nil, image: mediaEditor.values.drawing, entities: codableEntities) + if mediaEditor.resultIsVideo { let videoResult: Result.VideoResult let duration: Double diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/VideoScrubberComponent.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/VideoScrubberComponent.swift index 0b798a9943..ef5a75bc8a 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/VideoScrubberComponent.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/VideoScrubberComponent.swift @@ -23,10 +23,10 @@ final class VideoScrubberComponent: Component { let startPosition: Double let endPosition: Double let position: Double + let maxDuration: Double let frames: [UIImage] let framesUpdateTimestamp: Double - let startPositionUpdated: (Double, Bool) -> Void - let endPositionUpdated: (Double, Bool) -> Void + let trimUpdated: (Double, Double, Bool, Bool) -> Void let positionUpdated: (Double, Bool) -> Void init( @@ -35,10 +35,10 @@ final class VideoScrubberComponent: Component { startPosition: Double, endPosition: Double, position: Double, + maxDuration: Double, frames: [UIImage], framesUpdateTimestamp: Double, - startPositionUpdated: @escaping (Double, Bool) -> Void, - endPositionUpdated: @escaping (Double, Bool) -> Void, + trimUpdated: @escaping (Double, Double, Bool, Bool) -> Void, positionUpdated: @escaping (Double, Bool) -> Void ) { self.context = context @@ -46,10 +46,10 @@ final class VideoScrubberComponent: Component { self.startPosition = startPosition self.endPosition = endPosition self.position = position + self.maxDuration = maxDuration self.frames = frames self.framesUpdateTimestamp = framesUpdateTimestamp - self.startPositionUpdated = startPositionUpdated - self.endPositionUpdated = endPositionUpdated + self.trimUpdated = trimUpdated self.positionUpdated = positionUpdated } @@ -69,6 +69,9 @@ final class VideoScrubberComponent: Component { if lhs.position != rhs.position { return false } + if lhs.maxDuration != rhs.maxDuration { + return false + } if lhs.framesUpdateTimestamp != rhs.framesUpdateTimestamp { return false } @@ -165,22 +168,28 @@ final class VideoScrubberComponent: Component { let end = self.frame.width - handleWidth let length = end - start let fraction = (location.x - start) / length - var value = max(0.0, component.duration * fraction) - if value > component.endPosition - minumumDuration { - value = max(0.0, component.endPosition - minumumDuration) + + var startValue = max(0.0, component.duration * fraction) + if startValue > component.endPosition - minumumDuration { + startValue = max(0.0, component.endPosition - minumumDuration) + } + var endValue = component.endPosition + if endValue - startValue > component.maxDuration { + let delta = (endValue - startValue) - component.maxDuration + endValue -= delta } var transition: Transition = .immediate switch gestureRecognizer.state { case .began, .changed: self.isPanningHandle = true - component.startPositionUpdated(value, false) + component.trimUpdated(startValue, endValue, false, false) if case .began = gestureRecognizer.state { transition = .easeInOut(duration: 0.25) } case .ended, .cancelled: self.isPanningHandle = false - component.startPositionUpdated(value, true) + component.trimUpdated(startValue, endValue, false, true) transition = .easeInOut(duration: 0.25) default: break @@ -197,22 +206,28 @@ final class VideoScrubberComponent: Component { let end = self.frame.width - handleWidth let length = end - start let fraction = (location.x - start) / length - var value = min(component.duration, component.duration * fraction) - if value < component.startPosition + minumumDuration { - value = min(component.duration, component.startPosition + minumumDuration) + + var endValue = min(component.duration, component.duration * fraction) + if endValue < component.startPosition + minumumDuration { + endValue = min(component.duration, component.startPosition + minumumDuration) + } + var startValue = component.startPosition + if endValue - startValue > component.maxDuration { + let delta = (endValue - startValue) - component.maxDuration + startValue += delta } var transition: Transition = .immediate switch gestureRecognizer.state { case .began, .changed: self.isPanningHandle = true - component.endPositionUpdated(value, false) + component.trimUpdated(startValue, endValue, true, false) if case .began = gestureRecognizer.state { transition = .easeInOut(duration: 0.25) } case .ended, .cancelled: self.isPanningHandle = false - component.endPositionUpdated(value, true) + component.trimUpdated(startValue, endValue, true, true) transition = .easeInOut(duration: 0.25) default: break diff --git a/submodules/TelegramUI/Sources/TelegramRootController.swift b/submodules/TelegramUI/Sources/TelegramRootController.swift index 254e0a6e6e..1017c1ab4d 100644 --- a/submodules/TelegramUI/Sources/TelegramRootController.swift +++ b/submodules/TelegramUI/Sources/TelegramRootController.swift @@ -376,7 +376,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon switch privacy { case let .story(storyPrivacy, _): let _ = self.context.engine.messages.uploadStory(media: .image(dimensions: dimensions, data: imageData), text: caption?.string ?? "", entities: [], privacy: storyPrivacy).start() - Queue.mainQueue().after(0.2, { [weak chatListController] in + Queue.mainQueue().after(0.3, { [weak chatListController] in chatListController?.animateStoryUploadRipple() }) case let .message(peerIds, timeout): @@ -457,7 +457,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon } if case let .story(storyPrivacy, _) = privacy { let _ = self.context.engine.messages.uploadStory(media: .video(dimensions: dimensions, duration: Int(duration), resource: resource), text: caption?.string ?? "", entities: [], privacy: storyPrivacy).start() - Queue.mainQueue().after(0.2, { [weak chatListController] in + Queue.mainQueue().after(0.3, { [weak chatListController] in chatListController?.animateStoryUploadRipple() }) } else { @@ -468,7 +468,9 @@ public final class TelegramRootController: NavigationController, TelegramRootCon } dismissCameraImpl?() - commit() + Queue.mainQueue().after(0.1) { + commit() + } } ) controller.cancelled = { showDraftTooltip in