import Foundation import UIKit import Display import AsyncDisplayKit import ComponentFlow import SwiftSignalKit import ViewControllerComponent import ComponentDisplayAdapters import TelegramPresentationData import AccountContext import TelegramCore import MultilineTextComponent import DrawingUI import MediaEditor import Photos import LottieAnimationComponent import MessageInputPanelComponent import EntityKeyboard enum DrawingScreenType { case drawing case text case sticker } private let muteButtonTag = GenericComponentViewTag() private let saveButtonTag = GenericComponentViewTag() final class MediaEditorScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext let mediaEditor: MediaEditor? let openDrawing: (DrawingScreenType) -> Void let openTools: () -> Void init( context: AccountContext, mediaEditor: MediaEditor?, openDrawing: @escaping (DrawingScreenType) -> Void, openTools: @escaping () -> Void ) { self.context = context self.mediaEditor = mediaEditor self.openDrawing = openDrawing self.openTools = openTools } static func ==(lhs: MediaEditorScreenComponent, rhs: MediaEditorScreenComponent) -> Bool { if lhs.context !== rhs.context { return false } return true } final class State: ComponentState { enum ImageKey: Hashable { case draw case text case sticker case tools case done } private var cachedImages: [ImageKey: UIImage] = [:] func image(_ key: ImageKey) -> UIImage { if let image = self.cachedImages[key] { return image } else { var image: UIImage switch key { case .draw: image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/Pencil"), color: .white)! case .text: image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/AddText"), color: .white)! case .sticker: image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/AddSticker"), color: .white)! case .tools: image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/Tools"), color: .white)! case .done: let accentColor = self.context.sharedContext.currentPresentationData.with { $0 }.theme.chat.inputPanel.panelControlAccentColor image = generateImage(CGSize(width: 33.0, height: 33.0), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) context.setFillColor(accentColor.cgColor) context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) context.setBlendMode(.copy) context.setStrokeColor(UIColor.white.cgColor) context.setLineWidth(2.0) context.setLineCap(.round) context.setLineJoin(.round) context.translateBy(x: 5.45, y: 4.0) context.saveGState() context.translateBy(x: 4.0, y: 4.0) let _ = try? drawSvgPath(context, path: "M1,7 L7,1 L13,7 S ") context.restoreGState() context.saveGState() context.translateBy(x: 10.0, y: 4.0) let _ = try? drawSvgPath(context, path: "M1,16 V1 S ") context.restoreGState() })! } cachedImages[key] = image return image } } let context: AccountContext init(context: AccountContext) { self.context = context super.init() } deinit { } } func makeState() -> State { return State( context: self.context ) } public final class View: UIView { private let cancelButton = ComponentView() private let drawButton = ComponentView() private let textButton = ComponentView() private let stickerButton = ComponentView() private let toolsButton = ComponentView() private let doneButton = ComponentView() private let inputPanel = ComponentView() private let inputPanelExternalState = MessageInputPanelComponent.ExternalState() private let scrubber = ComponentView() private let saveButton = ComponentView() private let muteButton = ComponentView() private var component: MediaEditorScreenComponent? private weak var state: State? private var environment: ViewControllerComponentContainer.Environment? override init(frame: CGRect) { super.init(frame: frame) self.backgroundColor = .clear } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func animateInFromCamera() { if let view = self.cancelButton.view { view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2) } let buttons = [ self.drawButton, self.textButton, self.stickerButton, self.toolsButton ] var delay: Double = 0.0 for button in buttons { if let view = button.view { view.layer.animatePosition(from: CGPoint(x: 0.0, y: 64.0), to: .zero, duration: 0.3, delay: delay, timingFunction: kCAMediaTimingFunctionSpring, additive: true) view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, delay: delay) view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2, delay: delay) delay += 0.05 } } if let view = self.doneButton.view { view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2) } if let view = self.inputPanel.view { view.layer.animatePosition(from: CGPoint(x: 0.0, y: 44.0), to: .zero, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true) view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2) } if let view = self.saveButton.view { view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2) } if let view = self.muteButton.view { view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2) } } func animateOutToCamera() { let transition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut)) if let view = self.cancelButton.view { transition.setAlpha(view: view, alpha: 0.0) transition.setScale(view: view, scale: 0.1) } let buttons = [ self.drawButton, self.textButton, self.stickerButton, self.toolsButton ] for button in buttons { if let view = button.view { view.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: 64.0), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true) view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) view.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2) } } if let view = self.doneButton.view { transition.setAlpha(view: view, alpha: 0.0) transition.setScale(view: view, scale: 0.1) } if let view = self.inputPanel.view { view.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: 44.0), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true) view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) view.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2) } if let view = self.saveButton.view { transition.setAlpha(view: view, alpha: 0.0) transition.setScale(view: view, scale: 0.1) } if let view = self.muteButton.view { transition.setAlpha(view: view, alpha: 0.0) transition.setScale(view: view, scale: 0.1) } } func animateOutToTool() { let transition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut)) if let view = self.cancelButton.view { view.alpha = 0.0 } let buttons = [ self.drawButton, self.textButton, self.stickerButton, self.toolsButton ] for button in buttons { if let view = button.view { view.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: -44.0), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true) transition.setAlpha(view: view, alpha: 0.0) view.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2) } } if let view = self.doneButton.view { transition.setAlpha(view: view, alpha: 0.0) transition.setScale(view: view, scale: 0.1) } if let view = self.inputPanel.view { transition.setAlpha(view: view, alpha: 0.0) view.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2) } if let view = self.saveButton.view { transition.setAlpha(view: view, alpha: 0.0) transition.setScale(view: view, scale: 0.1) } if let view = self.muteButton.view { transition.setAlpha(view: view, alpha: 0.0) transition.setScale(view: view, scale: 0.1) } } func animateInFromTool() { let transition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut)) if let view = self.cancelButton.view { view.alpha = 1.0 } let buttons = [ self.drawButton, self.textButton, self.stickerButton, self.toolsButton ] for button in buttons { if let view = button.view { view.layer.animatePosition(from: CGPoint(x: 0.0, y: -44.0), to: .zero, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true) transition.setAlpha(view: view, alpha: 1.0) view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2) } } if let view = self.doneButton.view { transition.setAlpha(view: view, alpha: 1.0) transition.setScale(view: view, scale: 1.0) } if let view = self.inputPanel.view { transition.setAlpha(view: view, alpha: 1.0) view.layer.animateScale(from: 0.0, to: 1.0, duration: 0.2) } if let view = self.saveButton.view { transition.setAlpha(view: view, alpha: 1.0) transition.setScale(view: view, scale: 1.0) } if let view = self.muteButton.view { transition.setAlpha(view: view, alpha: 1.0) transition.setScale(view: view, scale: 1.0) } } func update(component: MediaEditorScreenComponent, availableSize: CGSize, state: State, environment: Environment, transition: Transition) -> CGSize { let environment = environment[ViewControllerComponentContainer.Environment.self].value self.environment = environment self.component = component self.state = state let openDrawing = component.openDrawing let openTools = component.openTools let buttonSideInset: CGFloat = 10.0 let buttonBottomInset: CGFloat = 8.0 let cancelButtonSize = self.cancelButton.update( transition: transition, component: AnyComponent(Button( content: AnyComponent( LottieAnimationComponent( animation: LottieAnimationComponent.AnimationItem( name: "media_backToCancel", mode: .still(position: .begin), range: nil ), colors: ["__allcolors__": .white], size: CGSize(width: 33.0, height: 33.0) ) ), action: { guard let controller = environment.controller() as? MediaEditorScreen else { return } controller.requestDismiss(animated: true) } )), environment: {}, containerSize: CGSize(width: 44.0, height: 44.0) ) let cancelButtonFrame = CGRect( origin: CGPoint(x: buttonSideInset, y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset), size: cancelButtonSize ) if let cancelButtonView = self.cancelButton.view { if cancelButtonView.superview == nil { self.addSubview(cancelButtonView) } transition.setPosition(view: cancelButtonView, position: cancelButtonFrame.center) transition.setBounds(view: cancelButtonView, bounds: CGRect(origin: .zero, size: cancelButtonFrame.size)) } let doneButtonSize = self.doneButton.update( transition: transition, component: AnyComponent(Button( content: AnyComponent(Image( image: state.image(.done), size: CGSize(width: 33.0, height: 33.0) )), action: { guard let controller = environment.controller() as? MediaEditorScreen else { return } controller.requestCompletion(animated: true) } )), environment: {}, containerSize: CGSize(width: 44.0, height: 44.0) ) let doneButtonFrame = CGRect( origin: CGPoint(x: availableSize.width - buttonSideInset - doneButtonSize.width, y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset), size: doneButtonSize ) if let doneButtonView = self.doneButton.view { if doneButtonView.superview == nil { self.addSubview(doneButtonView) } transition.setPosition(view: doneButtonView, position: doneButtonFrame.center) transition.setBounds(view: doneButtonView, bounds: CGRect(origin: .zero, size: doneButtonFrame.size)) } let drawButtonSize = self.drawButton.update( transition: transition, component: AnyComponent(Button( content: AnyComponent(Image( image: state.image(.draw), size: CGSize(width: 30.0, height: 30.0) )), action: { openDrawing(.drawing) } )), environment: {}, 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), size: drawButtonSize ) if let drawButtonView = self.drawButton.view { if drawButtonView.superview == nil { self.addSubview(drawButtonView) } transition.setFrame(view: drawButtonView, frame: drawButtonFrame) } let textButtonSize = self.textButton.update( transition: transition, component: AnyComponent(Button( content: AnyComponent(Image( image: state.image(.text), size: CGSize(width: 30.0, height: 30.0) )), action: { openDrawing(.text) } )), environment: {}, 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), size: textButtonSize ) if let textButtonView = self.textButton.view { if textButtonView.superview == nil { self.addSubview(textButtonView) } transition.setFrame(view: textButtonView, frame: textButtonFrame) } let stickerButtonSize = self.stickerButton.update( transition: transition, component: AnyComponent(Button( content: AnyComponent(Image( image: state.image(.sticker), size: CGSize(width: 30.0, height: 30.0) )), action: { openDrawing(.sticker) } )), environment: {}, 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), size: stickerButtonSize ) if let stickerButtonView = self.stickerButton.view { if stickerButtonView.superview == nil { self.addSubview(stickerButtonView) } transition.setFrame(view: stickerButtonView, frame: stickerButtonFrame) } let toolsButtonSize = self.toolsButton.update( transition: transition, component: AnyComponent(Button( content: AnyComponent(Image( image: state.image(.tools), size: CGSize(width: 30.0, height: 30.0) )), action: { openTools() } )), environment: {}, 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), size: toolsButtonSize ) if let toolsButtonView = self.toolsButton.view { if toolsButtonView.superview == nil { self.addSubview(toolsButtonView) } transition.setFrame(view: toolsButtonView, frame: toolsButtonFrame) } var scrubberBottomInset: CGFloat = 0.0 if !"".isEmpty { let scrubberInset: CGFloat = 9.0 let scrubberSize = self.scrubber.update( transition: transition, component: AnyComponent(VideoScrubberComponent( context: component.context, duration: 1.0, startPosition: 0.0, endPosition: 1.0 )), environment: {}, containerSize: CGSize(width: availableSize.width - scrubberInset * 2.0, height: availableSize.height) ) let scrubberFrame = CGRect(origin: CGPoint(x: scrubberInset, y: availableSize.height - environment.safeInsets.bottom - scrubberSize.height - 8.0), size: scrubberSize) if let scrubberView = self.scrubber.view { if scrubberView.superview == nil { self.addSubview(scrubberView) } transition.setFrame(view: scrubberView, frame: scrubberFrame) } scrubberBottomInset = scrubberSize.height + 10.0 } else { } self.inputPanel.parentState = state let inputPanelSize = self.inputPanel.update( transition: transition, component: AnyComponent(MessageInputPanelComponent( externalState: self.inputPanelExternalState, context: component.context, theme: environment.theme, strings: environment.strings, style: .editor, placeholder: "Add a caption...", presentController: { [weak self] c in guard let self, let _ = self.component else { return } //component.presentController(c) }, sendMessageAction: { [weak self] in guard let _ = self else { return } //self.performSendMessageAction() }, setMediaRecordingActive: nil, attachmentAction: nil, reactionAction: nil, audioRecorder: nil, videoRecordingStatus: nil, displayGradient: false,//component.inputHeight != 0.0, bottomInset: 0.0 //component.inputHeight != 0.0 ? 0.0 : bottomContentInset )), environment: {}, containerSize: CGSize(width: availableSize.width, height: 200.0) ) var inputPanelOffset: CGFloat = 0.0 var inputPanelBottomInset: CGFloat = scrubberBottomInset if environment.inputHeight > 0.0 { inputPanelBottomInset = environment.inputHeight - environment.safeInsets.bottom inputPanelOffset = inputPanelBottomInset } let inputPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - environment.safeInsets.bottom - inputPanelBottomInset - inputPanelSize.height - 3.0), size: inputPanelSize) if let inputPanelView = self.inputPanel.view { if inputPanelView.superview == nil { self.addSubview(inputPanelView) } transition.setFrame(view: inputPanelView, frame: inputPanelFrame) } let saveButtonSize = self.saveButton.update( transition: transition, component: AnyComponent(Button( content: AnyComponent( LottieAnimationComponent( animation: LottieAnimationComponent.AnimationItem( name: "anim_storysave", mode: .still(position: .begin), range: nil ), colors: ["__allcolors__": .white], size: CGSize(width: 33.0, height: 33.0) ).tagged(saveButtonTag) ), action: { [weak self] in if let view = self?.saveButton.findTaggedView(tag: saveButtonTag) as? LottieAnimationComponent.View { view.playOnce() } if let controller = environment.controller() as? MediaEditorScreen { controller.requestSave() } } )), environment: {}, containerSize: CGSize(width: 44.0, height: 44.0) ) let saveButtonFrame = CGRect( origin: CGPoint(x: availableSize.width - 20.0 - saveButtonSize.width, y: environment.safeInsets.top + 20.0 - inputPanelOffset), size: saveButtonSize ) if let saveButtonView = self.saveButton.view { if saveButtonView.superview == nil { saveButtonView.layer.shadowOffset = CGSize(width: 0.0, height: 0.0) saveButtonView.layer.shadowRadius = 2.0 saveButtonView.layer.shadowColor = UIColor.black.cgColor saveButtonView.layer.shadowOpacity = 0.25 self.addSubview(saveButtonView) } transition.setPosition(view: saveButtonView, position: saveButtonFrame.center) transition.setBounds(view: saveButtonView, bounds: CGRect(origin: .zero, size: saveButtonFrame.size)) transition.setScale(view: saveButtonView, scale: self.inputPanelExternalState.isEditing ? 0.01 : 1.0) transition.setAlpha(view: saveButtonView, alpha: self.inputPanelExternalState.isEditing ? 0.0 : 1.0) } let isVideoMuted = component.mediaEditor?.values.videoIsMuted ?? false let muteButtonSize = self.muteButton.update( transition: transition, component: AnyComponent(Button( content: AnyComponent( LottieAnimationComponent( animation: LottieAnimationComponent.AnimationItem( name: "anim_storymute", mode: .animating(loop: false), range: isVideoMuted ? (0.0, 0.5) : (0.5, 1.0) ), colors: ["__allcolors__": .white], size: CGSize(width: 33.0, height: 33.0) ).tagged(muteButtonTag) ), action: { [weak self, weak state] in if let self, let mediaEditor = self.component?.mediaEditor { mediaEditor.setVideoIsMuted(!mediaEditor.values.videoIsMuted) state?.updated() } } )), environment: {}, containerSize: CGSize(width: 44.0, height: 44.0) ) let muteButtonFrame = CGRect( origin: CGPoint(x: availableSize.width - 20.0 - muteButtonSize.width - 50.0, y: environment.safeInsets.top + 20.0 - inputPanelOffset), size: muteButtonSize ) if let muteButtonView = self.muteButton.view { if muteButtonView.superview == nil { muteButtonView.layer.shadowOffset = CGSize(width: 0.0, height: 0.0) muteButtonView.layer.shadowRadius = 2.0 muteButtonView.layer.shadowColor = UIColor.black.cgColor muteButtonView.layer.shadowOpacity = 0.25 self.addSubview(muteButtonView) } transition.setPosition(view: muteButtonView, position: muteButtonFrame.center) transition.setBounds(view: muteButtonView, bounds: CGRect(origin: .zero, size: muteButtonFrame.size)) transition.setScale(view: muteButtonView, scale: self.inputPanelExternalState.isEditing ? 0.01 : 1.0) transition.setAlpha(view: muteButtonView, alpha: self.inputPanelExternalState.isEditing ? 0.0 : 1.0) } return availableSize } } func makeView() -> View { return View() } public func update(view: View, availableSize: CGSize, state: State, environment: Environment, transition: Transition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } private let storyDimensions = CGSize(width: 1080.0, height: 1920.0) public final class MediaEditorScreen: ViewController { fileprivate final class Node: ViewControllerTracingNode, UIGestureRecognizerDelegate { private weak var controller: MediaEditorScreen? private let context: AccountContext private let initializationTimestamp = CACurrentMediaTime() fileprivate var subject: MediaEditorScreen.Subject? private var subjectDisposable: Disposable? private let backgroundDimView: UIView fileprivate let componentHost: ComponentView private let previewContainerView: UIView private let gradientView: UIImageView private var gradientColorsDisposable: Disposable? fileprivate let entitiesContainerView: UIView fileprivate let entitiesView: DrawingEntitiesView fileprivate let drawingView: DrawingView fileprivate let previewView: MediaEditorPreviewView fileprivate var mediaEditor: MediaEditor? private let stickerPickerInputData = Promise() private var presentationData: PresentationData private let hapticFeedback = HapticFeedback() private var validLayout: ContainerViewLayout? init(controller: MediaEditorScreen) { self.controller = controller self.context = controller.context self.presentationData = self.context.sharedContext.currentPresentationData.with { $0 } self.backgroundDimView = UIView() self.backgroundDimView.alpha = 0.0 self.backgroundDimView.backgroundColor = .black self.componentHost = ComponentView() self.previewContainerView = UIView() self.previewContainerView.alpha = 0.0 self.previewContainerView.clipsToBounds = true self.previewContainerView.layer.cornerRadius = 12.0 if #available(iOS 13.0, *) { self.previewContainerView.layer.cornerCurve = .continuous } self.gradientView = UIImageView() self.entitiesContainerView = UIView(frame: CGRect(origin: .zero, size: storyDimensions)) self.entitiesView = DrawingEntitiesView(context: controller.context, size: storyDimensions) self.entitiesView.getEntityCenterPosition = { return CGPoint(x: storyDimensions.width / 2.0, y: storyDimensions.height / 2.0) } self.previewView = MediaEditorPreviewView(frame: .zero) self.drawingView = DrawingView(size: storyDimensions) self.drawingView.isUserInteractionEnabled = false super.init() self.backgroundColor = .clear //self.view.addSubview(self.backgroundDimView) self.view.addSubview(self.previewContainerView) self.previewContainerView.addSubview(self.gradientView) self.previewContainerView.addSubview(self.entitiesContainerView) self.entitiesContainerView.addSubview(self.entitiesView) self.previewContainerView.addSubview(self.drawingView) self.subjectDisposable = ( controller.subject |> filter { $0 != nil } |> take(1) |> deliverOnMainQueue ).start(next: { [weak self] subject in if let self, let subject { self.setup(with: subject) } }) let stickerPickerInputData = self.stickerPickerInputData Queue.concurrentDefaultQueue().after(0.5, { let emojiItems = EmojiPagerContentComponent.emojiInputData( context: controller.context, animationCache: controller.context.animationCache, animationRenderer: controller.context.animationRenderer, isStandalone: false, isStatusSelection: false, isReactionSelection: false, isEmojiSelection: true, hasTrending: false, topReactionItems: [], areUnicodeEmojiEnabled: true, areCustomEmojiEnabled: true, chatPeerId: controller.context.account.peerId, hasSearch: false, forceHasPremium: true ) let stickerItems = EmojiPagerContentComponent.stickerInputData( context: controller.context, animationCache: controller.context.animationCache, animationRenderer: controller.context.animationRenderer, stickerNamespaces: [Namespaces.ItemCollection.CloudStickerPacks], stickerOrderedItemListCollectionIds: [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers, Namespaces.OrderedItemList.CloudAllPremiumStickers], chatPeerId: controller.context.account.peerId, hasSearch: false, hasTrending: true, forceHasPremium: true ) let maskItems = EmojiPagerContentComponent.stickerInputData( context: controller.context, animationCache: controller.context.animationCache, animationRenderer: controller.context.animationRenderer, stickerNamespaces: [Namespaces.ItemCollection.CloudMaskPacks], stickerOrderedItemListCollectionIds: [], chatPeerId: controller.context.account.peerId, hasSearch: false, hasTrending: false, forceHasPremium: true ) let signal = combineLatest(queue: .mainQueue(), emojiItems, stickerItems, maskItems ) |> map { emoji, stickers, masks -> StickerPickerInputData in return StickerPickerInputData(emoji: emoji, stickers: stickers, masks: masks) } stickerPickerInputData.set(signal) }) } deinit { self.subjectDisposable?.dispose() self.gradientColorsDisposable?.dispose() } private func setup(with subject: MediaEditorScreen.Subject) { self.subject = subject guard let _ = self.controller else { return } let mediaDimensions = subject.dimensions let maxSide: CGFloat = 1920.0 / UIScreen.main.scale let fittedSize = mediaDimensions.cgSize.fitted(CGSize(width: maxSide, height: maxSide)) let mediaEntity = DrawingMediaEntity(content: subject.mediaContent, size: fittedSize) mediaEntity.position = CGPoint(x: storyDimensions.width / 2.0, y: storyDimensions.height / 2.0) if fittedSize.height > fittedSize.width { mediaEntity.scale = storyDimensions.height / fittedSize.height } else { mediaEntity.scale = storyDimensions.width / fittedSize.width } self.entitiesView.add(mediaEntity, announce: false) let initialPosition = mediaEntity.position let initialScale = mediaEntity.scale let initialRotation = mediaEntity.rotation if let entityView = self.entitiesView.getView(for: mediaEntity.uuid) as? DrawingMediaEntityView { entityView.previewView = self.previewView entityView.updated = { [weak self, weak mediaEntity] in if let self, let mediaEntity { let rotationDelta = mediaEntity.rotation - initialRotation let positionDelta = CGPoint(x: mediaEntity.position.x - initialPosition.x, y: mediaEntity.position.y - initialPosition.y) let scaleDelta = mediaEntity.scale / initialScale self.mediaEditor?.setCrop(offset: positionDelta, scale: scaleDelta, rotation: rotationDelta, mirroring: false) } } } let mediaEditor = MediaEditor(subject: subject.editorSubject, hasHistogram: true) mediaEditor.attachPreviewView(self.previewView) self.gradientColorsDisposable = mediaEditor.gradientColors.start(next: { [weak self] colors in if let self, let colors { let (topColor, bottomColor) = colors let gradientImage = generateGradientImage(size: CGSize(width: 5.0, height: 640.0), colors: [topColor, bottomColor], locations: [0.0, 1.0]) Queue.mainQueue().async { self.gradientView.image = gradientImage self.previewContainerView.alpha = 1.0 if CACurrentMediaTime() - self.initializationTimestamp > 0.2 { self.previewContainerView.layer.allowsGroupOpacity = true self.previewContainerView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, completion: { _ in self.previewContainerView.layer.allowsGroupOpacity = false }) } } } }) self.mediaEditor = mediaEditor } override func didLoad() { super.didLoad() self.view.disablesInteractiveModalDismiss = true self.view.disablesInteractiveKeyboardGestureRecognizer = true let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:))) panGestureRecognizer.delegate = self panGestureRecognizer.minimumNumberOfTouches = 2 panGestureRecognizer.maximumNumberOfTouches = 2 self.previewContainerView.addGestureRecognizer(panGestureRecognizer) let pinchGestureRecognizer = UIPinchGestureRecognizer(target: self, action: #selector(self.handlePinch(_:))) pinchGestureRecognizer.delegate = self self.previewContainerView.addGestureRecognizer(pinchGestureRecognizer) let rotateGestureRecognizer = UIRotationGestureRecognizer(target: self, action: #selector(self.handleRotate(_:))) rotateGestureRecognizer.delegate = self self.previewContainerView.addGestureRecognizer(rotateGestureRecognizer) } @objc func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { return true } @objc func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) { self.entitiesView.handlePan(gestureRecognizer) } @objc func handlePinch(_ gestureRecognizer: UIPinchGestureRecognizer) { self.entitiesView.handlePinch(gestureRecognizer) } @objc func handleRotate(_ gestureRecognizer: UIRotationGestureRecognizer) { self.entitiesView.handleRotate(gestureRecognizer) } func animateIn() { if let sourceHint = self.controller?.sourceHint { switch sourceHint { case .camera: if let view = self.componentHost.view as? MediaEditorScreenComponent.View { view.animateInFromCamera() } } } } func animateOut(completion: @escaping () -> Void) { guard let controller = self.controller else { return } if let sourceHint = controller.sourceHint { switch sourceHint { case .camera: if let view = self.componentHost.view as? MediaEditorScreenComponent.View { view.animateOutToCamera() } let transition = Transition(animation: .curve(duration: 0.25, curve: .easeInOut)) transition.setAlpha(view: self.previewContainerView, alpha: 0.0, completion: { _ in completion() }) } } else { completion() } } func animateOutToTool() { if let view = self.componentHost.view as? MediaEditorScreenComponent.View { view.animateOutToTool() } } func animateInFromTool() { if let view = self.componentHost.view as? MediaEditorScreenComponent.View { view.animateInFromTool() } } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { let result = super.hitTest(point, with: event) if result == self.componentHost.view { self.controller?.view.endEditing(true) let point = self.view.convert(point, to: self.previewContainerView) return self.previewContainerView.hitTest(point, with: event) } return result } private var drawingScreen: DrawingScreen? func containerLayoutUpdated(layout: ContainerViewLayout, forceUpdate: Bool = false, animateOut: Bool = false, transition: Transition) { guard let _ = self.controller else { return } let isFirstTime = self.validLayout == nil self.validLayout = layout let previewSize = CGSize(width: layout.size.width, height: floorToScreenPixels(layout.size.width * 1.77778)) let topInset: CGFloat = floor(layout.size.height - previewSize.height) / 2.0 let environment = ViewControllerComponentContainer.Environment( statusBarHeight: layout.statusBarHeight ?? 0.0, navigationHeight: 0.0, safeInsets: UIEdgeInsets( top: topInset, left: layout.safeInsets.left, bottom: topInset, right: layout.safeInsets.right ), inputHeight: layout.inputHeight ?? 0.0, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, orientation: nil, isVisible: true, theme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, controller: { [weak self] in return self?.controller } ) let componentSize = self.componentHost.update( transition: transition, component: AnyComponent( MediaEditorScreenComponent( context: self.context, mediaEditor: self.mediaEditor, openDrawing: { [weak self] mode in if let self { 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, existingStickerPickerInputData: self.stickerPickerInputData) self.drawingScreen = controller self.drawingView.isUserInteractionEnabled = true let selectionContainerView = controller.selectionContainerView selectionContainerView.frame = self.previewContainerView.bounds self.previewContainerView.addSubview(selectionContainerView) controller.requestDismiss = { [weak controller, weak self, weak selectionContainerView] in self?.drawingScreen = nil controller?.animateOut({ controller?.dismiss() }) self?.drawingView.isUserInteractionEnabled = false self?.animateInFromTool() selectionContainerView?.removeFromSuperview() } controller.requestApply = { [weak controller, weak self, weak selectionContainerView] in self?.drawingScreen = nil controller?.animateOut({ controller?.dismiss() }) self?.drawingView.isUserInteractionEnabled = false self?.animateInFromTool() selectionContainerView?.removeFromSuperview() } 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 { let controller = MediaToolsScreen(context: self.context, mediaEditor: mediaEditor) controller.dismissed = { [weak self] in if let self { self.animateInFromTool() } } self.controller?.present(controller, in: .current) self.animateOutToTool() } } ) ), environment: { environment }, forceUpdate: forceUpdate || animateOut, containerSize: layout.size ) if let componentView = self.componentHost.view { if componentView.superview == nil { self.view.insertSubview(componentView, at: 3) componentView.clipsToBounds = true } let componentFrame = CGRect(origin: .zero, size: componentSize) transition.setFrame(view: componentView, frame: CGRect(origin: componentFrame.origin, size: CGSize(width: componentFrame.width, height: componentFrame.height))) } var bottomInputOffset: CGFloat = 0.0 if let inputHeight = layout.inputHeight, inputHeight > 0.0 { bottomInputOffset = inputHeight - topInset } transition.setFrame(view: self.backgroundDimView, frame: CGRect(origin: .zero, size: layout.size)) var previewFrame = CGRect(origin: CGPoint(x: 0.0, y: topInset - bottomInputOffset), size: previewSize) if let inputHeight = layout.inputHeight, inputHeight > 0.0, self.drawingScreen != nil { previewFrame = previewFrame.offsetBy(dx: 0.0, dy: inputHeight / 2.0) } transition.setFrame(view: self.previewContainerView, frame: previewFrame) let entitiesViewScale = previewSize.width / storyDimensions.width self.entitiesContainerView.transform = CGAffineTransformMakeScale(entitiesViewScale, entitiesViewScale) transition.setFrame(view: self.entitiesContainerView, frame: CGRect(origin: .zero, size: previewFrame.size)) transition.setFrame(view: self.gradientView, frame: CGRect(origin: .zero, size: previewFrame.size)) transition.setFrame(view: self.drawingView, frame: CGRect(origin: .zero, size: previewFrame.size)) if isFirstTime { self.animateIn() } } } fileprivate var node: Node { return self.displayNode as! Node } public enum Subject { case image(UIImage, PixelDimensions) case video(String, PixelDimensions) case asset(PHAsset) var dimensions: PixelDimensions { switch self { case let .image(_, dimensions), let .video(_, dimensions): return dimensions case let .asset(asset): return PixelDimensions(width: Int32(asset.pixelWidth), height: Int32(asset.pixelHeight)) } } var editorSubject: MediaEditor.Subject { switch self { case let .image(image, dimensions): return .image(image, dimensions) case let .video(videoPath, dimensions): return .video(videoPath, dimensions) case let .asset(asset): return .asset(asset) } } var mediaContent: DrawingMediaEntity.Content { switch self { case let .image(image, dimensions): return .image(image, dimensions) case let .video(videoPath, dimensions): return .video(videoPath, dimensions) case let .asset(asset): return .asset(asset) } } } public enum Result { case image(UIImage) case video(String, UIImage?, MediaEditorValues) } fileprivate let context: AccountContext fileprivate let subject: Signal public enum SourceHint { case camera } public var sourceHint: SourceHint? public var cancelled: () -> Void = {} public var completion: (MediaEditorScreen.Result, @escaping () -> Void) -> Void = { _, _ in } public init(context: AccountContext, subject: Signal, completion: @escaping (MediaEditorScreen.Result, @escaping () -> Void) -> Void) { self.context = context self.subject = subject self.completion = completion super.init(navigationBarPresentationData: nil) self.navigationPresentation = .flatModal self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) self.statusBar.statusBarStyle = .White } required public init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } override public func loadDisplayNode() { self.displayNode = Node(controller: self) super.displayNodeDidLoad() } func requestDismiss(animated: Bool) { self.cancelled() self.node.animateOut(completion: { [weak self] in self?.dismiss() }) } func requestCompletion(animated: Bool) { guard let mediaEditor = self.node.mediaEditor else { return } if mediaEditor.resultIsVideo { } else { if let image = mediaEditor.resultImage { self.completion(.image(image), { [weak self] in self?.node.animateOut(completion: { [weak self] in self?.dismiss() }) }) } } } private var export: MediaEditorVideoExport? private var exportDisposable: Disposable? func requestSave() { guard let mediaEditor = self.node.mediaEditor, let subject = self.node.subject else { return } let tempVideoPath = NSTemporaryDirectory() + "\(Int64.random(in: Int64.min ... Int64.max)).mp4" let saveToPhotos: (String, Bool) -> Void = { path, isVideo in PHPhotoLibrary.shared().performChanges({ if isVideo { if let _ = try? FileManager.default.copyItem(atPath: path, toPath: tempVideoPath) { PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: URL(fileURLWithPath: path)) } } else { if let fileData = try? Data(contentsOf: URL(fileURLWithPath: path)) { PHAssetCreationRequest.forAsset().addResource(with: .photo, data: fileData, options: nil) } } }, completionHandler: { _, error in if let error = error { print("\(error)") } let _ = try? FileManager.default.removeItem(atPath: tempVideoPath) }) } if mediaEditor.resultIsVideo { let exportSubject: MediaEditorVideoExport.Subject if case let .video(path, _) = subject { let asset = AVURLAsset(url: NSURL(fileURLWithPath: path) as URL) exportSubject = .video(asset) } else { fatalError() } let configuration = recommendedExportConfiguration(mediaEditor: mediaEditor) let outputPath = NSTemporaryDirectory() + "\(Int64.random(in: 0 ..< .max)).mp4" let export = MediaEditorVideoExport(subject: exportSubject, configuration: configuration, outputPath: outputPath) self.export = export export.startExport() self.exportDisposable = (export.status |> deliverOnMainQueue).start(next: { [weak self] status in if let _ = self { if case .completed = status { saveToPhotos(outputPath, true) } } }) } else { } } override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) (self.displayNode as! Node).containerLayoutUpdated(layout: layout, transition: Transition(transition)) } } private func recommendedExportConfiguration(mediaEditor: MediaEditor) -> MediaEditorVideoExport.Configuration { let compressionProperties: [String: Any] = [ AVVideoAverageBitRateKey: 2000000 ] let videoSettings: [String: Any] = [ AVVideoCodecKey: AVVideoCodecType.h264, AVVideoCompressionPropertiesKey: compressionProperties, AVVideoWidthKey: 1080, AVVideoHeightKey: 1920 ] let audioSettings: [String: Any] = [ AVFormatIDKey: kAudioFormatMPEG4AAC, AVSampleRateKey: 44100, AVEncoderBitRateKey: 64000, AVNumberOfChannelsKey: 2 ] return MediaEditorVideoExport.Configuration( videoSettings: videoSettings, audioSettings: audioSettings, values: mediaEditor.values ) }