import Foundation import UIKit import CoreServices 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 import TooltipUI import BlurredBackgroundComponent import AvatarNode import ShareWithPeersScreen import PresentationDataUtils import ContextUI import BundleIconComponent import CameraButtonComponent enum DrawingScreenType { case drawing case text case sticker } private let privacyButtonTag = GenericComponentViewTag() private let muteButtonTag = GenericComponentViewTag() private let saveButtonTag = GenericComponentViewTag() final class MediaEditorScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext let isDisplayingTool: Bool let isInteractingWithEntities: Bool let isSavingAvailable: Bool let hasAppeared: Bool let isDismissing: Bool let mediaEditor: MediaEditor? let privacy: MediaEditorResultPrivacy let selectedEntity: DrawingEntity? let entityViewForEntity: (DrawingEntity) -> DrawingEntityView? let openDrawing: (DrawingScreenType) -> Void let openTools: () -> Void init( context: AccountContext, isDisplayingTool: Bool, isInteractingWithEntities: Bool, isSavingAvailable: Bool, hasAppeared: Bool, isDismissing: Bool, mediaEditor: MediaEditor?, privacy: MediaEditorResultPrivacy, selectedEntity: DrawingEntity?, entityViewForEntity: @escaping (DrawingEntity) -> DrawingEntityView?, openDrawing: @escaping (DrawingScreenType) -> Void, openTools: @escaping () -> Void ) { self.context = context self.isDisplayingTool = isDisplayingTool self.isInteractingWithEntities = isInteractingWithEntities self.isSavingAvailable = isSavingAvailable self.hasAppeared = hasAppeared self.isDismissing = isDismissing self.mediaEditor = mediaEditor self.privacy = privacy self.selectedEntity = selectedEntity self.entityViewForEntity = entityViewForEntity self.openDrawing = openDrawing self.openTools = openTools } static func ==(lhs: MediaEditorScreenComponent, rhs: MediaEditorScreenComponent) -> Bool { if lhs.context !== rhs.context { return false } if lhs.isDisplayingTool != rhs.isDisplayingTool { return false } if lhs.isInteractingWithEntities != rhs.isInteractingWithEntities { return false } if lhs.isSavingAvailable != rhs.isSavingAvailable { return false } if lhs.hasAppeared != rhs.hasAppeared { return false } if lhs.isDismissing != rhs.isDismissing { return false } if lhs.privacy != rhs.privacy { return false } if lhs.selectedEntity?.uuid != rhs.selectedEntity?.uuid { 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: image = generateImage(CGSize(width: 33.0, height: 33.0), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) context.setFillColor(UIColor.white.cgColor) context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) context.setBlendMode(.copy) context.setStrokeColor(UIColor.black.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 var playerStateDisposable: Disposable? var playerState: MediaEditorPlayerState? init(context: AccountContext, mediaEditor: MediaEditor?) { self.context = context super.init() if let mediaEditor { self.playerStateDisposable = (mediaEditor.playerState(framesCount: 16) |> deliverOnMainQueue).start(next: { [weak self] playerState in if let self { self.playerState = playerState self.updated() } }) } } deinit { self.playerStateDisposable?.dispose() } var muteDidChange = false } func makeState() -> State { return State( context: self.context, mediaEditor: self.mediaEditor ) } 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 fadeView = UIButton() private let inputPanel = ComponentView() private let inputPanelExternalState = MessageInputPanelComponent.ExternalState() private let scrubber = ComponentView() private let privacyButton = ComponentView() private let flipStickerButton = ComponentView() private let muteButton = ComponentView() private let saveButton = ComponentView() private let settingsButton = ComponentView() private let textCancelButton = ComponentView() private let textDoneButton = ComponentView() private let textSize = ComponentView() private var isDismissed = false 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 self.fadeView.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.4) self.fadeView.addTarget(self, action: #selector(self.fadePressed), for: .touchUpInside) self.fadeView.alpha = 0.0 self.addSubview(self.fadeView) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @objc private func fadePressed() { self.endEditing(true) } enum TransitionAnimationSource { case camera case gallery } func animateIn(from source: TransitionAnimationSource, completion: @escaping () -> Void = {}) { let buttons = [ self.drawButton, self.textButton, self.stickerButton, self.toolsButton ] 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) } 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 case .camera = source { var delay: Double = 0.0 for button in buttons { if let view = button.view { view.alpha = 0.0 Queue.mainQueue().after(delay, { view.alpha = 1.0 view.layer.animatePosition(from: CGPoint(x: 0.0, y: 64.0), to: .zero, duration: 0.3, delay: 0.0, timingFunction: kCAMediaTimingFunctionSpring, additive: true) view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, delay: 0.0) view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2, delay: 0.0) }) delay += 0.03 Queue.mainQueue().after(0.45, completion) } } 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) } if let view = self.settingsButton.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.privacyButton.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.6, to: 1.0, duration: 0.2) } if let view = self.scrubber.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.6, to: 1.0, duration: 0.2) } } } func animateOut(to source: TransitionAnimationSource) { self.isDismissed = true 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: view.alpha, 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 case .camera = source { 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: view.alpha, to: 0.0, duration: 0.2, removeOnCompletion: false) view.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2) } if let view = self.scrubber.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: view.alpha, 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) } if let view = self.settingsButton.view { transition.setAlpha(view: view, alpha: 0.0) transition.setScale(view: view, scale: 0.1) } if let view = self.privacyButton.view { transition.setAlpha(view: view, alpha: 0.0) transition.setScale(view: view, scale: 0.1) } if let view = self.scrubber.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: view.alpha, to: 0.0, duration: 0.2, removeOnCompletion: false) view.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2) } } func animateOutToTool(transition: Transition) { 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) view.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2) } } if let view = self.doneButton.view { transition.setScale(view: view, scale: 0.1) } if let view = self.inputPanel.view { view.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2) } if let view = self.scrubber.view { view.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2) } } func animateInFromTool(transition: Transition) { 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) view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2) } } if let view = self.doneButton.view { transition.setScale(view: view, scale: 1.0) } if let view = self.inputPanel.view { view.layer.animateScale(from: 0.0, to: 1.0, duration: 0.2) } if let view = self.scrubber.view { view.layer.animateScale(from: 0.0, to: 1.0, duration: 0.2) } } private var isEditingCaption = false func update(component: MediaEditorScreenComponent, availableSize: CGSize, state: State, environment: Environment, transition: Transition) -> CGSize { guard !self.isDismissed else { return availableSize } let environment = environment[ViewControllerComponentContainer.Environment.self].value self.environment = environment self.component = component self.state = state let isTablet: Bool if case .regular = environment.metrics.widthClass { isTablet = true } else { isTablet = false } let openDrawing = component.openDrawing let openTools = component.openTools let buttonSideInset: CGFloat let buttonBottomInset: CGFloat = 8.0 let previewSize: CGSize let topInset: CGFloat = environment.statusBarHeight + 12.0 if isTablet { let previewHeight = availableSize.height - topInset - 75.0 previewSize = CGSize(width: floorToScreenPixels(previewHeight / 1.77778), height: previewHeight) buttonSideInset = 30.0 } else { previewSize = CGSize(width: availableSize.width, height: floorToScreenPixels(availableSize.width * 1.77778)) buttonSideInset = 10.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.maybePresentDiscardAlert() } )), 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)) transition.setAlpha(view: cancelButtonView, alpha: component.isDisplayingTool || component.isDismissing || component.isInteractingWithEntities ? 0.0 : 1.0) } 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: { [weak self] in guard let self, let controller = environment.controller() as? MediaEditorScreen else { return } guard let inputPanelView = self.inputPanel.view as? MessageInputPanelComponent.View else { return } var inputText = NSAttributedString(string: "") switch inputPanelView.getSendMessageInput() { case let .text(text): inputText = NSAttributedString(string: text) } controller.requestCompletion(caption: inputText, 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)) transition.setAlpha(view: doneButtonView, alpha: component.isDisplayingTool || component.isDismissing || component.isInteractingWithEntities ? 0.0 : 1.0) } let buttonsAvailableWidth: CGFloat let buttonsLeftOffset: CGFloat if isTablet { buttonsAvailableWidth = previewSize.width + 260.0 buttonsLeftOffset = floorToScreenPixels((availableSize.width - buttonsAvailableWidth) / 2.0) } else { buttonsAvailableWidth = availableSize.width buttonsLeftOffset = 0.0 } 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: buttonsLeftOffset + floorToScreenPixels(buttonsAvailableWidth / 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 { if drawButtonView.superview == nil { self.addSubview(drawButtonView) } transition.setPosition(view: drawButtonView, position: drawButtonFrame.center) transition.setBounds(view: drawButtonView, bounds: CGRect(origin: .zero, size: drawButtonFrame.size)) transition.setAlpha(view: drawButtonView, alpha: component.isDisplayingTool || component.isDismissing || component.isInteractingWithEntities ? 0.0 : 1.0) } 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: buttonsLeftOffset + floorToScreenPixels(buttonsAvailableWidth / 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 { if textButtonView.superview == nil { self.addSubview(textButtonView) } transition.setPosition(view: textButtonView, position: textButtonFrame.center) transition.setBounds(view: textButtonView, bounds: CGRect(origin: .zero, size: textButtonFrame.size)) transition.setAlpha(view: textButtonView, alpha: component.isDisplayingTool || component.isDismissing || component.isInteractingWithEntities ? 0.0 : 1.0) } 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 - buttonsLeftOffset - buttonsAvailableWidth / 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 { if stickerButtonView.superview == nil { self.addSubview(stickerButtonView) } transition.setPosition(view: stickerButtonView, position: stickerButtonFrame.center) transition.setBounds(view: stickerButtonView, bounds: CGRect(origin: .zero, size: stickerButtonFrame.size)) transition.setAlpha(view: stickerButtonView, alpha: component.isDisplayingTool || component.isDismissing || component.isInteractingWithEntities ? 0.0 : 1.0) } 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: buttonsLeftOffset + floorToScreenPixels(buttonsAvailableWidth / 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 { if toolsButtonView.superview == nil { self.addSubview(toolsButtonView) } transition.setPosition(view: toolsButtonView, position: toolsButtonFrame.center) transition.setBounds(view: toolsButtonView, bounds: CGRect(origin: .zero, size: toolsButtonFrame.size)) transition.setAlpha(view: toolsButtonView, alpha: component.isDisplayingTool || component.isDismissing || component.isInteractingWithEntities ? 0.0 : 1.0) } let mediaEditor = component.mediaEditor var scrubberBottomInset: CGFloat = 0.0 if let playerState = state.playerState { let scrubberInset: CGFloat = 9.0 let scrubberSize = self.scrubber.update( transition: transition, component: AnyComponent(VideoScrubberComponent( context: component.context, generationTimestamp: playerState.generationTimestamp, duration: playerState.duration, startPosition: playerState.timeRange?.lowerBound ?? 0.0, endPosition: playerState.timeRange?.upperBound ?? min(playerState.duration, storyMaxVideoDuration), position: playerState.position, maxDuration: storyMaxVideoDuration, isPlaying: playerState.isPlaying, frames: playerState.frames, framesUpdateTimestamp: playerState.framesUpdateTimestamp, trimUpdated: { [weak mediaEditor] start, end, updatedEnd, done in if let mediaEditor { mediaEditor.setVideoTrimRange(start.. 0.0 { inputPanelBottomInset = environment.inputHeight - environment.safeInsets.bottom inputPanelOffset = inputPanelBottomInset } let inputPanelFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - inputPanelSize.width) / 2.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) transition.setAlpha(view: inputPanelView, alpha: isEditingTextEntity || component.isDisplayingTool || component.isDismissing || component.isInteractingWithEntities ? 0.0 : 1.0) } let privacyText: String switch component.privacy { case let .story(privacy, _, _): switch privacy.base { case .everyone: privacyText = "Everyone" case .closeFriends: privacyText = "Close Friends" case .contacts: privacyText = "Contacts" case .nobody: privacyText = "Selected Contacts" } case let .message(peerIds, _): if peerIds.count == 1 { privacyText = "1 Recipient" } else { privacyText = "\(peerIds.count) Recipients" } } let displayTopButtons = !(self.inputPanelExternalState.isEditing || isEditingTextEntity || component.isDisplayingTool) let privacyButtonSize = self.privacyButton.update( transition: transition, component: AnyComponent(Button( content: AnyComponent( PrivacyButtonComponent( backgroundColor: isTablet ? UIColor(rgb: 0x303030, alpha: 0.5) : UIColor(white: 0.0, alpha: 0.5), icon: UIImage(bundleImageName: "Media Editor/Recipient")!, text: privacyText ) ), action: { if let controller = environment.controller() as? MediaEditorScreen { controller.openPrivacySettings() } } ).tagged(privacyButtonTag)), environment: {}, containerSize: CGSize(width: 44.0, height: 44.0) ) let privacyButtonFrame: CGRect if isTablet { privacyButtonFrame = CGRect( origin: CGPoint(x: availableSize.width - buttonSideInset - doneButtonSize.width - privacyButtonSize.width - 24.0, y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset + 1.0), size: privacyButtonSize ) } else { privacyButtonFrame = CGRect( origin: CGPoint(x: 16.0, y: environment.safeInsets.top + 20.0 - inputPanelOffset), size: privacyButtonSize ) } if let privacyButtonView = self.privacyButton.view { if privacyButtonView.superview == nil { self.addSubview(privacyButtonView) } transition.setPosition(view: privacyButtonView, position: privacyButtonFrame.center) transition.setBounds(view: privacyButtonView, bounds: CGRect(origin: .zero, size: privacyButtonFrame.size)) transition.setScale(view: privacyButtonView, scale: displayTopButtons ? 1.0 : 0.01) transition.setAlpha(view: privacyButtonView, alpha: displayTopButtons && !component.isDismissing && !component.isInteractingWithEntities ? 1.0 : 0.0) } let saveContentComponent: AnyComponentWithIdentity if component.hasAppeared { saveContentComponent = AnyComponentWithIdentity( id: "animatedIcon", component: AnyComponent( LottieAnimationComponent( animation: LottieAnimationComponent.AnimationItem( name: "anim_storysave", mode: .still(position: .begin), range: nil ), colors: ["__allcolors__": .white], size: CGSize(width: 30.0, height: 30.0) ).tagged(saveButtonTag) ) ) } else { saveContentComponent = AnyComponentWithIdentity( id: "staticIcon", component: AnyComponent( BundleIconComponent( name: "Media Editor/SaveIcon", tintColor: nil ) ) ) } let saveButtonSize = self.saveButton.update( transition: transition, component: AnyComponent(CameraButton( content: saveContentComponent, 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.35 self.addSubview(saveButtonView) } let saveButtonAlpha = component.isSavingAvailable ? 1.0 : 0.3 saveButtonView.isUserInteractionEnabled = component.isSavingAvailable transition.setPosition(view: saveButtonView, position: saveButtonFrame.center) transition.setBounds(view: saveButtonView, bounds: CGRect(origin: .zero, size: saveButtonFrame.size)) transition.setScale(view: saveButtonView, scale: displayTopButtons ? 1.0 : 0.01) transition.setAlpha(view: saveButtonView, alpha: displayTopButtons && !component.isDismissing && !component.isInteractingWithEntities ? saveButtonAlpha : 0.0) } if let playerState = state.playerState, playerState.hasAudio { let isVideoMuted = component.mediaEditor?.values.videoIsMuted ?? false let muteContentComponent: AnyComponentWithIdentity if component.hasAppeared { muteContentComponent = AnyComponentWithIdentity( id: "animatedIcon", component: AnyComponent( LottieAnimationComponent( animation: LottieAnimationComponent.AnimationItem( name: "anim_storymute", mode: state.muteDidChange ? .animating(loop: false) : .still(position: .begin), range: isVideoMuted ? (0.0, 0.5) : (0.5, 1.0) ), colors: ["__allcolors__": .white], size: CGSize(width: 30.0, height: 30.0) ).tagged(muteButtonTag) ) ) } else { muteContentComponent = AnyComponentWithIdentity( id: "staticIcon", component: AnyComponent( BundleIconComponent( name: "Media Editor/MuteIcon", tintColor: nil ) ) ) } let muteButtonSize = self.muteButton.update( transition: transition, component: AnyComponent(CameraButton( content: muteContentComponent, action: { [weak self, weak state] in if let self, let mediaEditor = self.component?.mediaEditor { state?.muteDidChange = true let isMuted = !mediaEditor.values.videoIsMuted mediaEditor.setVideoIsMuted(isMuted) state?.updated() if let controller = environment.controller() as? MediaEditorScreen { controller.node.presentMutedTooltip() } } } )), 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.35 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: displayTopButtons ? 1.0 : 0.01) transition.setAlpha(view: muteButtonView, alpha: displayTopButtons && !component.isDismissing && !component.isInteractingWithEntities ? 1.0 : 0.0) } } if let _ = state.playerState { let settingsButtonSize = self.settingsButton.update( transition: transition, component: AnyComponent(Button( content: AnyComponent( BundleIconComponent( name: "Chat/Input/Media/EntityInputSettingsIcon", tintColor: UIColor(rgb: 0xffffff) ) ), action: { if let controller = environment.controller() as? MediaEditorScreen { controller.requestSettings() } } )), environment: {}, containerSize: CGSize(width: 44.0, height: 44.0) ) let settingsButtonFrame = CGRect( origin: CGPoint(x: floorToScreenPixels((availableSize.width - settingsButtonSize.width) / 2.0), y: environment.safeInsets.top + 20.0 - inputPanelOffset), size: settingsButtonSize ) if let settingsButtonView = self.settingsButton.view { if settingsButtonView.superview == nil { settingsButtonView.layer.shadowOffset = CGSize(width: 0.0, height: 0.0) settingsButtonView.layer.shadowRadius = 2.0 settingsButtonView.layer.shadowColor = UIColor.black.cgColor settingsButtonView.layer.shadowOpacity = 0.35 //self.addSubview(settingsButtonView) } transition.setPosition(view: settingsButtonView, position: settingsButtonFrame.center) transition.setBounds(view: settingsButtonView, bounds: CGRect(origin: .zero, size: settingsButtonFrame.size)) transition.setScale(view: settingsButtonView, scale: displayTopButtons ? 1.0 : 0.01) transition.setAlpha(view: settingsButtonView, alpha: displayTopButtons && !component.isDismissing && !component.isInteractingWithEntities ? 1.0 : 0.0) } } let textCancelButtonSize = self.textCancelButton.update( transition: transition, component: AnyComponent(Button( content: AnyComponent( Text(text: environment.strings.Common_Cancel, font: Font.regular(17.0), color: .white) ), action: { if let controller = environment.controller() as? MediaEditorScreen { controller.node.interaction?.endTextEditing(reset: true) } } )), environment: {}, containerSize: CGSize(width: 100.0, height: 30.0) ) let textCancelButtonFrame = CGRect( origin: CGPoint(x: 13.0, y: environment.statusBarHeight + 20.0), size: textCancelButtonSize ) if let textCancelButtonView = self.textCancelButton.view { if textCancelButtonView.superview == nil { self.addSubview(textCancelButtonView) } transition.setPosition(view: textCancelButtonView, position: textCancelButtonFrame.center) transition.setBounds(view: textCancelButtonView, bounds: CGRect(origin: .zero, size: textCancelButtonFrame.size)) transition.setScale(view: textCancelButtonView, scale: isEditingTextEntity ? 1.0 : 0.01) transition.setAlpha(view: textCancelButtonView, alpha: isEditingTextEntity ? 1.0 : 0.0) } let textDoneButtonSize = self.textDoneButton.update( transition: transition, component: AnyComponent(Button( content: AnyComponent( Text(text: environment.strings.Common_Done, font: Font.regular(17.0), color: .white) ), action: { if let controller = environment.controller() as? MediaEditorScreen { controller.node.interaction?.endTextEditing(reset: false) } } )), environment: {}, containerSize: CGSize(width: 100.0, height: 30.0) ) let textDoneButtonFrame = CGRect( origin: CGPoint(x: availableSize.width - textDoneButtonSize.width - 13.0, y: environment.statusBarHeight + 20.0), size: textDoneButtonSize ) if let textDoneButtonView = self.textDoneButton.view { if textDoneButtonView.superview == nil { self.addSubview(textDoneButtonView) } transition.setPosition(view: textDoneButtonView, position: textDoneButtonFrame.center) transition.setBounds(view: textDoneButtonView, bounds: CGRect(origin: .zero, size: textDoneButtonFrame.size)) transition.setScale(view: textDoneButtonView, scale: isEditingTextEntity ? 1.0 : 0.01) transition.setAlpha(view: textDoneButtonView, alpha: isEditingTextEntity ? 1.0 : 0.0) } let textSizeSize = self.textSize.update( transition: transition, component: AnyComponent(TextSizeSliderComponent( value: sizeValue ?? 0.5, tag: nil, updated: { [weak state] size in if let controller = environment.controller() as? MediaEditorScreen { controller.node.interaction?.updateEntitySize(size) state?.updated() } }, released: { } )), environment: {}, containerSize: CGSize(width: 30.0, height: 240.0) ) let bottomInset: CGFloat = environment.inputHeight > 0.0 ? environment.inputHeight : environment.safeInsets.bottom let textSizeFrame = CGRect( origin: CGPoint(x: 0.0, y: environment.safeInsets.top + (availableSize.height - environment.safeInsets.top - bottomInset) / 2.0 - textSizeSize.height / 2.0), size: textSizeSize ) if let textSizeView = self.textSize.view { if textSizeView.superview == nil { self.addSubview(textSizeView) } transition.setPosition(view: textSizeView, position: textSizeFrame.center) transition.setBounds(view: textSizeView, bounds: CGRect(origin: .zero, size: textSizeFrame.size)) transition.setAlpha(view: textSizeView, alpha: sizeSliderVisible && !component.isInteractingWithEntities ? 1.0 : 0.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) private let storyMaxVideoDuration: Double = 60.0 public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate { public enum TransitionIn { public final class GalleryTransitionIn { public weak var sourceView: UIView? public let sourceRect: CGRect public let sourceImage: UIImage? public init( sourceView: UIView, sourceRect: CGRect, sourceImage: UIImage? ) { self.sourceView = sourceView self.sourceRect = sourceRect self.sourceImage = sourceImage } } case camera case gallery(GalleryTransitionIn) } public final class TransitionOut { public weak var destinationView: UIView? public let destinationRect: CGRect public let destinationCornerRadius: CGFloat public init( destinationView: UIView, destinationRect: CGRect, destinationCornerRadius: CGFloat ) { self.destinationView = destinationView self.destinationRect = destinationRect self.destinationCornerRadius = destinationCornerRadius } } struct State { var privacy: MediaEditorResultPrivacy = .story(privacy: EngineStoryPrivacy(base: .everyone, additionallyIncludePeers: []), timeout: 86400, archive: false) } var state = State() { didSet { self.node.requestUpdate() } } fileprivate final class Node: ViewControllerTracingNode, UIGestureRecognizerDelegate { private weak var controller: MediaEditorScreen? private let context: AccountContext fileprivate var interaction: DrawingToolsInteraction? private let initializationTimestamp = CACurrentMediaTime() fileprivate var subject: MediaEditorScreen.Subject? private var subjectDisposable: Disposable? private var appInForegroundDisposable: Disposable? private var wasPlaying = false private let backgroundDimView: UIView fileprivate let containerView: UIView fileprivate let componentHost: ComponentView fileprivate let storyPreview: ComponentView fileprivate let toolValue: ComponentView private let previewContainerView: UIView private var transitionInView: UIImageView? private let gradientView: UIImageView private var gradientColorsDisposable: Disposable? fileprivate let entitiesContainerView: UIView fileprivate let entitiesView: DrawingEntitiesView fileprivate let selectionContainerView: DrawingSelectionContainerView fileprivate let drawingView: DrawingView fileprivate let previewView: MediaEditorPreviewView fileprivate var mediaEditor: MediaEditor? private let stickerPickerInputData = Promise() private var dismissPanGestureRecognizer: UIPanGestureRecognizer? private var isDisplayingTool = false private var isInteractingWithEntities = false private var isEnhancing = false private var hasAppeared = false private var isDismissing = false private var dismissOffset: CGFloat = 0.0 private var isDismissed = false private var presentationData: PresentationData 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.isHidden = true self.backgroundDimView.backgroundColor = .black self.containerView = UIView() self.containerView.clipsToBounds = true self.componentHost = ComponentView() self.storyPreview = ComponentView() self.toolValue = 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.entitiesView.getEntityEdgePositions = { return UIEdgeInsets(top: 160.0, left: 36.0, bottom: storyDimensions.height - 160.0, right: storyDimensions.width - 36.0) } self.previewView = MediaEditorPreviewView(frame: .zero) self.drawingView = DrawingView(size: storyDimensions) self.drawingView.isUserInteractionEnabled = false self.selectionContainerView = DrawingSelectionContainerView(frame: .zero) self.entitiesView.selectionContainerView = self.selectionContainerView super.init() self.backgroundColor = .clear self.view.addSubview(self.backgroundDimView) self.view.addSubview(self.containerView) self.containerView.addSubview(self.previewContainerView) self.previewContainerView.addSubview(self.gradientView) self.previewContainerView.addSubview(self.entitiesContainerView) self.entitiesContainerView.addSubview(self.entitiesView) self.entitiesView.addSubview(self.drawingView) self.previewContainerView.addSubview(self.selectionContainerView) 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: true, 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: true, 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) }) self.entitiesView.edgePreviewUpdated = { [weak self] preview in if let self { let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut) if let storyPreviewView = self.storyPreview.view { transition.updateAlpha(layer: storyPreviewView.layer, alpha: preview ? 0.4 : 0.0) } } } self.appInForegroundDisposable = (controller.context.sharedContext.applicationBindings.applicationInForeground |> deliverOnMainQueue).start(next: { [weak self] inForeground in if let self, let mediaEditor = self.mediaEditor { if inForeground && self.wasPlaying { mediaEditor.play() } else if !inForeground { self.wasPlaying = mediaEditor.isPlaying mediaEditor.stop() } } }) } deinit { self.subjectDisposable?.dispose() self.gradientColorsDisposable?.dispose() self.appInForegroundDisposable?.dispose() } private func setup(with subject: MediaEditorScreen.Subject) { self.subject = subject guard let controller = self.controller else { return } let isSavingAvailable: Bool switch subject { case .image, .video: isSavingAvailable = true default: isSavingAvailable = false } controller.isSavingAvailable = isSavingAvailable controller.requestLayout(transition: .immediate) 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 = max(storyDimensions.width / fittedSize.width, storyDimensions.height / fittedSize.height) } else { mediaEntity.scale = storyDimensions.width / fittedSize.width } self.entitiesView.add(mediaEntity, announce: false) if case let .image(_, _, additionalImage, position) = subject, let additionalImage { let image = generateImage(CGSize(width: additionalImage.size.width, height: additionalImage.size.width), contextGenerator: { size, context in let bounds = CGRect(origin: .zero, size: size) context.clear(bounds) context.addEllipse(in: bounds) context.clip() if let cgImage = additionalImage.cgImage { context.draw(cgImage, in: CGRect(origin: CGPoint(x: (size.width - additionalImage.size.width) / 2.0, y: (size.height - additionalImage.size.height) / 2.0), size: additionalImage.size)) } }) let imageEntity = DrawingStickerEntity(content: .image(image ?? additionalImage)) imageEntity.referenceDrawingSize = storyDimensions imageEntity.scale = 1.49 imageEntity.mirrored = true imageEntity.position = position.getPosition(storyDimensions) self.entitiesView.add(imageEntity, announce: false) } else if case let .video(_, _, additionalVideoPath, additionalVideoImage, _, position) = subject, let additionalVideoPath { let videoEntity = DrawingStickerEntity(content: .video(additionalVideoPath, additionalVideoImage)) videoEntity.referenceDrawingSize = storyDimensions videoEntity.scale = 1.49 videoEntity.mirrored = true videoEntity.position = position.getPosition(storyDimensions) self.entitiesView.add(videoEntity, 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 { self.entitiesView.sendSubviewToBack(entityView) 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 initialValues: MediaEditorValues? if case let .draft(draft, _) = subject { initialValues = draft.values for entity in draft.values.entities { entitiesView.add(entity.entity, announce: false) } } else { initialValues = nil } let mediaEditor = MediaEditor(subject: subject.editorSubject, values: initialValues, hasHistogram: true) mediaEditor.attachPreviewView(self.previewView) mediaEditor.valuesUpdated = { [weak self] values in if let self, let controller = self.controller, values.gradientColors != nil, controller.previousSavedValues != values { if !isSavingAvailable && controller.previousSavedValues == nil { controller.previousSavedValues = values } else { controller.isSavingAvailable = true controller.requestLayout(transition: .animated(duration: 0.25, curve: .easeInOut)) } } } 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.backgroundDimView.isHidden = false }) } else { self.backgroundDimView.isHidden = false } } } }) self.mediaEditor = mediaEditor mediaEditor.onPlaybackAction = { [weak self] action in if let self { switch action { case .play: self.entitiesView.eachView({ view in if let sticker = view.entity as? DrawingStickerEntity, case .video = sticker.content { view.play() } }) case .pause: self.entitiesView.eachView({ view in if let sticker = view.entity as? DrawingStickerEntity, case .video = sticker.content { view.pause() } }) case let .seek(timestamp): self.entitiesView.eachView({ view in if let sticker = view.entity as? DrawingStickerEntity, case .video = sticker.content { view.seek(to: timestamp) } }) } } } } override func didLoad() { super.didLoad() self.view.disablesInteractiveModalDismiss = true self.view.disablesInteractiveKeyboardGestureRecognizer = true let dismissPanGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.handleDismissPan(_:))) dismissPanGestureRecognizer.delegate = self dismissPanGestureRecognizer.maximumNumberOfTouches = 1 self.previewContainerView.addGestureRecognizer(dismissPanGestureRecognizer) self.dismissPanGestureRecognizer = dismissPanGestureRecognizer 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) let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:))) tapGestureRecognizer.delegate = self self.previewContainerView.addGestureRecognizer(tapGestureRecognizer) self.interaction = DrawingToolsInteraction( context: self.context, drawingView: self.drawingView, entitiesView: self.entitiesView, selectionContainerView: self.selectionContainerView, isVideo: false, updateSelectedEntity: { [weak self] _ in if let self { self.requestUpdate(transition: .easeInOut(duration: 0.2)) } }, 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) } }, onInteractionUpdated: { [weak self] isInteracting in if let self { self.isInteractingWithEntities = isInteracting self.requestUpdate(transition: .easeInOut(duration: 0.2)) } }, 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 { if let panRecognizer = gestureRecognizer as? UIPanGestureRecognizer, panRecognizer.minimumNumberOfTouches == 1, panRecognizer.state == .changed { return false } else if let panRecognizer = otherGestureRecognizer as? UIPanGestureRecognizer, panRecognizer.minimumNumberOfTouches == 1, panRecognizer.state == .changed { return false } else if gestureRecognizer is UITapGestureRecognizer, (otherGestureRecognizer is UIPinchGestureRecognizer || otherGestureRecognizer is UIRotationGestureRecognizer) && otherGestureRecognizer.state == .changed { return false } return true } override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { if gestureRecognizer === self.dismissPanGestureRecognizer { if self.isDisplayingTool || self.entitiesView.hasSelection { return false } return true } else { return true } } private var enhanceGestureOffset: CGFloat? @objc func handleDismissPan(_ gestureRecognizer: UIPanGestureRecognizer) { guard let controller = self.controller, let layout = self.validLayout, (layout.inputHeight ?? 0.0).isZero else { return } var hasSwipeToDismiss = false if let subject = self.subject { if case .asset = subject { hasSwipeToDismiss = true } else if case .draft = subject { hasSwipeToDismiss = true } } let translation = gestureRecognizer.translation(in: self.view) let velocity = gestureRecognizer.velocity(in: self.view) switch gestureRecognizer.state { case .changed: if abs(translation.y) > 10.0 && !self.isEnhancing && hasSwipeToDismiss { if !self.isDismissing { self.isDismissing = true controller.requestLayout(transition: .animated(duration: 0.25, curve: .easeInOut)) } } else if abs(translation.x) > 10.0 && !self.isDismissing { self.isEnhancing = true controller.requestLayout(transition: .animated(duration: 0.3, curve: .easeInOut)) } if self.isDismissing { self.dismissOffset = translation.y controller.requestLayout(transition: .immediate) } else if self.isEnhancing { if let mediaEditor = self.mediaEditor { let value = mediaEditor.getToolValue(.enhance) as? Float ?? 0.0 let delta = Float((translation.x / self.frame.width) * 1.5) let updatedValue = max(-1.0, min(1.0, value + delta)) mediaEditor.setToolValue(.enhance, value: updatedValue) } self.requestUpdate() gestureRecognizer.setTranslation(.zero, in: self.view) } case .ended, .cancelled: if self.isDismissing { if abs(translation.y) > self.view.frame.height * 0.33 || abs(velocity.y) > 1000.0 { controller.requestDismiss(saveDraft: false, animated: true) } else { self.dismissOffset = 0.0 self.isDismissing = false controller.requestLayout(transition: .animated(duration: 0.4, curve: .spring)) } } else { self.isEnhancing = false Queue.mainQueue().after(0.5) { controller.requestLayout(transition: .animated(duration: 0.3, curve: .easeInOut)) } } default: break } } @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) } @objc func handleTap(_ gestureRecognizer: UITapGestureRecognizer) { let location = gestureRecognizer.location(in: self.view) var entitiesHitTestResult = self.entitiesView.hitTest(self.view.convert(location, to: self.entitiesView), with: nil) if entitiesHitTestResult is DrawingMediaEntityView { entitiesHitTestResult = nil } let selectionHitTestResult = self.selectionContainerView.hitTest(self.view.convert(location, to: self.selectionContainerView), with: nil) if entitiesHitTestResult == nil && selectionHitTestResult == nil { if self.entitiesView.hasSelection { self.entitiesView.selectEntity(nil) self.view.endEditing(true) } else { if let layout = self.validLayout, (layout.inputHeight ?? 0.0) > 0.0 { self.view.endEditing(true) } else { let textEntity = DrawingTextEntity(text: NSAttributedString(), style: .regular, animation: .none, font: .sanFrancisco, alignment: .center, fontSize: 1.0, color: DrawingColor(color: .white)) self.interaction?.insertEntity(textEntity) } } } } private func setupTransitionImage(_ image: UIImage) { self.previewContainerView.alpha = 1.0 let transitionInView = UIImageView(image: image) var initialScale: CGFloat if image.size.height > image.size.width { initialScale = max(self.previewContainerView.bounds.width / image.size.width, self.previewContainerView.bounds.height / image.size.height) } else { initialScale = self.previewContainerView.bounds.width / image.size.width } transitionInView.center = CGPoint(x: self.previewContainerView.bounds.width / 2.0, y: self.previewContainerView.bounds.height / 2.0) transitionInView.transform = CGAffineTransformMakeScale(initialScale, initialScale) self.previewContainerView.addSubview(transitionInView) self.transitionInView = transitionInView self.mediaEditor?.onFirstDisplay = { [weak self] in if let self, let transitionInView = self.transitionInView { self.transitionInView = nil transitionInView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak transitionInView] _ in transitionInView?.removeFromSuperview() }) } } } func animateIn() { let completion: () -> Void = { [weak self] in Queue.mainQueue().after(0.1) { self?.requestUpdate(hasAppeared: true, transition: .immediate) } } if let transitionIn = self.controller?.transitionIn { switch transitionIn { case .camera: if let view = self.componentHost.view as? MediaEditorScreenComponent.View { view.animateIn(from: .camera, completion: completion) } if let subject = self.subject, case let .video(_, transitionImage, _, _, _, _) = subject, let transitionImage { self.setupTransitionImage(transitionImage) } case let .gallery(transitionIn): if let sourceImage = transitionIn.sourceImage { self.setupTransitionImage(sourceImage) } if let sourceView = transitionIn.sourceView { if let view = self.componentHost.view as? MediaEditorScreenComponent.View { view.animateIn(from: .gallery) } let sourceLocalFrame = sourceView.convert(transitionIn.sourceRect, to: self.view) let sourceScale = sourceLocalFrame.width / self.previewContainerView.frame.width let sourceAspectRatio = sourceLocalFrame.height / sourceLocalFrame.width let duration: Double = 0.4 self.previewContainerView.layer.animatePosition(from: sourceLocalFrame.center, to: self.previewContainerView.center, duration: duration, timingFunction: kCAMediaTimingFunctionSpring, completion: { _ in completion() }) self.previewContainerView.layer.animateScale(from: sourceScale, to: 1.0, duration: duration, timingFunction: kCAMediaTimingFunctionSpring) self.previewContainerView.layer.animateBounds(from: CGRect(origin: CGPoint(x: 0.0, y: (self.previewContainerView.bounds.height - self.previewContainerView.bounds.width * sourceAspectRatio) / 2.0), size: CGSize(width: self.previewContainerView.bounds.width, height: self.previewContainerView.bounds.width * sourceAspectRatio)), to: self.previewContainerView.bounds, duration: duration, timingFunction: kCAMediaTimingFunctionSpring) self.backgroundDimView.isHidden = false self.backgroundDimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) if let componentView = self.componentHost.view { componentView.layer.animatePosition(from: sourceLocalFrame.center, to: componentView.center, duration: duration, timingFunction: kCAMediaTimingFunctionSpring) componentView.layer.animateScale(from: sourceScale, to: 1.0, duration: duration, timingFunction: kCAMediaTimingFunctionSpring) componentView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) } } } } else { if let view = self.componentHost.view as? MediaEditorScreenComponent.View { view.animateIn(from: .camera, completion: completion) } } // Queue.mainQueue().after(0.5) { // self.presentPrivacyTooltip() // } } func animateOut(finished: Bool, completion: @escaping () -> Void) { guard let controller = self.controller else { return } self.isDismissed = true controller.statusBar.statusBarStyle = .Ignore self.isUserInteractionEnabled = false let previousDimAlpha = self.backgroundDimView.alpha self.backgroundDimView.alpha = 0.0 self.backgroundDimView.layer.animateAlpha(from: previousDimAlpha, to: 0.0, duration: 0.15) if finished, case .message = controller.state.privacy { if let view = self.componentHost.view as? MediaEditorScreenComponent.View { view.animateOut(to: .camera) } let transition = Transition(animation: .curve(duration: 0.25, curve: .easeInOut)) transition.setAlpha(view: self.previewContainerView, alpha: 0.0, completion: { _ in completion() if let view = self.entitiesView.getView(where: { $0 is DrawingMediaEntityView }) as? DrawingMediaEntityView { view.previewView = nil } }) } else if let transitionOut = controller.transitionOut(finished), let destinationView = transitionOut.destinationView { var destinationTransitionView: UIView? if !finished { if let transitionIn = controller.transitionIn, case let .gallery(galleryTransitionIn) = transitionIn, let sourceImage = galleryTransitionIn.sourceImage { let sourceSuperView = galleryTransitionIn.sourceView?.superview?.superview let destinationTransitionOutView = UIImageView(image: sourceImage) destinationTransitionOutView.clipsToBounds = true destinationTransitionOutView.contentMode = .scaleAspectFill destinationTransitionOutView.frame = self.previewContainerView.convert(self.previewContainerView.bounds, to: sourceSuperView) sourceSuperView?.addSubview(destinationTransitionOutView) destinationTransitionView = destinationTransitionOutView } if let view = self.componentHost.view as? MediaEditorScreenComponent.View { view.animateOut(to: .gallery) } } let destinationLocalFrame = destinationView.convert(transitionOut.destinationRect, to: self.view) let destinationScale = destinationLocalFrame.width / self.previewContainerView.frame.width let destinationAspectRatio = destinationLocalFrame.height / destinationLocalFrame.width var destinationSnapshotView: UIView? if let destinationNode = destinationView.asyncdisplaykit_node, destinationNode is AvatarNode, let snapshotView = destinationView.snapshotView(afterScreenUpdates: false) { destinationView.isHidden = true snapshotView.layer.anchorPoint = CGPoint(x: 0.0, y: 0.5) let snapshotScale = self.previewContainerView.bounds.width / snapshotView.frame.width snapshotView.center = CGPoint(x: 0.0, y: self.previewContainerView.bounds.height / 2.0) snapshotView.layer.transform = CATransform3DMakeScale(snapshotScale, snapshotScale, 1.0) snapshotView.alpha = 0.0 Queue.mainQueue().after(0.15) { snapshotView.alpha = 1.0 snapshotView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) } self.previewContainerView.addSubview(snapshotView) destinationSnapshotView = snapshotView } self.previewContainerView.layer.animatePosition(from: self.previewContainerView.center, to: destinationLocalFrame.center, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in destinationView.isHidden = false destinationSnapshotView?.removeFromSuperview() completion() if let view = self.entitiesView.getView(where: { $0 is DrawingMediaEntityView }) as? DrawingMediaEntityView { view.previewView = nil } }) self.previewContainerView.layer.animateScale(from: 1.0, to: destinationScale, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) self.previewContainerView.layer.animateBounds(from: self.previewContainerView.bounds, to: CGRect(origin: CGPoint(x: 0.0, y: (self.previewContainerView.bounds.height - self.previewContainerView.bounds.width * destinationAspectRatio) / 2.0), size: CGSize(width: self.previewContainerView.bounds.width, height: self.previewContainerView.bounds.width * destinationAspectRatio)), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) if let destinationTransitionView { self.previewContainerView.layer.allowsGroupOpacity = true self.previewContainerView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false) destinationTransitionView.layer.animateFrame(from: destinationTransitionView.frame, to: destinationView.convert(destinationView.bounds, to: destinationTransitionView.superview), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { [weak destinationTransitionView] _ in destinationTransitionView?.removeFromSuperview() }) } let targetCornerRadius: CGFloat if transitionOut.destinationCornerRadius > 0.0 { targetCornerRadius = self.previewContainerView.bounds.width } else { targetCornerRadius = 0.0 } self.previewContainerView.layer.animate( from: self.previewContainerView.layer.cornerRadius as NSNumber, to: targetCornerRadius / 2.0 as NSNumber, keyPath: "cornerRadius", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.4, removeOnCompletion: false ) if let componentView = self.componentHost.view { componentView.clipsToBounds = true componentView.layer.animatePosition(from: componentView.center, to: destinationLocalFrame.center, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) componentView.layer.animateScale(from: 1.0, to: destinationScale, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) componentView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false) if finished { componentView.layer.animateBounds(from: componentView.bounds, to: CGRect(origin: CGPoint(x: 0.0, y: (componentView.bounds.height - componentView.bounds.width) / 2.0), size: CGSize(width: componentView.bounds.width, height: componentView.bounds.width)), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) componentView.layer.animate( from: componentView.layer.cornerRadius as NSNumber, to: componentView.bounds.width / 2.0 as NSNumber, keyPath: "cornerRadius", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.4, removeOnCompletion: false ) } } } else if let transitionIn = controller.transitionIn, case .camera = transitionIn { if let view = self.componentHost.view as? MediaEditorScreenComponent.View { view.animateOut(to: .camera) } 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() { self.isDisplayingTool = true let transition: Transition = .easeInOut(duration: 0.2) if let view = self.componentHost.view as? MediaEditorScreenComponent.View { view.animateOutToTool(transition: transition) } self.requestUpdate(transition: transition) } func animateInFromTool() { self.isDisplayingTool = false let transition: Transition = .easeInOut(duration: 0.2) if let view = self.componentHost.view as? MediaEditorScreenComponent.View { view.animateInFromTool(transition: transition) } self.requestUpdate(transition: transition) } func presentPrivacyTooltip() { guard let sourceView = self.componentHost.findTaggedView(tag: privacyButtonTag) else { return } let parentFrame = self.view.convert(self.bounds, to: nil) let absoluteFrame = sourceView.convert(sourceView.bounds, to: nil).offsetBy(dx: -parentFrame.minX, dy: 0.0) let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.maxY + 3.0), size: CGSize()) let tooltipController = TooltipScreen(account: self.context.account, sharedContext: self.context.sharedContext, text: "You can set who can view this story.", location: .point(location, .top), displayDuration: .manual, inset: 16.0, shouldDismissOnTouch: { _ in return .ignore }) self.controller?.present(tooltipController, in: .current) } private weak var muteTooltip: ViewController? func presentMutedTooltip() { guard let sourceView = self.componentHost.findTaggedView(tag: muteButtonTag) else { return } if let muteTooltip = self.muteTooltip { muteTooltip.dismiss() self.muteTooltip = nil } let isMuted = self.mediaEditor?.values.videoIsMuted ?? false let parentFrame = self.view.convert(self.bounds, to: nil) let absoluteFrame = sourceView.convert(sourceView.bounds, to: nil).offsetBy(dx: -parentFrame.minX, dy: 0.0) let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.maxY + 3.0), size: CGSize()) let tooltipController = TooltipScreen(account: self.context.account, sharedContext: self.context.sharedContext, text: isMuted ? "The story will have no sound." : "The story will have sound." , location: .point(location, .top), displayDuration: .default, inset: 16.0, shouldDismissOnTouch: { _ in return .ignore }) self.muteTooltip = tooltipController self.controller?.present(tooltipController, in: .current) } private weak var saveTooltip: SaveProgressScreen? func presentSaveTooltip() { guard let controller = self.controller else { return } if let saveTooltip = self.saveTooltip { if case .completion = saveTooltip.content { saveTooltip.dismiss() self.saveTooltip = nil } } let text: String let isVideo = self.mediaEditor?.resultIsVideo ?? false if isVideo { text = "Video saved to Photos." } else { text = "Image saved to Photos." } if let tooltipController = self.saveTooltip { tooltipController.content = .completion(text) } else { let tooltipController = SaveProgressScreen(context: self.context, content: .completion(text)) controller.present(tooltipController, in: .current) self.saveTooltip = tooltipController } } func updateEditProgress(_ progress: Float) { guard let controller = self.controller else { return } if let saveTooltip = self.saveTooltip { if case .completion = saveTooltip.content { saveTooltip.dismiss() self.saveTooltip = nil } } let text = "Uploading..." if let tooltipController = self.saveTooltip { tooltipController.content = .progress(text, progress) } else { let tooltipController = SaveProgressScreen(context: self.context, content: .progress(text, 0.0)) tooltipController.cancelled = { [weak self] in if let self, let controller = self.controller { controller.cancelVideoExport() } } controller.present(tooltipController, in: .current) self.saveTooltip = tooltipController } } func updateVideoExportProgress(_ progress: Float) { guard let controller = self.controller else { return } if let saveTooltip = self.saveTooltip { if case .completion = saveTooltip.content { saveTooltip.dismiss() self.saveTooltip = nil } } let text = "Preparing video..." if let tooltipController = self.saveTooltip { tooltipController.content = .progress(text, progress) } else { let tooltipController = SaveProgressScreen(context: self.context, content: .progress(text, 0.0)) tooltipController.cancelled = { [weak self] in if let self, let controller = self.controller { controller.cancelVideoExport() } } controller.present(tooltipController, in: .current) self.saveTooltip = tooltipController } } private weak var storyArchiveTooltip: ViewController? func presentStoryArchiveTooltip(sourceView: UIView) { guard let controller = self.controller, case let .story(_, _, archive) = controller.state.privacy else { return } if let storyArchiveTooltip = self.storyArchiveTooltip { storyArchiveTooltip.dismiss(animated: true) self.storyArchiveTooltip = nil } let parentFrame = self.view.convert(self.bounds, to: nil) let absoluteFrame = sourceView.convert(sourceView.bounds, to: nil).offsetBy(dx: -parentFrame.minX, dy: 0.0) let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.minY - 5.0), size: CGSize()) let text: String if archive { text = "Story will be kept on your page." } else { text = "Story will disappear in 24 hours." } let tooltipController = TooltipScreen(account: self.context.account, sharedContext: self.context.sharedContext, text: text, location: .point(location, .bottom), displayDuration: .default, inset: 7.0, cornerRadius: 9.0, shouldDismissOnTouch: { _ in return .ignore }) self.storyArchiveTooltip = tooltipController self.controller?.present(tooltipController, in: .current) } func updateModalTransitionFactor(_ value: CGFloat, transition: ContainedViewLayoutTransition) { guard let layout = self.validLayout, case .compact = layout.metrics.widthClass else { return } let progress = 1.0 - value let maxScale = (layout.size.width - 16.0 * 2.0) / layout.size.width let topInset: CGFloat = (layout.statusBarHeight ?? 0.0) + 12.0 let targetTopInset = ceil((layout.statusBarHeight ?? 0.0) - (layout.size.height - layout.size.height * maxScale) / 2.0) let deltaOffset = (targetTopInset - topInset) let scale = 1.0 * progress + (1.0 - progress) * maxScale let offset = (1.0 - progress) * deltaOffset transition.updateSublayerTransformScaleAndOffset(layer: self.containerView.layer, scale: scale, offset: CGPoint(x: 0.0, y: offset), beginWithCurrentState: true) } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { let result = super.hitTest(point, with: event) if result == self.componentHost.view { let point = self.view.convert(point, to: self.previewContainerView) return self.previewContainerView.hitTest(point, with: event) } return result } func requestUpdate(hasAppeared: Bool = false, transition: Transition = .immediate) { if let layout = self.validLayout { self.containerLayoutUpdated(layout: layout, hasAppeared: hasAppeared, transition: transition) } } private var drawingScreen: DrawingScreen? private var stickerScreen: StickerPickerScreen? func containerLayoutUpdated(layout: ContainerViewLayout, forceUpdate: Bool = false, hasAppeared: Bool = false, transition: Transition) { guard let controller = self.controller, !self.isDismissed else { return } let isFirstTime = self.validLayout == nil self.validLayout = layout let isTablet: Bool if case .regular = layout.metrics.widthClass { isTablet = true } else { isTablet = false } let topInset: CGFloat = (layout.statusBarHeight ?? 0.0) + 12.0 let previewSize: CGSize if isTablet { let previewHeight = layout.size.height - topInset - 75.0 previewSize = CGSize(width: floorToScreenPixels(previewHeight / 1.77778), height: previewHeight) } else { previewSize = CGSize(width: layout.size.width, height: floorToScreenPixels(layout.size.width * 1.77778)) } let bottomInset = layout.size.height - previewSize.height - topInset var inputHeight = layout.inputHeight ?? 0.0 if self.stickerScreen != nil { inputHeight = 0.0 } let environment = ViewControllerComponentContainer.Environment( statusBarHeight: layout.statusBarHeight ?? 0.0, navigationHeight: 0.0, safeInsets: UIEdgeInsets( top: topInset, left: layout.safeInsets.left, bottom: bottomInset, right: layout.safeInsets.right ), inputHeight: inputHeight, 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 } ) if hasAppeared && !self.hasAppeared { self.hasAppeared = hasAppeared } let componentSize = self.componentHost.update( transition: transition, component: AnyComponent( MediaEditorScreenComponent( context: self.context, isDisplayingTool: self.isDisplayingTool, isInteractingWithEntities: self.isInteractingWithEntities, isSavingAvailable: controller.isSavingAvailable, hasAppeared: self.hasAppeared, isDismissing: self.isDismissing, mediaEditor: self.mediaEditor, privacy: controller.state.privacy, selectedEntity: self.isDisplayingTool ? nil : self.entitiesView.selectedEntityView?.entity, entityViewForEntity: { [weak self] entity in if let self { return self.entitiesView.getView(for: entity.uuid) } else { return nil } }, 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 { if let file { let stickerEntity = DrawingStickerEntity(content: .file(file)) self.interaction?.insertEntity(stickerEntity) self.controller?.isSavingAvailable = true self.controller?.requestLayout(transition: .immediate) } self.stickerScreen = nil } } controller.customModalStyleOverlayTransitionFactorUpdated = { [weak self, weak controller] transition in if let self, let controller { let transitionFactor = controller.modalStyleOverlayTransitionFactor self.updateModalTransitionFactor(transitionFactor, transition: transition) } } self.stickerScreen = controller self.controller?.present(controller, in: .current) return case .text: let textEntity = DrawingTextEntity(text: NSAttributedString(), style: .regular, animation: .none, font: .sanFrancisco, alignment: .center, fontSize: 1.0, color: DrawingColor(color: .white)) self.interaction?.insertEntity(textEntity) self.controller?.isSavingAvailable = true self.controller?.requestLayout(transition: .immediate) return case .drawing: self.interaction?.deactivate() 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() self?.interaction?.activate() 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?.interaction?.activate() self?.entitiesView.selectEntity(nil) } self.controller?.present(controller, in: .current) 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 { self.animateInFromTool() } } self.controller?.present(controller, in: .current) self.animateOutToTool() } } ) ), environment: { environment }, forceUpdate: forceUpdate, containerSize: layout.size ) if let componentView = self.componentHost.view { if componentView.superview == nil { self.containerView.addSubview(componentView) componentView.clipsToBounds = true } transition.setFrame(view: componentView, frame: CGRect(origin: CGPoint(x: 0.0, y: self.dismissOffset), size: componentSize)) } let storyPreviewSize = self.storyPreview.update( transition: transition, component: AnyComponent( StoryPreviewComponent( context: self.context, caption: "" ) ), environment: {}, forceUpdate: false, containerSize: previewSize ) if let storyPreviewView = self.storyPreview.view { if storyPreviewView.superview == nil { storyPreviewView.alpha = 0.0 storyPreviewView.isUserInteractionEnabled = false self.previewContainerView.addSubview(storyPreviewView) } transition.setFrame(view: storyPreviewView, frame: CGRect(origin: CGPoint(x: 0.0, y: self.dismissOffset), size: storyPreviewSize)) } let enhanceValue = self.mediaEditor?.getToolValue(.enhance) as? Float ?? 0.0 let toolValueSize = self.toolValue.update( transition: transition, component: AnyComponent( ToolValueComponent( title: "Enhance", value: "\(Int(abs(enhanceValue) * 100.0))" ) ), environment: {}, forceUpdate: false, containerSize: CGSize(width: previewSize.width, height: 120.0) ) if let toolValueView = self.toolValue.view { if toolValueView.superview == nil { toolValueView.alpha = 0.0 toolValueView.isUserInteractionEnabled = false self.previewContainerView.addSubview(toolValueView) } transition.setFrame(view: toolValueView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((previewSize.width - toolValueSize.width) / 2.0), y: 88.0), size: toolValueSize)) transition.setAlpha(view: toolValueView, alpha: self.isEnhancing ? 1.0 : 0.0) } transition.setFrame(view: self.backgroundDimView, frame: CGRect(origin: .zero, size: layout.size)) transition.setAlpha(view: self.backgroundDimView, alpha: self.isDismissing ? 0.0 : 1.0) var bottomInputOffset: CGFloat = 0.0 if inputHeight > 0.0 { if self.stickerScreen == nil { if self.entitiesView.selectedEntityView != nil || self.isDisplayingTool { bottomInputOffset = inputHeight / 2.0 } else { bottomInputOffset = inputHeight - bottomInset - 17.0 } } } transition.setPosition(view: self.containerView, position: CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0)) transition.setBounds(view: self.containerView, bounds: CGRect(origin: .zero, size: layout.size)) let previewFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - previewSize.width) / 2.0), y: topInset - bottomInputOffset + self.dismissOffset), size: previewSize) transition.setFrame(view: self.previewContainerView, frame: previewFrame) let entitiesViewScale = previewSize.width / storyDimensions.width self.entitiesContainerView.transform = CGAffineTransformMakeScale(entitiesViewScale, entitiesViewScale) 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: self.entitiesView.bounds.size)) transition.setFrame(view: self.selectionContainerView, frame: CGRect(origin: .zero, size: previewFrame.size)) self.interaction?.containerLayoutUpdated(layout: layout, transition: transition) if isFirstTime { self.animateIn() } } } fileprivate var node: Node { return self.displayNode as! Node } public enum PIPPosition { case topLeft case topRight case bottomLeft case bottomRight func getPosition(_ size: CGSize) -> CGPoint { switch self { case .topLeft: return CGPoint(x: 224.0, y: 477.0) case .topRight: return CGPoint(x: size.width - 224.0, y: 477.0) case .bottomLeft: return CGPoint(x: 224.0, y: size.height - 477.0) case .bottomRight: return CGPoint(x: size.width - 224.0, y: size.height - 477.0) } } } public enum Subject { case image(UIImage, PixelDimensions, UIImage?, PIPPosition) case video(String, UIImage?, String?, UIImage?, PixelDimensions, PIPPosition) case asset(PHAsset) case draft(MediaEditorDraft, Int64?) 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)) case let .draft(draft, _): return draft.dimensions } } var editorSubject: MediaEditor.Subject { switch self { case let .image(image, dimensions, _, _): return .image(image, dimensions) case let .video(videoPath, transitionImage, _, _, dimensions, _): return .video(videoPath, transitionImage, dimensions) case let .asset(asset): return .asset(asset) case let .draft(draft, _): return .draft(draft) } } 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) case let .draft(draft, _): return .image(draft.thumbnail, draft.dimensions) } } } public enum Result { public enum VideoResult { case imageFile(path: String) case videoFile(path: String) case asset(localIdentifier: String) } case image(image: UIImage, dimensions: PixelDimensions, caption: NSAttributedString?) case video(video: VideoResult, coverImage: UIImage?, values: MediaEditorValues, duration: Double, dimensions: PixelDimensions, caption: NSAttributedString?) } fileprivate let context: AccountContext fileprivate let subject: Signal fileprivate let transitionIn: TransitionIn? fileprivate let transitionOut: (Bool) -> TransitionOut? public var cancelled: (Bool) -> Void = { _ in } public var completion: (Int64, MediaEditorScreen.Result, MediaEditorResultPrivacy, @escaping (@escaping () -> Void) -> Void) -> Void = { _, _, _, _ in } public var dismissed: () -> Void = { } private let hapticFeedback = HapticFeedback() public init( context: AccountContext, subject: Signal, transitionIn: TransitionIn?, transitionOut: @escaping (Bool) -> TransitionOut?, completion: @escaping (Int64, MediaEditorScreen.Result, MediaEditorResultPrivacy, @escaping (@escaping () -> Void) -> Void) -> Void ) { self.context = context self.subject = subject self.transitionIn = transitionIn self.transitionOut = transitionOut self.completion = completion if let transitionIn, case .camera = transitionIn { self.isSavingAvailable = true } 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") } deinit { self.exportDisposable.dispose() } override public func loadDisplayNode() { self.displayNode = Node(controller: self) super.displayNodeDidLoad() let dropInteraction = UIDropInteraction(delegate: self) self.displayNode.view.addInteraction(dropInteraction) } func openPrivacySettings() { self.hapticFeedback.impact(.light) if case .message(_, _) = self.state.privacy { self.openSendAsMessage() } else { let stateContext = ShareWithPeersScreen.StateContext(context: self.context, subject: .stories) let _ = (stateContext.ready |> filter { $0 } |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in guard let self else { return } var archive = true var timeout: Int = 86400 let initialPrivacy: EngineStoryPrivacy if case let .story(privacy, timeoutValue, archiveValue) = self.state.privacy { initialPrivacy = privacy timeout = timeoutValue archive = archiveValue } else { initialPrivacy = EngineStoryPrivacy(base: .everyone, additionallyIncludePeers: []) } self.push( ShareWithPeersScreen( context: self.context, initialPrivacy: initialPrivacy, stateContext: stateContext, completion: { [weak self] privacy in guard let self else { return } self.state.privacy = .story(privacy: privacy, timeout: timeout, archive: archive) }, editCategory: { [weak self] privacy in guard let self else { return } self.openEditCategory(privacy: privacy, completion: { [weak self] privacy in guard let self else { return } self.state.privacy = .story(privacy: privacy, timeout: timeout, archive: archive) self.openPrivacySettings() }) }, secondaryAction: { [weak self] in guard let self else { return } self.openSendAsMessage() } ) ) }) } } private func openEditCategory(privacy: EngineStoryPrivacy, completion: @escaping (EngineStoryPrivacy) -> Void) { let stateContext = ShareWithPeersScreen.StateContext(context: self.context, subject: .contacts(privacy.base)) let _ = (stateContext.ready |> filter { $0 } |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in guard let self else { return } self.push( ShareWithPeersScreen( context: self.context, initialPrivacy: privacy, stateContext: stateContext, completion: { [weak self] result in guard let self else { return } if case .closeFriends = privacy.base { let _ = self.context.engine.privacy.updateCloseFriends(peerIds: result.additionallyIncludePeers).start() } completion(result) }, editCategory: { _ in }, secondaryAction: { [weak self] in guard let self else { return } self.openSendAsMessage() } ) ) }) } private func openSendAsMessage() { var initialPeerIds = Set() if case let .message(peers, _) = self.state.privacy { initialPeerIds = Set(peers) } let stateContext = ShareWithPeersScreen.StateContext(context: self.context, subject: .chats, initialPeerIds: initialPeerIds) let _ = (stateContext.ready |> filter { $0 } |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in guard let self else { return } self.push( ShareWithPeersScreen( context: self.context, initialPrivacy: EngineStoryPrivacy(base: .everyone, additionallyIncludePeers: []), stateContext: stateContext, completion: { [weak self] privacy in guard let self else { return } self.state.privacy = .message(peers: privacy.additionallyIncludePeers, timeout: nil) }, editCategory: { _ in }, secondaryAction: {} ) ) }) } func presentTimeoutSetup(sourceView: UIView) { self.hapticFeedback.impact(.light) var items: [ContextMenuItem] = [] let updateTimeout: (Int?, Bool) -> Void = { [weak self] timeout, archive in guard let self else { return } switch self.state.privacy { case let .story(privacy, _, _): self.state.privacy = .story(privacy: privacy, timeout: timeout ?? 86400, archive: archive) case let .message(peers, _): self.state.privacy = .message(peers: peers, timeout: timeout) } } var currentValue: Int? var currentArchived = false let emptyAction: ((ContextMenuActionItem.Action) -> Void)? = nil let title: String switch self.state.privacy { case let .story(_, timeoutValue, archivedValue): title = "Choose how long the story will be visible." currentValue = timeoutValue currentArchived = archivedValue case let .message(_, timeoutValue): title = "Choose how long the media will be kept after opening." currentValue = timeoutValue } items.append(.action(ContextMenuActionItem(text: title, textLayout: .multiline, textFont: .small, icon: { _ in return nil }, action: emptyAction))) switch self.state.privacy { case .story: items.append(.action(ContextMenuActionItem(text: "6 Hours", icon: { theme in return currentValue == 3600 * 6 ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil }, action: { _, a in a(.default) updateTimeout(3600 * 6, false) }))) items.append(.action(ContextMenuActionItem(text: "12 Hours", icon: { theme in return currentValue == 3600 * 12 ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil }, action: { _, a in a(.default) updateTimeout(3600 * 12, false) }))) items.append(.action(ContextMenuActionItem(text: "24 Hours", icon: { theme in return currentValue == 86400 && !currentArchived ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil }, action: { _, a in a(.default) updateTimeout(86400, false) }))) items.append(.action(ContextMenuActionItem(text: "48 Hours", icon: { theme in return currentValue == 86400 * 2 ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil }, action: { _, a in a(.default) updateTimeout(86400 * 2, false) }))) items.append(.action(ContextMenuActionItem(text: "Keep Always", icon: { theme in return currentArchived ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil }, action: { _, a in a(.default) updateTimeout(86400, true) }))) items.append(.separator) items.append(.action(ContextMenuActionItem(text: "Select 'Keep Always' to always show the story in your profile.", textLayout: .multiline, textFont: .small, icon: { theme in return nil }, action: { _, _ in }))) case .message: items.append(.action(ContextMenuActionItem(text: "Until First View", icon: { _ in return nil }, action: { _, a in a(.default) updateTimeout(1, false) }))) items.append(.action(ContextMenuActionItem(text: "3 Seconds", icon: { _ in return nil }, action: { _, a in a(.default) updateTimeout(3, false) }))) items.append(.action(ContextMenuActionItem(text: "10 Seconds", icon: { _ in return nil }, action: { _, a in a(.default) updateTimeout(10, false) }))) items.append(.action(ContextMenuActionItem(text: "1 Minute", icon: { _ in return nil }, action: { _, a in a(.default) updateTimeout(60, false) }))) items.append(.action(ContextMenuActionItem(text: "Keep Always", icon: { _ in return nil }, action: { _, a in a(.default) updateTimeout(nil, false) }))) } let presentationData = self.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkPresentationTheme) let contextController = ContextController(account: self.context.account, presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: self, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), gesture: nil) self.present(contextController, in: .window(.root)) } func maybePresentDiscardAlert() { self.hapticFeedback.impact(.light) if "".isEmpty { self.requestDismiss(saveDraft: false, animated: true) return } if let subject = self.node.subject, case .asset = subject, self.node.mediaEditor?.values.hasChanges == false { self.requestDismiss(saveDraft: false, animated: true) return } let title: String let save: String if case .draft = self.node.subject { title = "Discard Draft?" save = "Keep Draft" } else { title = "Discard Media?" save = "Save Draft" } let theme = defaultDarkPresentationTheme let controller = textAlertController( context: self.context, forceTheme: theme, title: title, text: "If you go back now, you will lose any changes that you've made.", actions: [ TextAlertAction(type: .destructiveAction, title: "Discard", action: { [weak self] in if let self { self.requestDismiss(saveDraft: false, animated: true) } }), TextAlertAction(type: .genericAction, title: save, action: { [weak self] in if let self { self.requestDismiss(saveDraft: true, animated: true) } }), TextAlertAction(type: .genericAction, title: "Cancel", action: { }) ], actionLayout: .vertical ) self.present(controller, in: .window(.root)) } func requestDismiss(saveDraft: Bool, animated: Bool) { self.dismissAllTooltips() if saveDraft { self.saveDraft(id: nil) } else { // if case let .draft(draft, _) = self.node.subject { // removeStoryDraft(engine: self.context.engine, path: draft.path, delete: true) // } } if let mediaEditor = self.node.mediaEditor { mediaEditor.invalidate() } self.node.entitiesView.invalidate() self.cancelled(saveDraft) self.node.animateOut(finished: false, completion: { [weak self] in self?.dismiss() self?.dismissed() }) } private func saveDraft(id: Int64?) { guard let subject = self.node.subject, let values = self.node.mediaEditor?.values else { return } try? FileManager.default.createDirectory(atPath: draftPath(), withIntermediateDirectories: true) let privacy = self.state.privacy if let resultImage = self.node.mediaEditor?.resultImage { self.node.mediaEditor?.seek(0.0, andPlay: false) makeEditorImageComposition(account: self.context.account, inputImage: resultImage, dimensions: storyDimensions, values: values, time: .zero, completion: { resultImage in guard let resultImage else { return } let fittedSize = resultImage.size.aspectFitted(CGSize(width: 128.0, height: 128.0)) let saveImageDraft: (UIImage, PixelDimensions) -> Void = { image, dimensions in if let thumbnailImage = generateScaledImage(image: resultImage, size: fittedSize) { let path = draftPath() + "/\(Int64.random(in: .min ... .max)).jpg" if let data = image.jpegData(compressionQuality: 0.87) { try? data.write(to: URL(fileURLWithPath: path)) let draft = MediaEditorDraft(path: path, isVideo: false, thumbnail: thumbnailImage, dimensions: dimensions, values: values, caption: NSAttributedString(), privacy: privacy) if let id { saveStorySource(engine: self.context.engine, item: draft, id: id) } else { addStoryDraft(engine: self.context.engine, item: draft) } } } } let saveVideoDraft: (String, PixelDimensions) -> Void = { videoPath, dimensions in if let thumbnailImage = generateScaledImage(image: resultImage, size: fittedSize) { let path = draftPath() + "/\(Int64.random(in: .min ... .max)).mp4" try? FileManager.default.moveItem(atPath: videoPath, toPath: path) let draft = MediaEditorDraft(path: path, isVideo: true, thumbnail: thumbnailImage, dimensions: dimensions, values: values, caption: NSAttributedString(), privacy: privacy) if let id { saveStorySource(engine: self.context.engine, item: draft, id: id) } else { addStoryDraft(engine: self.context.engine, item: draft) } } } switch subject { case let .image(image, dimensions, _, _): saveImageDraft(image, dimensions) case let .video(path, _, _, _, dimensions, _): saveVideoDraft(path, dimensions) case let .asset(asset): if asset.mediaType == .video { PHImageManager.default().requestAVAsset(forVideo: asset, options: nil) { avAsset, _, _ in if let urlAsset = avAsset as? AVURLAsset { saveVideoDraft(urlAsset.url.absoluteString, PixelDimensions(width: Int32(asset.pixelWidth), height: Int32(asset.pixelHeight))) } } } else { let options = PHImageRequestOptions() options.deliveryMode = .highQualityFormat PHImageManager.default().requestImage(for: asset, targetSize: PHImageManagerMaximumSize, contentMode: .default, options: options) { image, _ in if let image { saveImageDraft(image, PixelDimensions(image.size)) } } } case let .draft(draft, _): if draft.isVideo { saveVideoDraft(draft.path, draft.dimensions) } else if let image = UIImage(contentsOfFile: draft.path) { saveImageDraft(image, draft.dimensions) } // if let thumbnailImage = generateScaledImage(image: resultImage, size: fittedSize) { // removeStoryDraft(engine: self.context.engine, path: draft.path, delete: false) // let draft = MediaEditorDraft(path: draft.path, isVideo: draft.isVideo, thumbnail: thumbnailImage, dimensions: draft.dimensions, values: values) // addStoryDraft(engine: self.context.engine, item: draft) // } } }) } } private var didComplete = false func requestCompletion(caption: NSAttributedString, animated: Bool) { guard let mediaEditor = self.node.mediaEditor, let subject = self.node.subject, !self.didComplete else { return } self.didComplete = true self.dismissAllTooltips() mediaEditor.seek(0.0, andPlay: false) mediaEditor.invalidate() self.node.entitiesView.invalidate() if let navigationController = self.navigationController as? NavigationController { navigationController.updateRootContainerTransitionOffset(0.0, transition: .immediate) } let entities = self.node.entitiesView.entities.filter { !($0 is DrawingMediaEntity) } let codableEntities = DrawingEntitiesView.encodeEntities(entities, entitiesView: self.node.entitiesView) mediaEditor.setDrawingAndEntities(data: nil, image: mediaEditor.values.drawing, entities: codableEntities) let randomId: Int64 if case let .draft(_, id) = subject, let id { randomId = id } else { randomId = Int64.random(in: .min ... .max) } if mediaEditor.resultIsVideo { let videoResult: Result.VideoResult let duration: Double switch subject { case let .image(image, _, _, _): let tempImagePath = NSTemporaryDirectory() + "\(Int64.random(in: Int64.min ... Int64.max)).jpg" if let data = image.jpegData(compressionQuality: 0.85) { try? data.write(to: URL(fileURLWithPath: tempImagePath)) } videoResult = .imageFile(path: tempImagePath) duration = 5.0 case let .video(path, _, _, _, _, _): videoResult = .videoFile(path: path) if let videoTrimRange = mediaEditor.values.videoTrimRange { duration = videoTrimRange.upperBound - videoTrimRange.lowerBound } else { duration = 5.0 } case let .asset(asset): videoResult = .asset(localIdentifier: asset.localIdentifier) if asset.mediaType == .video { if let videoTrimRange = mediaEditor.values.videoTrimRange { duration = videoTrimRange.upperBound - videoTrimRange.lowerBound } else { duration = asset.duration } } else { duration = 5.0 } case let .draft(draft, _): if draft.isVideo { videoResult = .videoFile(path: draft.path) if let videoTrimRange = mediaEditor.values.videoTrimRange { duration = videoTrimRange.upperBound - videoTrimRange.lowerBound } else { duration = 5.0 } } else { videoResult = .imageFile(path: draft.path) duration = 5.0 } } // makeEditorImageComposition(account: self.context.account, inputImage: image, dimensions: storyDimensions, values: mediaEditor.values, time: .zero, completion: { [weak self] coverImage in // if let self { self.completion(randomId, .video(video: videoResult, coverImage: nil, values: mediaEditor.values, duration: duration, dimensions: mediaEditor.values.resultDimensions, caption: caption), self.state.privacy, { [weak self] finished in self?.node.animateOut(finished: true, completion: { [weak self] in self?.dismiss() Queue.mainQueue().justDispatch { finished() } }) }) // } // }) if case let .draft(draft, id) = subject, id == nil { removeStoryDraft(engine: self.context.engine, path: draft.path, delete: true) } } else { if let image = mediaEditor.resultImage { self.saveDraft(id: randomId) makeEditorImageComposition(account: self.context.account, inputImage: image, dimensions: storyDimensions, values: mediaEditor.values, time: .zero, completion: { [weak self] resultImage in if let self, let resultImage { self.completion(randomId, .image(image: resultImage, dimensions: PixelDimensions(resultImage.size), caption: caption), self.state.privacy, { [weak self] finished in self?.node.animateOut(finished: true, completion: { [weak self] in self?.dismiss() Queue.mainQueue().justDispatch { finished() } }) }) if case let .draft(draft, id) = subject, id == nil { removeStoryDraft(engine: self.context.engine, path: draft.path, delete: true) } } }) } } } private var videoExport: MediaEditorVideoExport? private var exportDisposable = MetaDisposable() fileprivate var isSavingAvailable = false private var previousSavedValues: MediaEditorValues? func requestSave() { guard let mediaEditor = self.node.mediaEditor, let subject = self.node.subject, self.isSavingAvailable else { return } let entities = self.node.entitiesView.entities.filter { !($0 is DrawingMediaEntity) } let codableEntities = DrawingEntitiesView.encodeEntities(entities, entitiesView: self.node.entitiesView) mediaEditor.setDrawingAndEntities(data: nil, image: mediaEditor.values.drawing, entities: codableEntities) if let previousSavedValues = self.previousSavedValues, mediaEditor.values == previousSavedValues { return } self.hapticFeedback.impact(.light) self.previousSavedValues = mediaEditor.values self.isSavingAvailable = false self.requestLayout(transition: .animated(duration: 0.25, curve: .easeInOut)) 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 { mediaEditor.stop() self.node.entitiesView.pause() let exportSubject: Signal switch subject { case let .video(path, _, _, _, _, _): let asset = AVURLAsset(url: NSURL(fileURLWithPath: path) as URL) exportSubject = .single(.video(asset)) case let .image(image, _, _, _): exportSubject = .single(.image(image)) case let .asset(asset): exportSubject = Signal { subscriber in if asset.mediaType == .video { PHImageManager.default().requestAVAsset(forVideo: asset, options: nil) { avAsset, _, _ in if let avAsset { subscriber.putNext(.video(avAsset)) subscriber.putCompletion() } } } else { let options = PHImageRequestOptions() options.deliveryMode = .highQualityFormat PHImageManager.default().requestImage(for: asset, targetSize: PHImageManagerMaximumSize, contentMode: .default, options: options) { image, _ in if let image { subscriber.putNext(.image(image)) subscriber.putCompletion() } } } return EmptyDisposable } case let .draft(draft, _): if draft.isVideo { let asset = AVURLAsset(url: NSURL(fileURLWithPath: draft.path) as URL) exportSubject = .single(.video(asset)) } else { if let image = UIImage(contentsOfFile: draft.path) { exportSubject = .single(.image(image)) } else { fatalError() } } } let _ = exportSubject.start(next: { [weak self] exportSubject in guard let self else { return } let configuration = recommendedVideoExportConfiguration(values: mediaEditor.values, forceFullHd: true, frameRate: 60.0) let outputPath = NSTemporaryDirectory() + "\(Int64.random(in: 0 ..< .max)).mp4" let videoExport = MediaEditorVideoExport(account: self.context.account, subject: exportSubject, configuration: configuration, outputPath: outputPath) self.videoExport = videoExport videoExport.start() self.exportDisposable.set((videoExport.status |> deliverOnMainQueue).start(next: { [weak self] status in if let self { switch status { case .completed: self.videoExport = nil saveToPhotos(outputPath, true) self.node.presentSaveTooltip() self.node.mediaEditor?.play() self.node.entitiesView.play() case let .progress(progress): if self.videoExport != nil { self.node.updateVideoExportProgress(progress) } case .failed: self.videoExport = nil self.node.mediaEditor?.play() self.node.entitiesView.play() case .unknown: break } } })) }) } else { if let image = mediaEditor.resultImage { Queue.concurrentDefaultQueue().async { makeEditorImageComposition(account: self.context.account, inputImage: image, dimensions: storyDimensions, values: mediaEditor.values, time: .zero, completion: { resultImage in if let data = resultImage?.jpegData(compressionQuality: 0.8) { let outputPath = NSTemporaryDirectory() + "\(Int64.random(in: 0 ..< .max)).jpg" try? data.write(to: URL(fileURLWithPath: outputPath)) Queue.mainQueue().async { saveToPhotos(outputPath, false) } } }) } self.node.presentSaveTooltip() } } } func requestSettings() { } fileprivate func cancelVideoExport() { if let videoExport = self.videoExport { self.previousSavedValues = nil videoExport.cancel() self.videoExport = nil self.exportDisposable.set(nil) self.node.mediaEditor?.play() self.node.entitiesView.play() } } public func updateEditProgress(_ progress: Float) { self.node.updateEditProgress(progress) } private func dismissAllTooltips() { self.window?.forEachController({ controller in if let controller = controller as? TooltipScreen { controller.dismiss() } }) self.forEachController({ controller in if let controller = controller as? TooltipScreen { controller.dismiss() } if let controller = controller as? SaveProgressScreen { controller.dismiss() } return true }) } override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) (self.displayNode as! Node).containerLayoutUpdated(layout: layout, transition: Transition(transition)) } @available(iOSApplicationExtension 11.0, iOS 11.0, *) public func dropInteraction(_ interaction: UIDropInteraction, canHandle session: UIDropSession) -> Bool { return session.hasItemsConforming(toTypeIdentifiers: [kUTTypeImage as String]) } @available(iOSApplicationExtension 11.0, iOS 11.0, *) public func dropInteraction(_ interaction: UIDropInteraction, sessionDidUpdate session: UIDropSession) -> UIDropProposal { let operation: UIDropOperation operation = .copy return UIDropProposal(operation: operation) } @available(iOSApplicationExtension 11.0, iOS 11.0, *) public func dropInteraction(_ interaction: UIDropInteraction, performDrop session: UIDropSession) { session.loadObjects(ofClass: UIImage.self) { [weak self] imageItems in guard let self else { return } let images = imageItems as! [UIImage] if images.count == 1, let image = images.first, max(image.size.width, image.size.height) > 1.0 { self.node.interaction?.insertEntity(DrawingStickerEntity(content: .image(image))) } } } @available(iOSApplicationExtension 11.0, iOS 11.0, *) public func dropInteraction(_ interaction: UIDropInteraction, sessionDidExit session: UIDropSession) { } @available(iOSApplicationExtension 11.0, iOS 11.0, *) public func dropInteraction(_ interaction: UIDropInteraction, sessionDidEnd session: UIDropSession) { } } final class PrivacyButtonComponent: CombinedComponent { let backgroundColor: UIColor let icon: UIImage let text: String init( backgroundColor: UIColor, icon: UIImage, text: String ) { self.backgroundColor = backgroundColor self.icon = icon self.text = text } static func ==(lhs: PrivacyButtonComponent, rhs: PrivacyButtonComponent) -> Bool { if lhs.backgroundColor != rhs.backgroundColor { return false } if lhs.text != rhs.text { return false } return true } static var body: Body { let background = Child(BlurredBackgroundComponent.self) let icon = Child(Image.self) let text = Child(Text.self) return { context in let icon = icon.update( component: Image(image: context.component.icon, size: CGSize(width: 9.0, height: 11.0)), availableSize: CGSize(width: 180.0, height: 100.0), transition: .immediate ) let text = text.update( component: Text( text: "\(context.component.text)", font: Font.medium(14.0), color: .white ), availableSize: CGSize(width: 180.0, height: 100.0), transition: .immediate ) let backgroundSize = CGSize(width: text.size.width + 38.0, height: 30.0) let background = background.update( component: BlurredBackgroundComponent(color: context.component.backgroundColor), availableSize: backgroundSize, transition: .immediate ) context.add(background .position(CGPoint(x: backgroundSize.width / 2.0, y: backgroundSize.height / 2.0)) .cornerRadius(min(backgroundSize.width, backgroundSize.height) / 2.0) .clipsToBounds(true) ) context.add(icon .position(CGPoint(x: 16.0, y: backgroundSize.height / 2.0)) ) context.add(text .position(CGPoint(x: backgroundSize.width / 2.0 + 7.0, y: backgroundSize.height / 2.0)) ) return backgroundSize } } } private final class HeaderContextReferenceContentSource: ContextReferenceContentSource { private let controller: ViewController private let sourceView: UIView var keepInPlace: Bool { return true } init(controller: ViewController, sourceView: UIView) { self.controller = controller self.sourceView = sourceView } func transitionInfo() -> ContextControllerReferenceViewInfo? { return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds, actionsPosition: .top) } } private func draftPath() -> String { return NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] + "/storyDrafts" } private final class ToolValueComponent: Component { typealias EnvironmentType = Empty let title: String let value: String init( title: String, value: String ) { self.title = title self.value = value } static func ==(lhs: ToolValueComponent, rhs: ToolValueComponent) -> Bool { if lhs.title != rhs.title { return false } if lhs.value != rhs.value { return false } return true } public final class View: UIView { private let title = ComponentView() private let value = ComponentView() private let hapticFeedback = HapticFeedback() private var component: ToolValueComponent? private weak var state: EmptyComponentState? override init(frame: CGRect) { super.init(frame: frame) self.backgroundColor = .clear } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func update(component: ToolValueComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { let previousValue = self.component?.value self.component = component self.state = state let titleSize = self.title.update( transition: .immediate, component: AnyComponent(Text( text: component.title, font: Font.light(34.0), color: .white )), environment: {}, containerSize: CGSize(width: 180.0, height: 44.0) ) let titleFrame = CGRect( origin: CGPoint(x: floorToScreenPixels((availableSize.width - titleSize.width) / 2.0), y: 0.0), size: titleSize ) if let titleView = self.title.view { if titleView.superview == nil { titleView.layer.shadowOffset = CGSize(width: 0.0, height: 0.0) titleView.layer.shadowRadius = 3.0 titleView.layer.shadowColor = UIColor.black.cgColor titleView.layer.shadowOpacity = 0.35 self.addSubview(titleView) } transition.setPosition(view: titleView, position: titleFrame.center) titleView.bounds = CGRect(origin: .zero, size: titleFrame.size) } let valueSize = self.value.update( transition: .immediate, component: AnyComponent(Text( text: component.value, font: Font.with(size: 90.0, weight: .thin, traits: .monospacedNumbers), color: .white )), environment: {}, containerSize: CGSize(width: 180.0, height: 44.0) ) let valueFrame = CGRect( origin: CGPoint(x: floorToScreenPixels((availableSize.width - valueSize.width) / 2.0), y: 40.0), size: valueSize ) if let valueView = self.value.view { if valueView.superview == nil { valueView.layer.shadowOffset = CGSize(width: 0.0, height: 0.0) valueView.layer.shadowRadius = 3.0 valueView.layer.shadowColor = UIColor.black.cgColor valueView.layer.shadowOpacity = 0.35 self.addSubview(valueView) } transition.setPosition(view: valueView, position: valueFrame.center) valueView.bounds = CGRect(origin: .zero, size: valueFrame.size) } if let previousValue, component.value != previousValue, self.alpha > 0.0 { self.hapticFeedback.impact(.click05) } return availableSize } } func makeView() -> View { return View() } public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } }