Camera and editor improvements

This commit is contained in:
Ilya Laktyushin 2023-06-17 22:27:07 +04:00
parent 0882817bed
commit f1218abc9b
43 changed files with 3260 additions and 1880 deletions

View File

@ -892,7 +892,7 @@ public protocol SharedAccountContext: AnyObject {
func makeStickerPackScreen(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, mainStickerPack: StickerPackReference, stickerPacks: [StickerPackReference], loadedStickerPacks: [LoadedStickerPack], parentNavigationController: NavigationController?, sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)?) -> ViewController
func makeMediaPickerScreen(context: AccountContext, completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping () -> (UIView, CGRect)?, @escaping () -> Void) -> Void, dismissed: @escaping () -> Void) -> ViewController
func makeMediaPickerScreen(context: AccountContext, getSourceRect: @escaping () -> CGRect, completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping () -> (UIView, CGRect)?, @escaping () -> Void) -> Void, dismissed: @escaping () -> Void) -> ViewController
func makeProxySettingsController(sharedContext: SharedAccountContext, account: UnauthorizedAccount) -> ViewController

View File

@ -313,6 +313,9 @@ public enum ChatTextInputStateTextAttributeType: Codable, Equatable {
case textMention(EnginePeer.Id)
case textUrl(String)
case customEmoji(stickerPack: StickerPackReference?, fileId: Int64)
case strikethrough
case underline
case spoiler
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: StringCodingKey.self)
@ -334,6 +337,12 @@ public enum ChatTextInputStateTextAttributeType: Codable, Equatable {
let stickerPack = try container.decodeIfPresent(StickerPackReference.self, forKey: "s")
let fileId = try container.decode(Int64.self, forKey: "f")
self = .customEmoji(stickerPack: stickerPack, fileId: fileId)
case 6:
self = .strikethrough
case 7:
self = .underline
case 8:
self = .spoiler
default:
assertionFailure()
self = .bold
@ -359,6 +368,12 @@ public enum ChatTextInputStateTextAttributeType: Codable, Equatable {
try container.encode(5 as Int32, forKey: "t")
try container.encodeIfPresent(stickerPack, forKey: "s")
try container.encode(fileId, forKey: "f")
case .strikethrough:
try container.encode(6 as Int32, forKey: "t")
case .underline:
try container.encode(7 as Int32, forKey: "t")
case .spoiler:
try container.encode(8 as Int32, forKey: "t")
}
}
}
@ -426,6 +441,12 @@ public struct ChatTextInputStateText: Codable, Equatable {
parsedAttributes.append(ChatTextInputStateTextAttribute(type: .textUrl(value.url), range: range.location ..< (range.location + range.length)))
} else if key == ChatTextInputAttributes.customEmoji, let value = value as? ChatTextInputTextCustomEmojiAttribute {
parsedAttributes.append(ChatTextInputStateTextAttribute(type: .customEmoji(stickerPack: nil, fileId: value.fileId), range: range.location ..< (range.location + range.length)))
} else if key == ChatTextInputAttributes.strikethrough {
parsedAttributes.append(ChatTextInputStateTextAttribute(type: .strikethrough, range: range.location ..< (range.location + range.length)))
} else if key == ChatTextInputAttributes.underline {
parsedAttributes.append(ChatTextInputStateTextAttribute(type: .underline, range: range.location ..< (range.location + range.length)))
} else if key == ChatTextInputAttributes.spoiler {
parsedAttributes.append(ChatTextInputStateTextAttribute(type: .spoiler, range: range.location ..< (range.location + range.length)))
}
}
})
@ -464,6 +485,12 @@ public struct ChatTextInputStateText: Codable, Equatable {
result.addAttribute(ChatTextInputAttributes.textUrl, value: ChatTextInputTextUrlAttribute(url: url), range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count))
case let .customEmoji(_, fileId):
result.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: fileId, file: nil), range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count))
case .strikethrough:
result.addAttribute(ChatTextInputAttributes.strikethrough, value: true as NSNumber, range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count))
case .underline:
result.addAttribute(ChatTextInputAttributes.underline, value: true as NSNumber, range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count))
case .spoiler:
result.addAttribute(ChatTextInputAttributes.spoiler, value: true as NSNumber, range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count))
}
}
return result

View File

@ -678,6 +678,7 @@ public class AttachmentController: ViewController {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak self] _ in
let _ = self?.container.dismiss(transition: .immediate, completion: completion)
self?.animating = false
self?.layer.removeAllAnimations()
})
} else {
let positionTransition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut)
@ -745,12 +746,12 @@ public class AttachmentController: ViewController {
let position: CGPoint
let positionY = layout.size.height - size.height - insets.bottom - 40.0
if let sourceRect = controller.getSourceRect?() {
position = CGPoint(x: floor(sourceRect.midX - size.width / 2.0), y: min(positionY, sourceRect.minY - size.height))
position = CGPoint(x: min(layout.size.width - size.width - 28.0, floor(sourceRect.midX - size.width / 2.0)), y: min(positionY, sourceRect.minY - size.height))
} else {
position = CGPoint(x: masterWidth - 174.0, y: positionY)
}
if controller.isStandalone {
if controller.isStandalone && !controller.forceSourceRect {
var containerY = floorToScreenPixels((layout.size.height - size.height) / 2.0)
if let inputHeight = layout.inputHeight, inputHeight > 88.0 {
containerY = layout.size.height - inputHeight - size.height - 80.0
@ -933,6 +934,8 @@ public class AttachmentController: ViewController {
fatalError("init(coder:) has not been implemented")
}
public var forceSourceRect = false
fileprivate var isStandalone: Bool {
return self.buttons.contains(.standalone)
}

View File

@ -37,12 +37,15 @@ final class CameraDeviceContext {
private weak var session: CameraSession?
private weak var previewView: CameraSimplePreviewView?
private let exclusive: Bool
let device = CameraDevice()
let input = CameraInput()
let output = CameraOutput()
init(session: CameraSession) {
init(session: CameraSession, exclusive: Bool) {
self.session = session
self.exclusive = exclusive
}
func configure(position: Camera.Position, previewView: CameraSimplePreviewView?, audio: Bool, photo: Bool, metadata: Bool) {
@ -81,6 +84,9 @@ final class CameraDeviceContext {
}
private var preferredMaxFrameRate: Double {
if !self.exclusive {
return 30.0
}
switch DeviceModel.current {
case .iPhone14ProMax, .iPhone13ProMax:
return 60.0
@ -95,7 +101,7 @@ private final class CameraContext {
private let session: CameraSession
private let mainDeviceContext: CameraDeviceContext
private var mainDeviceContext: CameraDeviceContext
private var additionalDeviceContext: CameraDeviceContext?
private let cameraImageContext = CIContext()
@ -162,7 +168,7 @@ private final class CameraContext {
self.simplePreviewView = previewView
self.secondaryPreviewView = secondaryPreviewView
self.mainDeviceContext = CameraDeviceContext(session: session)
self.mainDeviceContext = CameraDeviceContext(session: session, exclusive: true)
self.configure {
self.mainDeviceContext.configure(position: configuration.position, previewView: self.simplePreviewView, audio: configuration.audio, photo: configuration.photo, metadata: configuration.metadata)
}
@ -306,9 +312,29 @@ private final class CameraContext {
self.modeChange = .dualCamera
if enabled {
self.configure {
self.additionalDeviceContext = CameraDeviceContext(session: self.session)
self.mainDeviceContext.invalidate()
self.mainDeviceContext = CameraDeviceContext(session: self.session, exclusive: false)
self.mainDeviceContext.configure(position: .back, previewView: self.simplePreviewView, audio: self.initialConfiguration.audio, photo: self.initialConfiguration.photo, metadata: self.initialConfiguration.metadata)
self.additionalDeviceContext = CameraDeviceContext(session: self.session, exclusive: false)
self.additionalDeviceContext?.configure(position: .front, previewView: self.secondaryPreviewView, audio: false, photo: true, metadata: false)
}
self.mainDeviceContext.output.processSampleBuffer = { [weak self] sampleBuffer, pixelBuffer, connection in
guard let self else {
return
}
self.previewNode?.enqueue(sampleBuffer)
let timestamp = CACurrentMediaTime()
if timestamp > self.lastSnapshotTimestamp + 2.5 {
var mirror = false
if #available(iOS 13.0, *) {
mirror = connection.inputPorts.first?.sourceDevicePosition == .front
}
self.savePreviewSnapshot(pixelBuffer: pixelBuffer, mirror: mirror, additional: false)
self.lastSnapshotTimestamp = timestamp
}
}
self.additionalDeviceContext?.output.processSampleBuffer = { [weak self] sampleBuffer, pixelBuffer, connection in
guard let self else {
return
@ -325,9 +351,29 @@ private final class CameraContext {
}
} else {
self.configure {
self.mainDeviceContext.invalidate()
self.mainDeviceContext = CameraDeviceContext(session: self.session, exclusive: true)
self.mainDeviceContext.configure(position: .back, previewView: self.simplePreviewView, audio: self.initialConfiguration.audio, photo: self.initialConfiguration.photo, metadata: self.initialConfiguration.metadata)
self.additionalDeviceContext?.invalidate()
self.additionalDeviceContext = nil
}
self.mainDeviceContext.output.processSampleBuffer = { [weak self] sampleBuffer, pixelBuffer, connection in
guard let self else {
return
}
self.previewNode?.enqueue(sampleBuffer)
let timestamp = CACurrentMediaTime()
if timestamp > self.lastSnapshotTimestamp + 2.5 {
var mirror = false
if #available(iOS 13.0, *) {
mirror = connection.inputPorts.first?.sourceDevicePosition == .front
}
self.savePreviewSnapshot(pixelBuffer: pixelBuffer, mirror: mirror, additional: false)
self.lastSnapshotTimestamp = timestamp
}
}
}
self.queue.after(0.5) {
@ -394,11 +440,33 @@ private final class CameraContext {
}
public func startRecording() -> Signal<Double, NoError> {
return self.mainDeviceContext.output.startRecording()
if let additionalDeviceContext = self.additionalDeviceContext {
return combineLatest(
self.mainDeviceContext.output.startRecording(),
additionalDeviceContext.output.startRecording()
) |> map { value, _ in
return value
}
} else {
return self.mainDeviceContext.output.startRecording()
}
}
public func stopRecording() -> Signal<(String, UIImage?)?, NoError> {
return self.mainDeviceContext.output.stopRecording()
public func stopRecording() -> Signal<VideoCaptureResult, NoError> {
if let additionalDeviceContext = self.additionalDeviceContext {
return combineLatest(
self.mainDeviceContext.output.stopRecording(),
additionalDeviceContext.output.stopRecording()
) |> mapToSignal { main, additional in
if case let .finished(mainResult, _, _) = main, case let .finished(additionalResult, _, _) = additional {
return .single(.finished(mainResult, additionalResult, CACurrentMediaTime()))
} else {
return .complete()
}
}
} else {
return self.mainDeviceContext.output.stopRecording()
}
}
var detectedCodes: Signal<[CameraCode], NoError> {
@ -559,7 +627,7 @@ public final class Camera {
}
}
public func stopRecording() -> Signal<(String, UIImage?)?, NoError> {
public func stopRecording() -> Signal<VideoCaptureResult, NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.queue.async {

View File

@ -6,6 +6,28 @@ import CoreImage
import Vision
import VideoToolbox
public enum VideoCaptureResult: Equatable {
case finished((String, UIImage), (String, UIImage)?, Double)
case failed
public static func == (lhs: VideoCaptureResult, rhs: VideoCaptureResult) -> Bool {
switch lhs {
case .failed:
if case .failed = rhs {
return true
} else {
return false
}
case let .finished(_, _, lhsTime):
if case let .finished(_, _, rhsTime) = rhs, lhsTime == rhsTime {
return true
} else {
return false
}
}
}
}
public struct CameraCode: Equatable {
public enum CodeType {
case qr
@ -272,7 +294,7 @@ final class CameraOutput: NSObject {
}
}
private var recordingCompletionPipe = ValuePipe<(String, UIImage?)?>()
private var recordingCompletionPipe = ValuePipe<VideoCaptureResult>()
func startRecording() -> Signal<Double, NoError> {
guard self.videoRecorder == nil else {
return .complete()
@ -288,18 +310,16 @@ final class CameraOutput: NSObject {
guard let videoSettings = self.videoOutput.recommendedVideoSettings(forVideoCodecType: codecType, assetWriterOutputFileType: .mp4) else {
return .complete()
}
guard let audioSettings = self.audioOutput.recommendedAudioSettingsForAssetWriter(writingTo: .mp4) else {
return .complete()
}
let audioSettings = self.audioOutput.recommendedAudioSettingsForAssetWriter(writingTo: .mp4) ?? [:]
let outputFileName = NSUUID().uuidString
let outputFilePath = NSTemporaryDirectory() + outputFileName + ".mp4"
let outputFileURL = URL(fileURLWithPath: outputFilePath)
let videoRecorder = VideoRecorder(configuration: VideoRecorder.Configuration(videoSettings: videoSettings, audioSettings: audioSettings), videoTransform: CGAffineTransform(rotationAngle: .pi / 2.0), fileUrl: outputFileURL, completion: { [weak self] result in
if case let .success(transitionImage) = result {
self?.recordingCompletionPipe.putNext((outputFilePath, transitionImage))
self?.recordingCompletionPipe.putNext(.finished((outputFilePath, transitionImage!), nil, CACurrentMediaTime()))
} else {
self?.recordingCompletionPipe.putNext(nil)
self?.recordingCompletionPipe.putNext(.failed)
}
})
@ -318,7 +338,7 @@ final class CameraOutput: NSObject {
}
}
func stopRecording() -> Signal<(String, UIImage?)?, NoError> {
func stopRecording() -> Signal<VideoCaptureResult, NoError> {
self.videoRecorder?.stop()
return self.recordingCompletionPipe.signal()

View File

@ -1191,18 +1191,19 @@ public final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDele
return bandingStart + (1.0 - (1.0 / ((bandedOffset * coefficient / range) + 1.0))) * range
}
let cameraIsAlreadyOpened = self.controller?.hasStoryCameraTransition ?? false
if selectedIndex <= 0 && translation.x > 0.0 {
transitionFraction = 0.0
self.controller?.storyCameraPanGestureChanged(transitionFraction: translation.x / layout.size.width)
} else if translation.x <= 0.0 && cameraIsAlreadyOpened {
self.controller?.storyCameraPanGestureChanged(transitionFraction: 0.0)
}
if cameraIsAlreadyOpened {
transitionFraction = 0.0
return
if case .compact = layout.metrics.widthClass {
let cameraIsAlreadyOpened = self.controller?.hasStoryCameraTransition ?? false
if selectedIndex <= 0 && translation.x > 0.0 {
transitionFraction = 0.0
self.controller?.storyCameraPanGestureChanged(transitionFraction: translation.x / layout.size.width)
} else if translation.x <= 0.0 && cameraIsAlreadyOpened {
self.controller?.storyCameraPanGestureChanged(transitionFraction: 0.0)
}
if cameraIsAlreadyOpened {
transitionFraction = 0.0
return
}
}
if selectedIndex >= maxFilterIndex && translation.x < 0.0 {

View File

@ -321,7 +321,7 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
if setup {
text.referenceDrawingSize = self.size
text.width = floor(self.size.width * 0.9)
text.fontSize = 0.3
text.fontSize = 0.08
text.scale = zoomScale
}
}
@ -415,9 +415,16 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
let newEntity = entity.duplicate()
self.prepareNewEntity(newEntity, setup: false, relativeTo: entity)
guard let view = makeEntityView(context: self.context, entity: entity) else {
guard let view = makeEntityView(context: self.context, entity: newEntity) else {
fatalError()
}
if let initialView = self.getView(for: entity.uuid) {
view.onSnapUpdated = initialView.onSnapUpdated
view.onPositionUpdated = initialView.onPositionUpdated
view.onInteractionUpdated = initialView.onInteractionUpdated
}
view.containerView = self
view.update()
self.addSubview(view)
@ -516,6 +523,12 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
return nil
}
public func eachView(_ f: (DrawingEntityView) -> Void) {
for case let view as DrawingEntityView in self.subviews {
f(view)
}
}
public func play() {
for case let view as DrawingEntityView in self.subviews {
view.play()
@ -700,15 +713,15 @@ public class DrawingEntityView: UIView {
return self.bounds
}
func play() {
public func play() {
}
func pause() {
public func pause() {
}
func seek(to timestamp: Double) {
public func seek(to timestamp: Double) {
}

View File

@ -43,17 +43,17 @@ public final class DrawingMediaEntityView: DrawingEntityView, DrawingEntityMedia
}
}
override func play() {
public override func play() {
self.isVisible = true
self.applyVisibility()
}
override func pause() {
public override func pause() {
self.isVisible = false
self.applyVisibility()
}
override func seek(to timestamp: Double) {
public override func seek(to timestamp: Double) {
self.isVisible = false
self.isPlaying = false

View File

@ -738,7 +738,7 @@ private final class DrawingScreenComponent: CombinedComponent {
areUnicodeEmojiEnabled: true,
areCustomEmojiEnabled: true,
chatPeerId: context.account.peerId,
hasSearch: false,
hasSearch: true,
forceHasPremium: true
)
@ -749,7 +749,7 @@ private final class DrawingScreenComponent: CombinedComponent {
stickerNamespaces: [Namespaces.ItemCollection.CloudStickerPacks],
stickerOrderedItemListCollectionIds: [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers, Namespaces.OrderedItemList.CloudAllPremiumStickers],
chatPeerId: context.account.peerId,
hasSearch: false,
hasSearch: true,
hasTrending: true,
forceHasPremium: true
)
@ -761,7 +761,7 @@ private final class DrawingScreenComponent: CombinedComponent {
stickerNamespaces: [Namespaces.ItemCollection.CloudMaskPacks],
stickerOrderedItemListCollectionIds: [],
chatPeerId: context.account.peerId,
hasSearch: false,
hasSearch: true,
hasTrending: false,
forceHasPremium: true
)
@ -1153,8 +1153,14 @@ private final class DrawingScreenComponent: CombinedComponent {
controlsAreVisible = false
}
let previewSize = CGSize(width: context.availableSize.width, height: floorToScreenPixels(context.availableSize.width * 1.77778))
let previewSize: CGSize
let previewTopInset: CGFloat = environment.statusBarHeight + 12.0
if case .regular = environment.metrics.widthClass {
let previewHeight = context.availableSize.height - previewTopInset - 75.0
previewSize = CGSize(width: floorToScreenPixels(previewHeight / 1.77778), height: previewHeight)
} else {
previewSize = CGSize(width: context.availableSize.width, height: floorToScreenPixels(context.availableSize.width * 1.77778))
}
let previewBottomInset = context.availableSize.height - previewSize.height - previewTopInset
var topInset = environment.safeInsets.top + 31.0
@ -1646,7 +1652,7 @@ private final class DrawingScreenComponent: CombinedComponent {
transition: .immediate
)
context.add(fillButton
.position(CGPoint(x: context.availableSize.width / 2.0 - (hasFlip ? 46.0 : 0.0), y: environment.safeInsets.top + 31.0))
.position(CGPoint(x: context.availableSize.width / 2.0 - (hasFlip ? 46.0 : 0.0), y: topInset))
.appear(.default(scale: true))
.disappear(.default(scale: true))
)
@ -1678,7 +1684,7 @@ private final class DrawingScreenComponent: CombinedComponent {
transition: .immediate
)
context.add(flipButton
.position(CGPoint(x: context.availableSize.width / 2.0 + (isFilled != nil ? 46.0 : 0.0), y: environment.safeInsets.top + 31.0))
.position(CGPoint(x: context.availableSize.width / 2.0 + (isFilled != nil ? 46.0 : 0.0), y: topInset))
.appear(.default(scale: true))
.disappear(.default(scale: true))
.shadow(component.sourceHint == .storyEditor ? Shadow(color: UIColor(rgb: 0x000000, alpha: 0.35), radius: 2.0, offset: .zero) : nil)
@ -1988,6 +1994,9 @@ private final class DrawingScreenComponent: CombinedComponent {
var doneButtonPosition = CGPoint(x: context.availableSize.width - environment.safeInsets.right - doneButton.size.width / 2.0 - 3.0, y: context.availableSize.height - environment.safeInsets.bottom - doneButton.size.height / 2.0 - 2.0 - UIScreenPixel)
if component.sourceHint == .storyEditor {
doneButtonPosition.x = doneButtonPosition.x - 2.0
if case .regular = environment.metrics.widthClass {
doneButtonPosition.x -= 20.0
}
doneButtonPosition.y = floorToScreenPixels(context.availableSize.height - previewBottomInset + 3.0 + doneButton.size.height / 2.0)
}
context.add(doneButton
@ -2105,6 +2114,9 @@ private final class DrawingScreenComponent: CombinedComponent {
var backButtonPosition = CGPoint(x: environment.safeInsets.left + backButton.size.width / 2.0 + 3.0, y: context.availableSize.height - environment.safeInsets.bottom - backButton.size.height / 2.0 - 2.0 - UIScreenPixel)
if component.sourceHint == .storyEditor {
backButtonPosition.x = backButtonPosition.x + 2.0
if case .regular = environment.metrics.widthClass {
backButtonPosition.x += 20.0
}
backButtonPosition.y = floorToScreenPixels(context.availableSize.height - previewBottomInset + 3.0 + backButton.size.height / 2.0)
}
context.add(backButton

View File

@ -1,5 +1,6 @@
import Foundation
import UIKit
import AVFoundation
import Display
import SwiftSignalKit
import TelegramCore
@ -21,6 +22,10 @@ final class DrawingStickerEntityView: DrawingEntityView {
private let imageNode: TransformImageNode
private var animationNode: AnimatedStickerNode?
private var videoPlayer: AVPlayer?
private var videoLayer: AVPlayerLayer?
private var videoImageView: UIImageView?
private var didSetUpAnimationNode = false
private let stickerFetchedDisposable = MetaDisposable()
private let cachedDisposable = MetaDisposable()
@ -63,12 +68,27 @@ final class DrawingStickerEntityView: DrawingEntityView {
}
}
private var video: String? {
if case let .video(path, _) = self.stickerEntity.content {
return path
} else {
return nil
}
}
private var dimensions: CGSize {
switch self.stickerEntity.content {
case let .file(file):
return file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0)
case let .image(image):
return image.size
case let .file(file):
return file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0)
case let .image(image):
return image.size
case let .video(_, image):
if let image {
let minSide = min(image.size.width, image.size.height)
return CGSize(width: minSide, height: minSide)
} else {
return CGSize(width: 512.0, height: 512.0)
}
}
}
@ -119,23 +139,64 @@ final class DrawingStickerEntityView: DrawingEntityView {
return context
}))
self.setNeedsLayout()
} else if case let .video(videoPath, image) = self.stickerEntity.content {
let url = URL(fileURLWithPath: videoPath)
let asset = AVURLAsset(url: url)
let playerItem = AVPlayerItem(asset: asset)
let player = AVPlayer(playerItem: playerItem)
player.automaticallyWaitsToMinimizeStalling = false
let layer = AVPlayerLayer(player: player)
layer.masksToBounds = true
layer.videoGravity = .resizeAspectFill
self.layer.addSublayer(layer)
self.videoPlayer = player
self.videoLayer = layer
let imageView = UIImageView(image: image)
imageView.clipsToBounds = true
imageView.contentMode = .scaleAspectFill
self.addSubview(imageView)
self.videoImageView = imageView
}
}
override func play() {
self.isVisible = true
self.applyVisibility()
if let player = self.videoPlayer {
player.play()
if let videoImageView = self.videoImageView {
self.videoImageView = nil
Queue.mainQueue().after(0.1) {
videoImageView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.1, removeOnCompletion: false, completion: { [weak videoImageView] _ in
videoImageView?.removeFromSuperview()
})
}
}
}
}
override func pause() {
self.isVisible = false
self.applyVisibility()
if let player = self.videoPlayer {
player.pause()
}
}
override func seek(to timestamp: Double) {
self.isVisible = false
self.isPlaying = false
self.animationNode?.seekTo(.timestamp(timestamp))
if let player = self.videoPlayer {
player.seek(to: CMTime(seconds: timestamp, preferredTimescale: CMTimeScale(60.0)), toleranceBefore: .zero, toleranceAfter: .zero, completionHandler: { _ in })
}
}
override func resetToStart() {
@ -184,10 +245,11 @@ final class DrawingStickerEntityView: DrawingEntityView {
let boundingSize = CGSize(width: sideSize, height: sideSize)
let imageSize = self.dimensions.aspectFitted(boundingSize)
let imageFrame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: (size.height - imageSize.height) / 2.0), size: imageSize)
self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))()
self.imageNode.frame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: (size.height - imageSize.height) / 2.0), size: imageSize)
self.imageNode.frame = imageFrame
if let animationNode = self.animationNode {
animationNode.frame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: (size.height - imageSize.height) / 2.0), size: imageSize)
animationNode.frame = imageFrame
animationNode.updateLayout(size: imageSize)
if !self.didApplyVisibility {
@ -195,6 +257,16 @@ final class DrawingStickerEntityView: DrawingEntityView {
self.applyVisibility()
}
}
if let videoLayer = self.videoLayer {
videoLayer.cornerRadius = imageFrame.width / 2.0
videoLayer.frame = imageFrame
}
if let videoImageView = self.videoImageView {
videoImageView.layer.cornerRadius = imageFrame.width / 2.0
videoImageView.frame = imageFrame
}
self.update(animated: false)
}
}
@ -226,13 +298,19 @@ final class DrawingStickerEntityView: DrawingEntityView {
UIView.animate(withDuration: 0.25, animations: {
self.imageNode.transform = animationTargetTransform
self.animationNode?.transform = animationTargetTransform
self.videoLayer?.transform = animationTargetTransform
}, completion: { finished in
self.imageNode.transform = staticTransform
self.animationNode?.transform = staticTransform
self.videoLayer?.transform = staticTransform
})
} else {
CATransaction.begin()
CATransaction.setDisableActions(true)
self.imageNode.transform = staticTransform
self.animationNode?.transform = staticTransform
self.videoLayer?.transform = staticTransform
CATransaction.commit()
}
super.update(animated: animated)

View File

@ -35,6 +35,7 @@ public struct StickerPickerInputData: Equatable {
private final class StickerSelectionComponent: Component {
typealias EnvironmentType = Empty
let context: AccountContext
let theme: PresentationTheme
let strings: PresentationStrings
let deviceMetrics: DeviceMetrics
@ -44,6 +45,7 @@ private final class StickerSelectionComponent: Component {
let separatorColor: UIColor
init(
context: AccountContext,
theme: PresentationTheme,
strings: PresentationStrings,
deviceMetrics: DeviceMetrics,
@ -52,6 +54,7 @@ private final class StickerSelectionComponent: Component {
backgroundColor: UIColor,
separatorColor: UIColor
) {
self.context = context
self.theme = theme
self.strings = strings
self.deviceMetrics = deviceMetrics
@ -129,6 +132,7 @@ private final class StickerSelectionComponent: Component {
let topPanelHeight: CGFloat = 42.0
//let context = component.context
let keyboardSize = self.keyboardView.update(
transition: transition.withUserData(EmojiPagerContentComponent.SynchronousLoadBehavior(isDisabled: true)),
component: AnyComponent(EntityKeyboardComponent(
@ -153,7 +157,44 @@ private final class StickerSelectionComponent: Component {
switchToTextInput: {},
switchToGifSubject: { _ in },
reorderItems: { _, _ in },
makeSearchContainerNode: { _ in return nil },
makeSearchContainerNode: { _ in
return nil
},
// makeSearchContainerNode: { [weak self, weak controllerInteraction] content in
// guard let self, let controllerInteraction = controllerInteraction else {
// return nil
// }
//
// let mappedMode: ChatMediaInputSearchMode
// switch content {
// case .stickers:
// mappedMode = .sticker
// case .gifs:
// mappedMode = .sticker
// }
//
// let presentationData = context.sharedContext.currentPresentationData.with { $0 }
// let searchContainerNode = PaneSearchContainerNode(
// context: context,
// theme: presentationData.theme,
// strings: presentationData.strings,
// controllerInteraction: controllerInteraction,
// inputNodeInteraction: inputNodeInteraction,
// mode: mappedMode,
// trendingGifsPromise: Promise(nil),
// cancel: {
// },
// peekBehavior: self.emojiInputInteraction?.peekBehavior
// )
// searchContainerNode.openGifContextMenu = { [weak self] item, sourceNode, sourceRect, gesture, isSaved in
// guard let self else {
// return
// }
// self.openGifContextMenu(file: item.file, contextResult: item.contextResult, sourceView: sourceNode.view, sourceRect: sourceRect, gesture: gesture, isSaved: isSaved)
// }
//
// return searchContainerNode
// },
contentIdUpdated: { _ in },
deviceMetrics: component.deviceMetrics,
hiddenInputHeight: 0.0,
@ -225,6 +266,39 @@ public class StickerPickerScreen: ViewController {
fileprivate var temporaryDismiss = false
private struct EmojiSearchResult {
var groups: [EmojiPagerContentComponent.ItemGroup]
var id: AnyHashable
var version: Int
var isPreset: Bool
}
private struct EmojiSearchState {
var result: EmojiSearchResult?
var isSearching: Bool
init(result: EmojiSearchResult?, isSearching: Bool) {
self.result = result
self.isSearching = isSearching
}
}
private let emojiSearchDisposable = MetaDisposable()
private let emojiSearchState = Promise<EmojiSearchState>(EmojiSearchState(result: nil, isSearching: false))
private var emojiSearchStateValue = EmojiSearchState(result: nil, isSearching: false) {
didSet {
self.emojiSearchState.set(.single(self.emojiSearchStateValue))
}
}
private let stickerSearchDisposable = MetaDisposable()
private let stickerSearchState = Promise<EmojiSearchState>(EmojiSearchState(result: nil, isSearching: false))
private var stickerSearchStateValue = EmojiSearchState(result: nil, isSearching: false) {
didSet {
self.stickerSearchState.set(.single(self.stickerSearchStateValue))
}
}
init(context: AccountContext, controller: StickerPickerScreen, theme: PresentationTheme) {
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.controller = controller
@ -249,8 +323,52 @@ public class StickerPickerScreen: ViewController {
self.wrappingView.addSubview(self.containerView)
self.containerView.addSubview(self.hostView)
self.contentDisposable.set(controller.inputData.start(next: { [weak self] inputData in
let signal = combineLatest(
queue: Queue.mainQueue(),
controller.inputData,
self.stickerSearchState.get(),
self.emojiSearchState.get()
)
self.contentDisposable.set(signal.start(next: { [weak self] inputData, stickerSearchState, emojiSearchState in
if let strongSelf = self {
let presentationData = strongSelf.presentationData
var inputData = inputData
let emoji = inputData.emoji
if let emojiSearchResult = emojiSearchState.result {
var emptySearchResults: EmojiPagerContentComponent.EmptySearchResults?
if !emojiSearchResult.groups.contains(where: { !$0.items.isEmpty || $0.fillWithLoadingPlaceholders }) {
emptySearchResults = EmojiPagerContentComponent.EmptySearchResults(
text: presentationData.strings.EmojiSearch_SearchEmojiEmptyResult,
iconFile: nil
)
}
let defaultSearchState: EmojiPagerContentComponent.SearchState = emojiSearchResult.isPreset ? .active : .empty(hasResults: true)
inputData.emoji = emoji.withUpdatedItemGroups(panelItemGroups: emoji.panelItemGroups, contentItemGroups: emojiSearchResult.groups, itemContentUniqueId: EmojiPagerContentComponent.ContentId(id: emojiSearchResult.id, version: emojiSearchResult.version), emptySearchResults: emptySearchResults, searchState: emojiSearchState.isSearching ? .searching : defaultSearchState)
} else if emojiSearchState.isSearching {
inputData.emoji = emoji.withUpdatedItemGroups(panelItemGroups: emoji.panelItemGroups, contentItemGroups: emoji.contentItemGroups, itemContentUniqueId: emoji.itemContentUniqueId, emptySearchResults: emoji.emptySearchResults, searchState: .searching)
}
if let stickerSearchResult = stickerSearchState.result {
var stickerSearchResults: EmojiPagerContentComponent.EmptySearchResults?
if !stickerSearchResult.groups.contains(where: { !$0.items.isEmpty || $0.fillWithLoadingPlaceholders }) {
stickerSearchResults = EmojiPagerContentComponent.EmptySearchResults(
text: presentationData.strings.EmojiSearch_SearchStickersEmptyResult,
iconFile: nil
)
}
if let stickers = inputData.stickers {
let defaultSearchState: EmojiPagerContentComponent.SearchState = stickerSearchResult.isPreset ? .active : .empty(hasResults: true)
inputData.stickers = stickers.withUpdatedItemGroups(panelItemGroups: stickers.panelItemGroups, contentItemGroups: stickerSearchResult.groups, itemContentUniqueId: EmojiPagerContentComponent.ContentId(id: stickerSearchResult.id, version: stickerSearchResult.version), emptySearchResults: stickerSearchResults, searchState: stickerSearchState.isSearching ? .searching : defaultSearchState)
}
} else if stickerSearchState.isSearching {
if let stickers = inputData.stickers {
inputData.stickers = stickers.withUpdatedItemGroups(panelItemGroups: stickers.panelItemGroups, contentItemGroups: stickers.contentItemGroups, itemContentUniqueId: stickers.itemContentUniqueId, emptySearchResults: stickers.emptySearchResults, searchState: .searching)
}
}
strongSelf.updateContent(inputData)
}
}))
@ -258,6 +376,8 @@ public class StickerPickerScreen: ViewController {
deinit {
self.contentDisposable.dispose()
self.emojiSearchDisposable.dispose()
self.stickerSearchDisposable.dispose()
}
func updateContent(_ content: StickerPickerInputData) {
@ -363,9 +483,224 @@ public class StickerPickerScreen: ViewController {
navigationController: { [weak self] in
return self?.controller?.navigationController as? NavigationController
},
requestUpdate: { _ in
requestUpdate: { [weak self] transition in
guard let strongSelf = self else {
return
}
if !transition.animation.isImmediate, let (layout, navigationHeight) = strongSelf.currentLayout {
strongSelf.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: transition)
}
},
updateSearchQuery: { _ in
updateSearchQuery: { [weak self] query in
guard let self, let controller = self.controller else {
return
}
let context = controller.context
switch query {
case .none:
self.emojiSearchDisposable.set(nil)
self.emojiSearchState.set(.single(EmojiSearchState(result: nil, isSearching: false)))
case let .text(rawQuery, languageCode):
let query = rawQuery.trimmingCharacters(in: .whitespacesAndNewlines)
if query.isEmpty {
self.emojiSearchDisposable.set(nil)
self.emojiSearchState.set(.single(EmojiSearchState(result: nil, isSearching: false)))
} else {
var signal = context.engine.stickers.searchEmojiKeywords(inputLanguageCode: languageCode, query: query, completeMatch: false)
if !languageCode.lowercased().hasPrefix("en") {
signal = signal
|> mapToSignal { keywords in
return .single(keywords)
|> then(
context.engine.stickers.searchEmojiKeywords(inputLanguageCode: "en-US", query: query, completeMatch: query.count < 3)
|> map { englishKeywords in
return keywords + englishKeywords
}
)
}
}
let hasPremium: Signal<Bool, NoError> = .single(true)
let resultSignal = combineLatest(
signal,
hasPremium
)
|> mapToSignal { keywords, hasPremium -> Signal<[EmojiPagerContentComponent.ItemGroup], NoError> in
var allEmoticons: [String: String] = [:]
for keyword in keywords {
for emoticon in keyword.emoticons {
allEmoticons[emoticon] = keyword.keyword
}
}
let remoteSignal: Signal<(items: [TelegramMediaFile], isFinalResult: Bool), NoError>
if hasPremium {
remoteSignal = context.engine.stickers.searchEmoji(emojiString: Array(allEmoticons.keys))
} else {
remoteSignal = .single(([], true))
}
return remoteSignal
|> mapToSignal { foundEmoji -> Signal<[EmojiPagerContentComponent.ItemGroup], NoError> in
if foundEmoji.items.isEmpty && !foundEmoji.isFinalResult {
return .complete()
}
var items: [EmojiPagerContentComponent.Item] = []
let appendUnicodeEmoji = {
for (_, list) in EmojiPagerContentComponent.staticEmojiMapping {
for emojiString in list {
if allEmoticons[emojiString] != nil {
let item = EmojiPagerContentComponent.Item(
animationData: nil,
content: .staticEmoji(emojiString),
itemFile: nil,
subgroupId: nil,
icon: .none,
tintMode: .none
)
items.append(item)
}
}
}
}
if !hasPremium {
appendUnicodeEmoji()
}
var existingIds = Set<MediaId>()
for itemFile in foundEmoji.items {
if existingIds.contains(itemFile.fileId) {
continue
}
existingIds.insert(itemFile.fileId)
if itemFile.isPremiumEmoji && !hasPremium {
continue
}
let animationData = EntityKeyboardAnimationData(file: itemFile)
let item = EmojiPagerContentComponent.Item(
animationData: animationData,
content: .animation(animationData),
itemFile: itemFile,
subgroupId: nil,
icon: .none,
tintMode: animationData.isTemplate ? .primary : .none
)
items.append(item)
}
if hasPremium {
appendUnicodeEmoji()
}
return .single([EmojiPagerContentComponent.ItemGroup(
supergroupId: "search",
groupId: "search",
title: nil,
subtitle: nil,
actionButtonTitle: nil,
isFeatured: false,
isPremiumLocked: false,
isEmbedded: false,
hasClear: false,
collapsedLineCount: nil,
displayPremiumBadges: false,
headerItem: nil,
fillWithLoadingPlaceholders: false,
items: items
)])
}
}
var version = 0
self.emojiSearchStateValue.isSearching = true
self.emojiSearchDisposable.set((resultSignal
|> delay(0.15, queue: .mainQueue())
|> deliverOnMainQueue).start(next: { [weak self] result in
guard let self else {
return
}
self.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result, id: AnyHashable(query), version: version, isPreset: false), isSearching: false)
version += 1
}))
}
case let .category(value):
let resultSignal = context.engine.stickers.searchEmoji(emojiString: value)
|> mapToSignal { files, isFinalResult -> Signal<(items: [EmojiPagerContentComponent.ItemGroup], isFinalResult: Bool), NoError> in
var items: [EmojiPagerContentComponent.Item] = []
var existingIds = Set<MediaId>()
for itemFile in files {
if existingIds.contains(itemFile.fileId) {
continue
}
existingIds.insert(itemFile.fileId)
let animationData = EntityKeyboardAnimationData(file: itemFile)
let item = EmojiPagerContentComponent.Item(
animationData: animationData,
content: .animation(animationData),
itemFile: itemFile, subgroupId: nil,
icon: .none,
tintMode: animationData.isTemplate ? .primary : .none
)
items.append(item)
}
return .single(([EmojiPagerContentComponent.ItemGroup(
supergroupId: "search",
groupId: "search",
title: nil,
subtitle: nil,
actionButtonTitle: nil,
isFeatured: false,
isPremiumLocked: false,
isEmbedded: false,
hasClear: false,
collapsedLineCount: nil,
displayPremiumBadges: false,
headerItem: nil,
fillWithLoadingPlaceholders: false,
items: items
)], isFinalResult))
}
var version = 0
self.emojiSearchDisposable.set((resultSignal
|> deliverOnMainQueue).start(next: { [weak self] result in
guard let self else {
return
}
guard let group = result.items.first else {
return
}
if group.items.isEmpty && !result.isFinalResult {
self.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: [
EmojiPagerContentComponent.ItemGroup(
supergroupId: "search",
groupId: "search",
title: nil,
subtitle: nil,
actionButtonTitle: nil,
isFeatured: false,
isPremiumLocked: false,
isEmbedded: false,
hasClear: false,
collapsedLineCount: nil,
displayPremiumBadges: false,
headerItem: nil,
fillWithLoadingPlaceholders: true,
items: []
)
], id: AnyHashable(value), version: version, isPreset: true), isSearching: false)
return
}
self.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result.items, id: AnyHashable(value), version: version, isPreset: true), isSearching: false)
version += 1
}))
}
},
updateScrollingToItemGroup: { [weak self] in
self?.update(isExpanded: true, transition: .animated(duration: 0.4, curve: .spring))
@ -570,7 +905,95 @@ public class StickerPickerScreen: ViewController {
},
requestUpdate: { _ in
},
updateSearchQuery: { _ in
updateSearchQuery: { [weak self] query in
guard let strongSelf = self, let controller = strongSelf.controller else {
return
}
let context = controller.context
switch query {
case .none:
strongSelf.stickerSearchDisposable.set(nil)
strongSelf.stickerSearchStateValue = EmojiSearchState(result: nil, isSearching: false)
case .text:
strongSelf.stickerSearchDisposable.set(nil)
strongSelf.stickerSearchStateValue = EmojiSearchState(result: nil, isSearching: false)
case let .category(value):
let resultSignal = context.engine.stickers.searchStickers(query: value, scope: [.installed, .remote])
|> mapToSignal { files -> Signal<(items: [EmojiPagerContentComponent.ItemGroup], isFinalResult: Bool), NoError> in
var items: [EmojiPagerContentComponent.Item] = []
var existingIds = Set<MediaId>()
for item in files.items {
let itemFile = item.file
if existingIds.contains(itemFile.fileId) {
continue
}
existingIds.insert(itemFile.fileId)
let animationData = EntityKeyboardAnimationData(file: itemFile)
let item = EmojiPagerContentComponent.Item(
animationData: animationData,
content: .animation(animationData),
itemFile: itemFile, subgroupId: nil,
icon: .none,
tintMode: animationData.isTemplate ? .primary : .none
)
items.append(item)
}
return .single(([EmojiPagerContentComponent.ItemGroup(
supergroupId: "search",
groupId: "search",
title: nil,
subtitle: nil,
actionButtonTitle: nil,
isFeatured: false,
isPremiumLocked: false,
isEmbedded: false,
hasClear: false,
collapsedLineCount: nil,
displayPremiumBadges: false,
headerItem: nil,
fillWithLoadingPlaceholders: false,
items: items
)], files.isFinalResult))
}
var version = 0
strongSelf.stickerSearchDisposable.set((resultSignal
|> deliverOnMainQueue).start(next: { [weak self] result in
guard let strongSelf = self else {
return
}
guard let group = result.items.first else {
return
}
if group.items.isEmpty && !result.isFinalResult {
//strongSelf.stickerSearchStateValue.isSearching = true
strongSelf.stickerSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: [
EmojiPagerContentComponent.ItemGroup(
supergroupId: "search",
groupId: "search",
title: nil,
subtitle: nil,
actionButtonTitle: nil,
isFeatured: false,
isPremiumLocked: false,
isEmbedded: false,
hasClear: false,
collapsedLineCount: nil,
displayPremiumBadges: false,
headerItem: nil,
fillWithLoadingPlaceholders: true,
items: []
)
], id: AnyHashable(value), version: version, isPreset: true), isSearching: false)
return
}
strongSelf.stickerSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result.items, id: AnyHashable(value), version: version, isPreset: true), isSearching: false)
version += 1
}))
}
},
updateScrollingToItemGroup: { [weak self] in
self?.update(isExpanded: true, transition: .animated(duration: 0.4, curve: .spring))
@ -667,6 +1090,9 @@ public class StickerPickerScreen: ViewController {
}
func containerLayoutUpdated(layout: ContainerViewLayout, navigationHeight: CGFloat, transition: Transition) {
guard let controller = self.controller else {
return
}
self.currentLayout = (layout, navigationHeight)
self.dim.frame = CGRect(origin: CGPoint(x: 0.0, y: -layout.size.height), size: CGSize(width: layout.size.width, height: layout.size.height * 3.0))
@ -691,7 +1117,10 @@ public class StickerPickerScreen: ViewController {
}
transition.setFrame(view: self.wrappingView, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset), size: layout.size), completion: nil)
let modalProgress = isLandscape ? 0.0 : (1.0 - topInset / self.defaultTopInset)
var modalProgress = isLandscape ? 0.0 : (1.0 - topInset / self.defaultTopInset)
if self.isDismissing {
modalProgress = 0.0
}
self.controller?.updateModalStyleOverlayTransitionFactor(modalProgress, transition: transition.containedViewLayoutTransition)
let clipFrame: CGRect
@ -761,6 +1190,7 @@ public class StickerPickerScreen: ViewController {
transition: stickersTransition,
component: AnyComponent(
StickerSelectionComponent(
context: controller.context,
theme: self.theme,
strings: self.presentationData.strings,
deviceMetrics: layout.deviceMetrics,

View File

@ -146,6 +146,8 @@ private class LegacyPaintStickerEntity: LegacyPaintEntity {
case let .image(image):
self.file = nil
self.imagePromise.set(.single(image))
case .video:
self.file = nil
}
}

View File

@ -668,6 +668,10 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
let transaction = MediaPickerGridTransaction(previousList: previousEntries, list: entries, context: controller.context, interaction: interaction, theme: self.presentationData.theme, scrollToItem: scrollToItem)
self.enqueueTransaction(transaction)
if !self.didSetReady {
updateLayout = true
}
if updateLayout, let (layout, navigationBarHeight) = self.validLayout {
self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: previousState == nil ? .immediate : .animated(duration: 0.2, curve: .easeInOut))
}
@ -2167,6 +2171,7 @@ public func wallpaperMediaPickerController(
public func storyMediaPickerController(
context: AccountContext,
getSourceRect: @escaping () -> CGRect,
completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping () -> (UIView, CGRect)?, @escaping () -> Void) -> Void,
dismissed: @escaping () -> Void
) -> ViewController {
@ -2175,6 +2180,8 @@ public func storyMediaPickerController(
let controller = AttachmentController(context: context, updatedPresentationData: updatedPresentationData, chatLocation: nil, buttons: [.standalone], initialButton: .standalone, fromMenu: false, hasTextInput: false, makeEntityInputView: {
return nil
})
controller.forceSourceRect = true
controller.getSourceRect = getSourceRect
controller.requestController = { _, present in
let mediaPickerController = MediaPickerScreen(context: context, updatedPresentationData: updatedPresentationData, peer: nil, threadTitle: nil, chatLocation: nil, bannedSendPhotos: nil, bannedSendVideos: nil, subject: .assets(nil, .story), mainButtonState: nil, mainButtonAction: nil)
mediaPickerController.customSelection = { controller, result in

View File

@ -0,0 +1,19 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "CameraButtonComponent",
module_name = "CameraButtonComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display",
"//submodules/ComponentFlow",
],
visibility = [
"//visibility:public",
],
)

View File

@ -2,14 +2,14 @@ import Foundation
import UIKit
import ComponentFlow
final class CameraButton: Component {
public final class CameraButton: Component {
let content: AnyComponentWithIdentity<Empty>
let minSize: CGSize?
let tag: AnyObject?
let isEnabled: Bool
let action: () -> Void
init(
public init(
content: AnyComponentWithIdentity<Empty>,
minSize: CGSize? = nil,
tag: AnyObject? = nil,
@ -23,7 +23,7 @@ final class CameraButton: Component {
self.action = action
}
func tagged(_ tag: AnyObject) -> CameraButton {
public func tagged(_ tag: AnyObject) -> CameraButton {
return CameraButton(
content: self.content,
minSize: self.minSize,
@ -33,7 +33,7 @@ final class CameraButton: Component {
)
}
static func ==(lhs: CameraButton, rhs: CameraButton) -> Bool {
public static func ==(lhs: CameraButton, rhs: CameraButton) -> Bool {
if lhs.content != rhs.content {
return false
}
@ -49,8 +49,8 @@ final class CameraButton: Component {
return true
}
final class View: UIButton, ComponentTaggedView {
private var contentView: ComponentHostView<Empty>
public final class View: UIButton, ComponentTaggedView {
public var contentView: ComponentHostView<Empty>
private var component: CameraButton?
private var currentIsHighlighted: Bool = false {
@ -74,7 +74,7 @@ final class CameraButton: Component {
transition.setScale(view: self, scale: scale)
}
override init(frame: CGRect) {
public override init(frame: CGRect) {
self.contentView = ComponentHostView<Empty>()
self.contentView.isUserInteractionEnabled = false
self.contentView.layer.allowsGroupOpacity = true
@ -104,19 +104,19 @@ final class CameraButton: Component {
self.component?.action()
}
override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
public override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
self.currentIsHighlighted = true
return super.beginTracking(touch, with: event)
}
override func endTracking(_ touch: UITouch?, with event: UIEvent?) {
public override func endTracking(_ touch: UITouch?, with event: UIEvent?) {
self.currentIsHighlighted = false
super.endTracking(touch, with: event)
}
override func cancelTracking(with event: UIEvent?) {
public override func cancelTracking(with event: UIEvent?) {
self.currentIsHighlighted = false
super.cancelTracking(with: event)
@ -155,11 +155,11 @@ final class CameraButton: Component {
}
}
func makeView() -> View {
public func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View File

@ -73,7 +73,8 @@ swift_library(
"//submodules/Components/BundleIconComponent:BundleIconComponent",
"//submodules/TooltipUI",
"//submodules/TelegramUI/Components/MediaEditor",
"//submodules/Components/MetalImageView:MetalImageView",
"//submodules/Components/MetalImageView",
"//submodules/TelegramUI/Components/CameraButtonComponent",
],
visibility = [
"//visibility:public",

View File

@ -37,9 +37,9 @@ float sdfCircle(float2 uv, float2 position, float radius) {
return length(uv - position) - radius;
}
float map(float2 uv, float4 primaryParameters, float2 secondaryParameters) {
float primary = sdfRoundedRectangle(uv, float2(primaryParameters.y, 0.0), primaryParameters.x, primaryParameters.w);
float secondary = sdfCircle(uv, float2(secondaryParameters.y, 0.0), secondaryParameters.x);
float map(float2 uv, float3 primaryParameters, float2 primaryOffset, float3 secondaryParameters, float2 secondaryOffset) {
float primary = sdfRoundedRectangle(uv, primaryOffset, primaryParameters.x, primaryParameters.z);
float secondary = sdfCircle(uv, secondaryOffset, secondaryParameters.x);
float metaballs = 1.0;
metaballs = smin(metaballs, primary, BindingDistance);
metaballs = smin(metaballs, secondary, BindingDistance);
@ -48,22 +48,32 @@ float map(float2 uv, float4 primaryParameters, float2 secondaryParameters) {
fragment half4 cameraBlobFragment(RasterizerData in[[stage_in]],
constant uint2 &resolution[[buffer(0)]],
constant float4 &primaryParameters[[buffer(1)]],
constant float2 &secondaryParameters[[buffer(2)]])
constant float3 &primaryParameters[[buffer(1)]],
constant float2 &primaryOffset[[buffer(2)]],
constant float3 &secondaryParameters[[buffer(3)]],
constant float2 &secondaryOffset[[buffer(4)]])
{
float2 R = float2(resolution.x, resolution.y);
float2 uv = (2.0 * in.position.xy - R.xy) / R.y;
float2 uv;
float offset;
if (R.x > R.y) {
uv = (2.0 * in.position.xy - R.xy) / R.y;
offset = uv.x;
} else {
uv = (2.0 * in.position.xy - R.xy) / R.x;
offset = uv.y;
}
float t = AARadius / resolution.y;
float cAlpha = 1.0 - primaryParameters.z;
float cAlpha = 1.0 - primaryParameters.y;
float bound = primaryParameters.x + 0.05;
if (abs(uv.x) > bound) {
cAlpha = mix(0.0, 1.0, min(1.0, (abs(uv.x) - bound) * 2.4));
if (abs(offset) > bound) {
cAlpha = mix(0.0, 1.0, min(1.0, (abs(offset) - bound) * 2.4));
}
float c = smoothstep(t, -t, map(uv, primaryParameters, secondaryParameters));
float c = smoothstep(t, -t, map(uv, primaryParameters, primaryOffset, secondaryParameters, secondaryOffset));
return half4(c, max(cAlpha, 0.231), max(cAlpha, 0.188), c);
}

View File

@ -18,6 +18,7 @@ import LottieAnimationComponent
import TooltipUI
import MediaEditor
import BundleIconComponent
import CameraButtonComponent
let videoRedColor = UIColor(rgb: 0xff3b30)
@ -86,6 +87,8 @@ private final class CameraScreenComponent: CombinedComponent {
let camera: Camera
let updateState: ActionSlot<CameraState>
let hasAppeared: Bool
let panelWidth: CGFloat
let flipAnimationAction: ActionSlot<Void>
let present: (ViewController) -> Void
let push: (ViewController) -> Void
let completion: ActionSlot<Signal<CameraScreen.Result, NoError>>
@ -95,6 +98,8 @@ private final class CameraScreenComponent: CombinedComponent {
camera: Camera,
updateState: ActionSlot<CameraState>,
hasAppeared: Bool,
panelWidth: CGFloat,
flipAnimationAction: ActionSlot<Void>,
present: @escaping (ViewController) -> Void,
push: @escaping (ViewController) -> Void,
completion: ActionSlot<Signal<CameraScreen.Result, NoError>>
@ -103,6 +108,8 @@ private final class CameraScreenComponent: CombinedComponent {
self.camera = camera
self.updateState = updateState
self.hasAppeared = hasAppeared
self.panelWidth = panelWidth
self.flipAnimationAction = flipAnimationAction
self.present = present
self.push = push
self.completion = completion
@ -115,6 +122,9 @@ private final class CameraScreenComponent: CombinedComponent {
if lhs.hasAppeared != rhs.hasAppeared {
return false
}
if lhs.panelWidth != rhs.panelWidth {
return false
}
return true
}
@ -186,7 +196,7 @@ private final class CameraScreenComponent: CombinedComponent {
}
})
Queue.mainQueue().async {
Queue.concurrentDefaultQueue().async {
self.setupRecentAssetSubscription()
}
}
@ -229,9 +239,18 @@ private final class CameraScreenComponent: CombinedComponent {
self.hapticFeedback.impact(.light)
}
func togglePosition() {
private var lastFlipTimestamp: Double?
func togglePosition(_ action: ActionSlot<Void>) {
let currentTimestamp = CACurrentMediaTime()
if let lastFlipTimestamp = self.lastFlipTimestamp, currentTimestamp - lastFlipTimestamp < 1.3 {
return
}
self.lastFlipTimestamp = currentTimestamp
self.camera.togglePosition()
self.hapticFeedback.impact(.light)
action.invoke(Void())
}
func toggleDualCamera() {
@ -256,7 +275,7 @@ private final class CameraScreenComponent: CombinedComponent {
case .began:
return .single(.pendingImage)
case let .finished(mainImage, additionalImage, _):
return .single(.image(mainImage, additionalImage))
return .single(.image(mainImage, additionalImage, .bottomRight))
case .failed:
return .complete()
}
@ -282,9 +301,9 @@ private final class CameraScreenComponent: CombinedComponent {
func stopVideoRecording() {
self.cameraState = self.cameraState.updatedRecording(.none).updatedDuration(0.0)
self.resultDisposable.set((self.camera.stopRecording()
|> deliverOnMainQueue).start(next: { [weak self] pathAndTransitionImage in
if let self, let (path, transitionImage) = pathAndTransitionImage {
self.completion.invoke(.single(.video(path, transitionImage, PixelDimensions(width: 1080, height: 1920))))
|> deliverOnMainQueue).start(next: { [weak self] result in
if let self, case let .finished(mainResult, additionalResult, _) = result {
self.completion.invoke(.single(.video(mainResult.0, mainResult.1, additionalResult?.0, additionalResult?.1, PixelDimensions(width: 1080, height: 1920), .bottomRight)))
}
}))
self.isTransitioning = true
@ -316,15 +335,13 @@ private final class CameraScreenComponent: CombinedComponent {
let zoomControl = Child(ZoomComponent.self)
let flashButton = Child(CameraButton.self)
let flipButton = Child(CameraButton.self)
// let dualButton = Child(CameraButton.self)
let dualButton = Child(CameraButton.self)
let modeControl = Child(ModeComponent.self)
let hintLabel = Child(HintLabelComponent.self)
let timeBackground = Child(RoundedRectangle.self)
let timeLabel = Child(MultilineTextComponent.self)
let flipAnimationAction = ActionSlot<Void>()
return { context in
let environment = context.environment[ViewControllerComponentContainer.Environment.self].value
let component = context.component
@ -339,6 +356,9 @@ private final class CameraScreenComponent: CombinedComponent {
isTablet = false
}
let smallPanelWidth = min(component.panelWidth, 88.0)
let panelWidth = min(component.panelWidth, 185.0)
let topControlInset: CGFloat = 20.0
if case .none = state.cameraState.recording, !state.isTransitioning {
let cancelButton = cancelButton.update(
@ -363,7 +383,7 @@ private final class CameraScreenComponent: CombinedComponent {
transition: .immediate
)
context.add(cancelButton
.position(CGPoint(x: topControlInset + cancelButton.size.width / 2.0, y: environment.safeInsets.top + topControlInset + cancelButton.size.height / 2.0))
.position(CGPoint(x: isTablet ? smallPanelWidth / 2.0 : topControlInset + cancelButton.size.width / 2.0, y: environment.safeInsets.top + topControlInset + cancelButton.size.height / 2.0))
.appear(.default(scale: true))
.disappear(.default(scale: true))
)
@ -423,36 +443,36 @@ private final class CameraScreenComponent: CombinedComponent {
transition: .immediate
)
context.add(flashButton
.position(CGPoint(x: availableSize.width - topControlInset - flashButton.size.width / 2.0, y: environment.safeInsets.top + topControlInset + flashButton.size.height / 2.0))
.position(CGPoint(x: isTablet ? availableSize.width - smallPanelWidth / 2.0 : availableSize.width - topControlInset - flashButton.size.width / 2.0, y: environment.safeInsets.top + topControlInset + flashButton.size.height / 2.0))
.appear(.default(scale: true))
.disappear(.default(scale: true))
)
// if #available(iOS 13.0, *) {
// let dualButton = dualButton.update(
// component: CameraButton(
// content: AnyComponentWithIdentity(
// id: "dual",
// component: AnyComponent(
// DualIconComponent(isSelected: state.cameraState.isDualCamEnabled)
// )
// ),
// action: { [weak state] in
// guard let state else {
// return
// }
// state.toggleDualCamera()
// }
// ).tagged(dualButtonTag),
// availableSize: CGSize(width: 40.0, height: 40.0),
// transition: .immediate
// )
// context.add(dualButton
// .position(CGPoint(x: availableSize.width / 2.0, y: environment.safeInsets.top + topControlInset + dualButton.size.height / 2.0))
// .appear(.default(scale: true))
// .disappear(.default(scale: true))
// )
// }
if #available(iOS 13.0, *), !isTablet && !"".isEmpty {
let dualButton = dualButton.update(
component: CameraButton(
content: AnyComponentWithIdentity(
id: "dual",
component: AnyComponent(
DualIconComponent(isSelected: state.cameraState.isDualCamEnabled)
)
),
action: { [weak state] in
guard let state else {
return
}
state.toggleDualCamera()
}
).tagged(dualButtonTag),
availableSize: CGSize(width: 40.0, height: 40.0),
transition: .immediate
)
context.add(dualButton
.position(CGPoint(x: availableSize.width / 2.0, y: environment.safeInsets.top + topControlInset + dualButton.size.height / 2.0))
.appear(.default(scale: true))
.disappear(.default(scale: true))
)
}
}
if case .holding = state.cameraState.recording {
@ -494,9 +514,17 @@ private final class CameraScreenComponent: CombinedComponent {
}
}
let flipAnimationAction = component.flipAnimationAction
let captureControlsAvailableSize: CGSize
if isTablet {
captureControlsAvailableSize = CGSize(width: panelWidth, height: availableSize.height)
} else {
captureControlsAvailableSize = availableSize
}
let captureControls = captureControls.update(
component: CaptureControlsComponent(
isTablet: isTablet,
hasAppeared: component.hasAppeared,
shutterState: shutterState,
lastGalleryAsset: state.lastGalleryAsset,
tag: captureControlsTag,
@ -537,7 +565,7 @@ private final class CameraScreenComponent: CombinedComponent {
guard let state else {
return
}
state.togglePosition()
state.togglePosition(flipAnimationAction)
},
galleryTapped: {
guard let controller = environment.controller() as? CameraScreen else {
@ -550,45 +578,50 @@ private final class CameraScreenComponent: CombinedComponent {
},
zoomUpdated: { fraction in
state.updateZoom(fraction: fraction)
}
},
flipAnimationAction: flipAnimationAction
),
availableSize: availableSize,
availableSize: captureControlsAvailableSize,
transition: context.transition
)
let captureControlsPosition: CGPoint
if isTablet {
captureControlsPosition = CGPoint(x: availableSize.width - panelWidth / 2.0, y: availableSize.height / 2.0)
} else {
captureControlsPosition = CGPoint(x: availableSize.width / 2.0, y: availableSize.height - captureControls.size.height / 2.0 - environment.safeInsets.bottom - 5.0)
}
context.add(captureControls
.position(CGPoint(x: availableSize.width / 2.0, y: availableSize.height - captureControls.size.height / 2.0 - environment.safeInsets.bottom - 5.0))
.position(captureControlsPosition)
)
if isTablet {
let flipButton = flipButton.update(
component: CameraButton(
component: CameraButton(
content: AnyComponentWithIdentity(
id: "flip",
component: AnyComponent(
FlipButtonContentComponent(action: flipAnimationAction)
FlipButtonContentComponent(
action: flipAnimationAction,
maskFrame: .zero
)
)
),
minSize: CGSize(width: 44.0, height: 44.0),
action: {
// let currentTimestamp = CACurrentMediaTime()
// if let lastFlipTimestamp = self.lastFlipTimestamp, currentTimestamp - lastFlipTimestamp < 1.3 {
// return
// }
// self.lastFlipTimestamp = currentTimestamp
state.togglePosition()
flipAnimationAction.invoke(Void())
state.togglePosition(flipAnimationAction)
}
),
availableSize: availableSize,
transition: context.transition
)
context.add(flipButton
.position(CGPoint(x: availableSize.width / 2.0, y: availableSize.height - captureControls.size.height / 2.0 - environment.safeInsets.bottom - 5.0))
.position(CGPoint(x: smallPanelWidth / 2.0, y: availableSize.height / 2.0))
)
}
var isVideoRecording = false
if case .video = state.cameraState.mode {
if case .video = state.cameraState.mode, isTablet {
isVideoRecording = true
} else if state.cameraState.recording != .none {
isVideoRecording = true
@ -607,6 +640,13 @@ private final class CameraScreenComponent: CombinedComponent {
transition: context.transition
)
let timePosition: CGPoint
if isTablet {
timePosition = CGPoint(x: availableSize.width - panelWidth / 2.0, y: availableSize.height / 2.0 - 97.0)
} else {
timePosition = CGPoint(x: availableSize.width / 2.0, y: environment.safeInsets.top + 40.0)
}
if state.cameraState.recording != .none {
let timeBackground = timeBackground.update(
component: RoundedRectangle(color: videoRedColor, cornerRadius: 4.0),
@ -614,19 +654,19 @@ private final class CameraScreenComponent: CombinedComponent {
transition: context.transition
)
context.add(timeBackground
.position(CGPoint(x: availableSize.width / 2.0, y: environment.safeInsets.top + 40.0))
.position(timePosition)
.appear(.default(alpha: true))
.disappear(.default(alpha: true))
)
}
context.add(timeLabel
.position(CGPoint(x: availableSize.width / 2.0, y: environment.safeInsets.top + 40.0))
.position(timePosition)
.appear(.default(alpha: true))
.disappear(.default(alpha: true))
)
if case .holding = state.cameraState.recording {
if case .holding = state.cameraState.recording, !isTablet {
let hintText: String?
switch state.swipeHint {
case .none:
@ -656,8 +696,15 @@ private final class CameraScreenComponent: CombinedComponent {
}
if case .none = state.cameraState.recording, !state.isTransitioning {
let availableModeControlSize: CGSize
if isTablet {
availableModeControlSize = CGSize(width: panelWidth, height: 120.0)
} else {
availableModeControlSize = availableSize
}
let modeControl = modeControl.update(
component: ModeComponent(
isTablet: isTablet,
availableModes: [.photo, .video],
currentMode: state.cameraState.mode,
updatedMode: { [weak state] mode in
@ -667,12 +714,18 @@ private final class CameraScreenComponent: CombinedComponent {
},
tag: modeControlTag
),
availableSize: availableSize,
availableSize: availableModeControlSize,
transition: context.transition
)
let modeControlPosition: CGPoint
if isTablet {
modeControlPosition = CGPoint(x: availableSize.width - panelWidth / 2.0, y: availableSize.height / 2.0 + modeControl.size.height + 26.0)
} else {
modeControlPosition = CGPoint(x: availableSize.width / 2.0, y: availableSize.height - environment.safeInsets.bottom + modeControl.size.height / 2.0)
}
context.add(modeControl
.clipsToBounds(true)
.position(CGPoint(x: availableSize.width / 2.0, y: availableSize.height - environment.safeInsets.bottom + modeControl.size.height / 2.0))
.position(modeControlPosition)
.appear(.default(alpha: true))
.disappear(.default(alpha: true))
)
@ -734,12 +787,30 @@ public class CameraScreen: ViewController {
case instantVideo
}
public enum PIPPosition {
case topLeft
case topRight
case bottomLeft
case bottomRight
}
public enum Result {
case pendingImage
case image(UIImage, UIImage?)
case video(String, UIImage?, PixelDimensions)
case image(UIImage, UIImage?, CameraScreen.PIPPosition)
case video(String, UIImage?, String?, UIImage?, PixelDimensions, CameraScreen.PIPPosition)
case asset(PHAsset)
case draft(MediaEditorDraft)
func withPIPPosition(_ position: CameraScreen.PIPPosition) -> Result {
switch self {
case let .image(mainImage, additionalImage, _):
return .image(mainImage, additionalImage, position)
case let .video(mainPath, mainImage, additionalPath, additionalImage, dimensions, _):
return .video(mainPath, mainImage, additionalPath, additionalImage, dimensions, position)
default:
return self
}
}
}
public final class TransitionIn {
@ -846,6 +917,10 @@ public class CameraScreen: ViewController {
fileprivate var previewBlurPromise = ValuePromise<Bool>(false)
private let flipAnimationAction = ActionSlot<Void>()
private var pipPosition: PIPPosition = .bottomRight
init(controller: CameraScreen) {
self.controller = controller
self.context = controller.context
@ -994,9 +1069,13 @@ public class CameraScreen: ViewController {
self.completion.connect { [weak self] result in
if let self {
let pipPosition = self.pipPosition
self.animateOutToEditor()
self.controller?.completion(
result
|> map { result in
return result.withPIPPosition(pipPosition)
}
|> beforeNext { [weak self] value in
guard let self else {
return
@ -1070,6 +1149,9 @@ public class CameraScreen: ViewController {
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:)))
self.effectivePreviewView.addGestureRecognizer(tapGestureRecognizer)
let pipPanGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePipPan(_:)))
self.additionalPreviewView?.addGestureRecognizer(pipPanGestureRecognizer)
self.camera.focus(at: CGPoint(x: 0.5, y: 0.5), autoFocus: true)
self.camera.startCapture()
}
@ -1128,7 +1210,30 @@ public class CameraScreen: ViewController {
self.camera.focus(at: point, autoFocus: false)
}
private var pipTranslation: CGPoint?
@objc private func handlePipPan(_ gestureRecognizer: UIPanGestureRecognizer) {
guard let layout = self.validLayout else {
return
}
let translation = gestureRecognizer.translation(in: self.view)
let location = gestureRecognizer.location(in: self.view)
let velocity = gestureRecognizer.velocity(in: self.view)
switch gestureRecognizer.state {
case .began, .changed:
self.pipTranslation = translation
self.containerLayoutUpdated(layout: layout, transition: .immediate)
case .ended, .cancelled:
self.pipTranslation = nil
self.pipPosition = pipPositionForLocation(layout: layout, position: location, velocity: velocity)
self.containerLayoutUpdated(layout: layout, transition: .spring(duration: 0.4))
default:
break
}
}
func animateIn() {
self.transitionDimView.alpha = 0.0
self.backgroundView.alpha = 0.0
UIView.animate(withDuration: 0.4, animations: {
self.backgroundView.alpha = 1.0
@ -1185,8 +1290,10 @@ public class CameraScreen: ViewController {
view.layer.animatePosition(from: view.center, to: destinationLocalFrame.center, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
view.layer.animateScale(from: 1.0, to: targetScale, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
}
} else {
completion()
}
self.componentHost.view?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
self.previewContainerView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.35, removeOnCompletion: false)
}
@ -1288,7 +1395,7 @@ public class CameraScreen: ViewController {
}
func updateModalTransitionFactor(_ value: CGFloat, transition: ContainedViewLayoutTransition) {
guard let layout = self.validLayout else {
guard let layout = self.validLayout, case .compact = layout.metrics.widthClass else {
return
}
@ -1337,7 +1444,11 @@ public class CameraScreen: ViewController {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let result = super.hitTest(point, with: event)
if result == self.componentHost.view {
return self.effectivePreviewView
if let additionalPreviewView = self.additionalPreviewView, additionalPreviewView.bounds.contains(self.view.convert(point, to: additionalPreviewView)) {
return additionalPreviewView
} else {
return self.effectivePreviewView
}
}
return result
}
@ -1379,6 +1490,19 @@ public class CameraScreen: ViewController {
let topInset: CGFloat = (layout.statusBarHeight ?? 0.0) + 12.0
let bottomInset = layout.size.height - previewSize.height - topInset
let panelWidth: CGFloat
let previewFrame: CGRect
let viewfinderFrame: CGRect
if isTablet {
previewFrame = CGRect(origin: .zero, size: layout.size)
viewfinderFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - previewSize.width) / 2.0), y: 0.0), size: previewSize)
panelWidth = viewfinderFrame.minX
} else {
previewFrame = CGRect(origin: CGPoint(x: 0.0, y: topInset), size: previewSize)
viewfinderFrame = previewFrame
panelWidth = 0.0
}
let environment = ViewControllerComponentContainer.Environment(
statusBarHeight: layout.statusBarHeight ?? 0.0,
navigationHeight: 0.0,
@ -1408,7 +1532,7 @@ public class CameraScreen: ViewController {
self.hasAppeared = hasAppeared
transition = transition.withUserData(CameraScreenTransition.finishedAnimateIn)
self.presentDualCameraTooltip()
// self.presentDualCameraTooltip()
}
let componentSize = self.componentHost.update(
@ -1419,6 +1543,8 @@ public class CameraScreen: ViewController {
camera: self.camera,
updateState: self.updateState,
hasAppeared: self.hasAppeared,
panelWidth: panelWidth,
flipAnimationAction: self.flipAnimationAction,
present: { [weak self] c in
self?.controller?.present(c, in: .window(.root))
},
@ -1452,16 +1578,6 @@ public class CameraScreen: ViewController {
transition.setFrame(view: self.transitionDimView, frame: CGRect(origin: .zero, size: layout.size))
let previewFrame: CGRect
let viewfinderFrame: CGRect
if isTablet {
previewFrame = CGRect(origin: .zero, size: layout.size)
viewfinderFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - previewSize.width) / 2.0), y: 0.0), size: previewSize)
} else {
previewFrame = CGRect(origin: CGPoint(x: 0.0, y: topInset), size: previewSize)
viewfinderFrame = previewFrame
}
transition.setFrame(view: self.previewContainerView, frame: previewFrame)
self.currentPreviewView.layer.cornerRadius = 0.0
transition.setFrame(view: self.currentPreviewView, frame: CGRect(origin: .zero, size: previewFrame.size))
@ -1470,7 +1586,36 @@ public class CameraScreen: ViewController {
if let additionalPreviewView = self.currentAdditionalPreviewView {
additionalPreviewView.layer.cornerRadius = 80.0
let additionalPreviewFrame = CGRect(origin: CGPoint(x: previewFrame.width - 160.0 - 10.0 + (self.isDualCamEnabled ? 0.0 : 180.0), y: previewFrame.height - 160.0 - 81.0), size: CGSize(width: 160.0, height: 160.0))
var origin: CGPoint
switch self.pipPosition {
case .topLeft:
origin = CGPoint(x: 10.0, y: 110.0)
if !self.isDualCamEnabled {
origin = origin.offsetBy(dx: -180.0, dy: 0.0)
}
case .topRight:
origin = CGPoint(x: previewFrame.width - 160.0 - 10.0, y: 110.0)
if !self.isDualCamEnabled {
origin = origin.offsetBy(dx: 180.0, dy: 0.0)
}
case .bottomLeft:
origin = CGPoint(x: 10.0, y: previewFrame.height - 160.0 - 110.0)
if !self.isDualCamEnabled {
origin = origin.offsetBy(dx: -180.0, dy: 0.0)
}
case .bottomRight:
origin = CGPoint(x: previewFrame.width - 160.0 - 10.0, y: previewFrame.height - 160.0 - 110.0)
if !self.isDualCamEnabled {
origin = origin.offsetBy(dx: 180.0, dy: 0.0)
}
}
if let pipTranslation = self.pipTranslation {
origin = origin.offsetBy(dx: pipTranslation.x, dy: pipTranslation.y)
}
let additionalPreviewFrame = CGRect(origin: origin, size: CGSize(width: 160.0, height: 160.0))
transition.setPosition(view: additionalPreviewView, position: additionalPreviewFrame.center)
transition.setBounds(view: additionalPreviewView, bounds: CGRect(origin: .zero, size: additionalPreviewFrame.size))
@ -1500,6 +1645,10 @@ public class CameraScreen: ViewController {
transition.setPosition(view: self.transitionCornersView, position: CGPoint(x: layout.size.width + screenCornerRadius / 2.0, y: layout.size.height / 2.0))
transition.setBounds(view: self.transitionCornersView, bounds: CGRect(origin: .zero, size: CGSize(width: screenCornerRadius, height: layout.size.height)))
if isTablet && isFirstTime {
self.animateIn()
}
}
}
@ -1563,6 +1712,10 @@ public class CameraScreen: ViewController {
self.navigationPresentation = .flatModal
self.requestAudioSession()
if #available(iOS 13.0, *) {
try? AVAudioSession.sharedInstance().setAllowHapticsAndSystemSoundsDuringRecording(true)
}
}
required public init(coder: NSCoder) {
@ -1571,6 +1724,9 @@ public class CameraScreen: ViewController {
deinit {
self.audioSessionDisposable?.dispose()
if #available(iOS 13.0, *) {
try? AVAudioSession.sharedInstance().setAllowHapticsAndSystemSoundsDuringRecording(false)
}
}
override public func loadDisplayNode() {
@ -1611,7 +1767,17 @@ public class CameraScreen: ViewController {
if let current = self.galleryController {
controller = current
} else {
controller = self.context.sharedContext.makeMediaPickerScreen(context: self.context, completion: { [weak self] result, transitionView, transitionRect, transitionImage, transitionOut, dismissed in
controller = self.context.sharedContext.makeMediaPickerScreen(context: self.context, getSourceRect: { [weak self] in
if let self {
if let galleryButton = self.node.componentHost.findTaggedView(tag: galleryButtonTag) {
return galleryButton.convert(galleryButton.bounds, to: self.view).offsetBy(dx: 0.0, dy: -15.0)
} else {
return .zero
}
} else {
return .zero
}
}, completion: { [weak self] result, transitionView, transitionRect, transitionImage, transitionOut, dismissed in
if let self {
stopCameraCapture()
@ -1665,15 +1831,21 @@ public class CameraScreen: ViewController {
self.node.camera.stopCapture(invalidate: true)
self.isDismissed = true
if animated {
self.statusBar.updateStatusBarStyle(.Ignore, animated: true)
if !interactive {
if let navigationController = self.navigationController as? NavigationController {
navigationController.updateRootContainerTransitionOffset(self.node.frame.width, transition: .immediate)
if let layout = self.validLayout, case .regular = layout.metrics.widthClass {
self.node.animateOut(completion: {
self.dismiss(animated: false)
})
} else {
self.statusBar.updateStatusBarStyle(.Ignore, animated: true)
if !interactive {
if let navigationController = self.navigationController as? NavigationController {
navigationController.updateRootContainerTransitionOffset(self.node.frame.width, transition: .immediate)
}
}
self.updateTransitionProgress(0.0, transition: .animated(duration: 0.4, curve: .spring), completion: { [weak self] in
self?.dismiss(animated: false)
})
}
self.updateTransitionProgress(0.0, transition: .animated(duration: 0.4, curve: .spring), completion: { [weak self] in
self?.dismiss(animated: false)
})
} else {
self.dismiss(animated: false)
}
@ -1694,6 +1866,9 @@ public class CameraScreen: ViewController {
}
public func updateTransitionProgress(_ transitionFraction: CGFloat, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void = {}) {
if let layout = self.validLayout, case .regular = layout.metrics.widthClass {
return
}
let offsetX = floorToScreenPixels((1.0 - transitionFraction) * self.node.frame.width * -1.0)
transition.updateTransform(layer: self.node.backgroundView.layer, transform: CGAffineTransform(translationX: offsetX, y: 0.0))
transition.updateTransform(layer: self.node.containerView.layer, transform: CGAffineTransform(translationX: offsetX, y: 0.0))
@ -1713,6 +1888,9 @@ public class CameraScreen: ViewController {
}
public func completeWithTransitionProgress(_ transitionFraction: CGFloat, velocity: CGFloat, dismissing: Bool) {
if let layout = self.validLayout, case .regular = layout.metrics.widthClass {
return
}
if dismissing {
if transitionFraction < 0.7 || velocity < -1000.0 {
self.statusBar.updateStatusBarStyle(.Ignore, animated: true)
@ -1842,3 +2020,109 @@ private final class DualIconComponent: Component {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
private func pipPositionForLocation(layout: ContainerViewLayout, position: CGPoint, velocity: CGPoint) -> CameraScreen.PIPPosition {
var layoutInsets = layout.insets(options: [.input])
layoutInsets.bottom += 48.0
var result = CGPoint()
if position.x < layout.size.width / 2.0 {
result.x = 0.0
} else {
result.x = 1.0
}
if position.y < layoutInsets.top + (layout.size.height - layoutInsets.bottom - layoutInsets.top) / 2.0 {
result.y = 0.0
} else {
result.y = 1.0
}
let currentPosition = result
let angleEpsilon: CGFloat = 30.0
var shouldHide = false
if (velocity.x * velocity.x + velocity.y * velocity.y) >= 500.0 * 500.0 {
let x = velocity.x
let y = velocity.y
var angle = atan2(y, x) * 180.0 / CGFloat.pi * -1.0
if angle < 0.0 {
angle += 360.0
}
if currentPosition.x.isZero && currentPosition.y.isZero {
if ((angle > 0 && angle < 90 - angleEpsilon) || angle > 360 - angleEpsilon) {
result.x = 1.0
result.y = 0.0
} else if (angle > 180 + angleEpsilon && angle < 270 + angleEpsilon) {
result.x = 0.0
result.y = 1.0
} else if (angle > 270 + angleEpsilon && angle < 360 - angleEpsilon) {
result.x = 1.0
result.y = 1.0
} else {
shouldHide = true
}
} else if !currentPosition.x.isZero && currentPosition.y.isZero {
if (angle > 90 + angleEpsilon && angle < 180 + angleEpsilon) {
result.x = 0.0
result.y = 0.0
}
else if (angle > 270 - angleEpsilon && angle < 360 - angleEpsilon) {
result.x = 1.0
result.y = 1.0
}
else if (angle > 180 + angleEpsilon && angle < 270 - angleEpsilon) {
result.x = 0.0
result.y = 1.0
}
else {
shouldHide = true
}
} else if currentPosition.x.isZero && !currentPosition.y.isZero {
if (angle > 90 - angleEpsilon && angle < 180 - angleEpsilon) {
result.x = 0.0
result.y = 0.0
}
else if (angle < angleEpsilon || angle > 270 + angleEpsilon) {
result.x = 1.0
result.y = 1.0
}
else if (angle > angleEpsilon && angle < 90 - angleEpsilon) {
result.x = 1.0
result.y = 0.0
}
else if (!shouldHide) {
shouldHide = true
}
} else if !currentPosition.x.isZero && !currentPosition.y.isZero {
if (angle > angleEpsilon && angle < 90 + angleEpsilon) {
result.x = 1.0
result.y = 0.0
}
else if (angle > 180 - angleEpsilon && angle < 270 - angleEpsilon) {
result.x = 0.0
result.y = 1.0
}
else if (angle > 90 + angleEpsilon && angle < 180 - angleEpsilon) {
result.x = 0.0
result.y = 0.0
}
else if (!shouldHide) {
shouldHide = true
}
}
}
var position: CameraScreen.PIPPosition = .bottomRight
if result.x == 0.0 && result.y == 0.0 {
position = .topLeft
} else if result.x == 1.0 && result.y == 0.0 {
position = .topRight
} else if result.x == 0.0 && result.y == 1.0 {
position = .bottomLeft
} else if result.x == 1.0 && result.y == 1.0 {
position = .bottomRight
}
return position
}

View File

@ -18,17 +18,20 @@ extension CameraMode {
private let buttonSize = CGSize(width: 55.0, height: 44.0)
final class ModeComponent: Component {
let isTablet: Bool
let availableModes: [CameraMode]
let currentMode: CameraMode
let updatedMode: (CameraMode) -> Void
let tag: AnyObject?
init(
isTablet: Bool,
availableModes: [CameraMode],
currentMode: CameraMode,
updatedMode: @escaping (CameraMode) -> Void,
tag: AnyObject?
) {
self.isTablet = isTablet
self.availableModes = availableModes
self.currentMode = currentMode
self.updatedMode = updatedMode
@ -36,6 +39,9 @@ final class ModeComponent: Component {
}
static func ==(lhs: ModeComponent, rhs: ModeComponent) -> Bool {
if lhs.isTablet != rhs.isTablet {
return false
}
if lhs.availableModes != rhs.availableModes {
return false
}
@ -114,14 +120,16 @@ final class ModeComponent: Component {
func update(component: ModeComponent, availableSize: CGSize, transition: Transition) -> CGSize {
self.component = component
let isTablet = component.isTablet
let updatedMode = component.updatedMode
let spacing: CGFloat = 14.0
let spacing: CGFloat = isTablet ? 9.0 : 14.0
var i = 0
var itemFrame = CGRect(origin: .zero, size: buttonSize)
var selectedCenter = itemFrame.minX
for mode in component.availableModes {
let itemView: ItemView
if self.itemViews.count == i {
@ -137,20 +145,37 @@ final class ModeComponent: Component {
itemView.update(value: mode.title, selected: mode == component.currentMode)
itemView.bounds = CGRect(origin: .zero, size: itemFrame.size)
itemView.center = CGPoint(x: itemFrame.midX, y: itemFrame.midY)
if mode == component.currentMode {
selectedCenter = itemFrame.midX
if isTablet {
itemView.center = CGPoint(x: availableSize.width / 2.0, y: itemFrame.midY)
if mode == component.currentMode {
selectedCenter = itemFrame.midY
}
itemFrame = itemFrame.offsetBy(dx: 0.0, dy: buttonSize.height + spacing)
} else {
itemView.center = CGPoint(x: itemFrame.midX, y: itemFrame.midY)
if mode == component.currentMode {
selectedCenter = itemFrame.midX
}
itemFrame = itemFrame.offsetBy(dx: buttonSize.width + spacing, dy: 0.0)
}
i += 1
itemFrame = itemFrame.offsetBy(dx: buttonSize.width + spacing, dy: 0.0)
}
let totalSize = CGSize(width: buttonSize.width * CGFloat(component.availableModes.count) + spacing * CGFloat(component.availableModes.count - 1), height: buttonSize.height)
transition.setFrame(view: self.containerView, frame: CGRect(origin: CGPoint(x: availableSize.width / 2.0 - selectedCenter, y: 0.0), size: totalSize))
let totalSize: CGSize
let size: CGSize
if isTablet {
totalSize = CGSize(width: availableSize.width, height: buttonSize.height * CGFloat(component.availableModes.count) + spacing * CGFloat(component.availableModes.count - 1))
size = CGSize(width: availableSize.width, height: availableSize.height)
transition.setFrame(view: self.containerView, frame: CGRect(origin: CGPoint(x: 0.0, y: availableSize.height / 2.0 - selectedCenter), size: totalSize))
} else {
size = CGSize(width: availableSize.width, height: buttonSize.height)
totalSize = CGSize(width: buttonSize.width * CGFloat(component.availableModes.count) + spacing * CGFloat(component.availableModes.count - 1), height: buttonSize.height)
transition.setFrame(view: self.containerView, frame: CGRect(origin: CGPoint(x: availableSize.width / 2.0 - selectedCenter, y: 0.0), size: totalSize))
}
return CGSize(width: availableSize.width, height: buttonSize.height)
return size
}
}

View File

@ -121,12 +121,6 @@ private func lookupSpringValue(_ t: CGFloat) -> CGFloat {
}
}
return 1.0
// print("---start---")
// for i in 0 ..< 16 {
// let j = Double(i) * 1.0 / 16.0
// print("\(j) \(listViewAnimationCurveSystem(j))")
// }
// print("---end---")
}
private class ShutterBlobLayer: MetalImageLayer {
@ -214,12 +208,14 @@ final class ShutterBlobView: UIView {
private var displayLink: SharedDisplayLinkDriver.Link?
private var primarySize = AnimatableProperty<CGFloat>(value: 0.63)
private var primaryOffset = AnimatableProperty<CGFloat>(value: 0.0)
private var primaryOffsetX = AnimatableProperty<CGFloat>(value: 0.0)
private var primaryOffsetY = AnimatableProperty<CGFloat>(value: 0.0)
private var primaryRedness = AnimatableProperty<CGFloat>(value: 0.0)
private var primaryCornerRadius = AnimatableProperty<CGFloat>(value: 0.63)
private var secondarySize = AnimatableProperty<CGFloat>(value: 0.34)
private var secondaryOffset = AnimatableProperty<CGFloat>(value: 0.0)
private var secondaryOffsetX = AnimatableProperty<CGFloat>(value: 0.0)
private var secondaryOffsetY = AnimatableProperty<CGFloat>(value: 0.0)
private var secondaryRedness = AnimatableProperty<CGFloat>(value: 0.0)
private(set) var state: BlobState = .generic
@ -309,22 +305,42 @@ final class ShutterBlobView: UIView {
self.tick()
}
func updatePrimaryOffset(_ offset: CGFloat, transition: Transition = .immediate) {
func updatePrimaryOffsetX(_ offset: CGFloat, transition: Transition = .immediate) {
guard self.frame.height > 0.0 else {
return
}
let mappedOffset = offset / self.frame.height * 2.0
self.primaryOffset.update(value: mappedOffset, transition: transition)
self.primaryOffsetX.update(value: mappedOffset, transition: transition)
self.tick()
}
func updateSecondaryOffset(_ offset: CGFloat, transition: Transition = .immediate) {
func updatePrimaryOffsetY(_ offset: CGFloat, transition: Transition = .immediate) {
guard self.frame.height > 0.0 else {
return
}
let mappedOffset = offset / self.frame.width * 2.0
self.primaryOffsetY.update(value: mappedOffset, transition: transition)
self.tick()
}
func updateSecondaryOffsetX(_ offset: CGFloat, transition: Transition = .immediate) {
guard self.frame.height > 0.0 else {
return
}
let mappedOffset = offset / self.frame.height * 2.0
self.secondaryOffset.update(value: mappedOffset, transition: transition)
self.secondaryOffsetX.update(value: mappedOffset, transition: transition)
self.tick()
}
func updateSecondaryOffsetY(_ offset: CGFloat, transition: Transition = .immediate) {
guard self.frame.height > 0.0 else {
return
}
let mappedOffset = offset / self.frame.width * 2.0
self.secondaryOffsetY.update(value: mappedOffset, transition: transition)
self.tick()
}
@ -332,11 +348,13 @@ final class ShutterBlobView: UIView {
private func updateAnimations() {
let properties = [
self.primarySize,
self.primaryOffset,
self.primaryOffsetX,
self.primaryOffsetY,
self.primaryRedness,
self.primaryCornerRadius,
self.secondarySize,
self.secondaryOffset,
self.secondaryOffsetX,
self.secondaryOffsetY,
self.secondaryRedness
]
@ -407,20 +425,31 @@ final class ShutterBlobView: UIView {
var resolution = simd_uint2(UInt32(drawableSize.width), UInt32(drawableSize.height))
renderEncoder.setFragmentBytes(&resolution, length: MemoryLayout<simd_uint2>.size * 2, index: 0)
var primaryParameters = simd_float4(
var primaryParameters = simd_float3(
Float(self.primarySize.presentationValue),
Float(self.primaryOffset.presentationValue),
Float(self.primaryRedness.presentationValue),
Float(self.primaryCornerRadius.presentationValue)
)
renderEncoder.setFragmentBytes(&primaryParameters, length: MemoryLayout<simd_float4>.size, index: 1)
renderEncoder.setFragmentBytes(&primaryParameters, length: MemoryLayout<simd_float3>.size, index: 1)
var secondaryParameters = simd_float3(
var primaryOffset = simd_float2(
Float(self.primaryOffsetX.presentationValue),
Float(self.primaryOffsetY.presentationValue)
)
renderEncoder.setFragmentBytes(&primaryOffset, length: MemoryLayout<simd_float2>.size, index: 2)
var secondaryParameters = simd_float2(
Float(self.secondarySize.presentationValue),
Float(self.secondaryOffset.presentationValue),
Float(self.secondaryRedness.presentationValue)
)
renderEncoder.setFragmentBytes(&secondaryParameters, length: MemoryLayout<simd_float3>.size, index: 2)
renderEncoder.setFragmentBytes(&secondaryParameters, length: MemoryLayout<simd_float4>.size, index: 3)
var secondaryOffset = simd_float2(
Float(self.secondaryOffsetX.presentationValue),
Float(self.secondaryOffsetY.presentationValue)
)
renderEncoder.setFragmentBytes(&secondaryOffset, length: MemoryLayout<simd_float2>.size, index: 4)
renderEncoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 6, instanceCount: 1)
renderEncoder.endEncoding()

View File

@ -8,6 +8,7 @@ public final class DrawingStickerEntity: DrawingEntity, Codable {
public enum Content: Equatable {
case file(TelegramMediaFile)
case image(UIImage)
case video(String, UIImage?)
public static func == (lhs: Content, rhs: Content) -> Bool {
switch lhs {
@ -23,6 +24,12 @@ public final class DrawingStickerEntity: DrawingEntity, Codable {
} else {
return false
}
case let .video(lhsPath, _):
if case let .video(rhsPath, _) = rhs {
return lhsPath == rhsPath
} else {
return false
}
}
}
}
@ -30,6 +37,8 @@ public final class DrawingStickerEntity: DrawingEntity, Codable {
case uuid
case file
case image
case videoPath
case videoImage
case referenceDrawingSize
case position
case scale
@ -64,6 +73,8 @@ public final class DrawingStickerEntity: DrawingEntity, Codable {
return file.isAnimatedSticker || file.isVideoSticker || file.mimeType == "video/webm"
case .image:
return false
case .video:
return true
}
}
@ -92,6 +103,8 @@ public final class DrawingStickerEntity: DrawingEntity, Codable {
self.content = .file(file)
} else if let imageData = try container.decodeIfPresent(Data.self, forKey: .image), let image = UIImage(data: imageData) {
self.content = .image(image)
} else if let videoPath = try container.decodeIfPresent(String.self, forKey: .videoPath), let imageData = try container.decodeIfPresent(Data.self, forKey: .image), let image = UIImage(data: imageData) {
self.content = .video(videoPath, image)
} else {
fatalError()
}
@ -110,6 +123,9 @@ public final class DrawingStickerEntity: DrawingEntity, Codable {
try container.encode(file, forKey: .file)
case let .image(image):
try container.encodeIfPresent(image.pngData(), forKey: .image)
case let .video(path, image):
try container.encode(path, forKey: .videoPath)
try container.encodeIfPresent(image?.jpegData(compressionQuality: 0.87), forKey: .videoImage)
}
try container.encode(self.referenceDrawingSize, forKey: .referenceDrawingSize)
try container.encode(self.position, forKey: .position)

View File

@ -420,11 +420,16 @@ public final class MediaEditor {
if let self {
let start = self.values.videoTrimRange?.lowerBound ?? 0.0
self.player?.seek(to: CMTime(seconds: start, preferredTimescale: CMTimeScale(1000)))
self.onPlaybackAction(.seek(start))
self.player?.play()
self.onPlaybackAction(.play)
}
})
player.playImmediately(atRate: 1.0)
self.volumeFade = self.player?.fadeVolume(from: 0.0, to: 1.0, duration: 0.4)
Queue.mainQueue().justDispatch {
player.playImmediately(atRate: 1.0)
self.onPlaybackAction(.play)
self.volumeFade = self.player?.fadeVolume(from: 0.0, to: 1.0, duration: 0.4)
}
}
}
})
@ -460,6 +465,12 @@ public final class MediaEditor {
return self.values.toolValues[key]
}
private var previewUnedited = false
public func setPreviewUnedited(_ preview: Bool) {
self.previewUnedited = preview
self.updateRenderChain()
}
public func setToolValue(_ key: EditorToolKey, value: Any) {
self.updateValues { values in
var updatedToolValues = values.toolValues
@ -481,11 +492,20 @@ public final class MediaEditor {
}
}
public enum PlaybackAction {
case play
case pause
case seek(Double)
}
public var onPlaybackAction: (PlaybackAction) -> Void = { _ in }
private var targetTimePosition: (CMTime, Bool)?
private var updatingTimePosition = false
public func seek(_ position: Double, andPlay play: Bool) {
if !play {
self.player?.pause()
self.onPlaybackAction(.pause)
}
let targetPosition = CMTime(seconds: position, preferredTimescale: CMTimeScale(60.0))
if self.targetTimePosition?.0 != targetPosition {
@ -496,6 +516,7 @@ public final class MediaEditor {
}
if play {
self.player?.play()
self.onPlaybackAction(.play)
}
}
@ -505,14 +526,17 @@ public final class MediaEditor {
public func play() {
self.player?.play()
self.onPlaybackAction(.play)
}
public func stop() {
self.player?.pause()
self.onPlaybackAction(.pause)
}
public func invalidate() {
self.player?.pause()
self.onPlaybackAction(.pause)
self.renderer.textureSource?.invalidate()
}
@ -531,6 +555,7 @@ public final class MediaEditor {
}
}
})
self.onPlaybackAction(.seek(targetPosition.seconds))
}
public func setVideoTrimRange(_ trimRange: Range<Double>, apply: Bool) {
@ -558,6 +583,7 @@ public final class MediaEditor {
private var previousUpdateTime: Double?
private var scheduledUpdate = false
private func updateRenderChain() {
self.renderer.renderPassedEnabled = !self.previewUnedited
self.renderChain.update(values: self.values)
if let player = self.player, player.rate > 0.0 {
} else {

View File

@ -20,6 +20,8 @@ func composerEntitiesForDrawingEntity(account: Account, entity: DrawingEntity, c
content = .file(file)
case let .image(image):
content = .image(image)
case let .video(path, _):
content = .video(path)
}
return [MediaEditorComposerStickerEntity(account: account, content: content, position: entity.position, scale: entity.scale, rotation: entity.rotation, baseSize: entity.baseSize, mirrored: entity.mirrored, colorSpace: colorSpace)]
} else if let renderImage = entity.renderImage, let image = CIImage(image: renderImage, options: [.colorSpace: colorSpace]) {
@ -69,6 +71,7 @@ private class MediaEditorComposerStickerEntity: MediaEditorComposerEntity {
public enum Content {
case file(TelegramMediaFile)
case image(UIImage)
case video(String)
var file: TelegramMediaFile? {
if case let .file(file) = self {
@ -90,7 +93,10 @@ private class MediaEditorComposerStickerEntity: MediaEditorComposerEntity {
var source: AnimatedStickerNodeSource?
var frameSource = Promise<QueueLocalObject<AnimatedStickerDirectFrameSource>?>()
var videoFrameSource = Promise<QueueLocalObject<VideoStickerDirectFrameSource>?>()
var isVideo = false
var isVideoSticker = false
var assetReader: AVAssetReader?
var videoOutput: AVAssetReaderTrackOutput?
var frameCount: Int?
var frameRate: Int?
@ -118,9 +124,9 @@ private class MediaEditorComposerStickerEntity: MediaEditorComposerEntity {
case let .file(file):
if file.isAnimatedSticker || file.isVideoSticker || file.mimeType == "video/webm" {
self.isAnimated = true
self.isVideo = file.isVideoSticker || file.mimeType == "video/webm"
self.isVideoSticker = file.isVideoSticker || file.mimeType == "video/webm"
self.source = AnimatedStickerResourceSource(account: account, resource: file.resource, isVideo: isVideo)
self.source = AnimatedStickerResourceSource(account: account, resource: file.resource, isVideo: isVideoSticker)
let pathPrefix = account.postbox.mediaBox.shortLivedResourceCachePathPrefix(file.resource.id)
if let source = self.source {
let dimensions = file.dimensions ?? PixelDimensions(width: 512, height: 512)
@ -131,7 +137,7 @@ private class MediaEditorComposerStickerEntity: MediaEditorComposerEntity {
if let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: [.mappedRead]) {
let queue = strongSelf.queue
if strongSelf.isVideo {
if strongSelf.isVideoSticker {
let frameSource = QueueLocalObject<VideoStickerDirectFrameSource>(queue: queue, generate: {
return VideoStickerDirectFrameSource(queue: queue, path: path, width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), cachePathPrefix: pathPrefix, unpremultiplyAlpha: false)!
})
@ -180,6 +186,27 @@ private class MediaEditorComposerStickerEntity: MediaEditorComposerEntity {
case let .image(image):
self.isAnimated = false
self.imagePromise.set(.single(image))
case let .video(videoPath):
self.isAnimated = true
let url = URL(fileURLWithPath: videoPath)
let asset = AVURLAsset(url: url)
if let assetReader = try? AVAssetReader(asset: asset), let videoTrack = asset.tracks(withMediaType: .video).first {
let outputSettings: [String: Any] = [
kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_420YpCbCr8BiPlanarFullRange,
kCVPixelBufferMetalCompatibilityKey as String: true
]
let videoOutput = AVAssetReaderTrackOutput(track: videoTrack, outputSettings: outputSettings)
videoOutput.alwaysCopiesSampleData = true
if assetReader.canAdd(videoOutput) {
assetReader.add(videoOutput)
}
assetReader.startReading()
self.assetReader = assetReader
self.videoOutput = videoOutput
}
}
}
@ -187,9 +214,52 @@ private class MediaEditorComposerStickerEntity: MediaEditorComposerEntity {
self.disposables.dispose()
}
var tested = false
private var circleMaskFilter: CIFilter?
func image(for time: CMTime, frameRate: Float, completion: @escaping (CIImage?) -> Void) {
if self.isAnimated {
if case .video = self.content {
if let videoOutput = self.videoOutput {
if let sampleBuffer = videoOutput.copyNextSampleBuffer(), let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) {
var ciImage = CIImage(cvPixelBuffer: imageBuffer)
ciImage = ciImage.oriented(forExifOrientation: UIImage.Orientation.right.exifOrientation)
let minSide = min(ciImage.extent.size.width, ciImage.extent.size.height)
let cropRect = CGRect(origin: CGPoint(x: floor((ciImage.extent.size.width - minSide) / 2.0), y: floor((ciImage.extent.size.height - minSide) / 2.0)), size: CGSize(width: minSide, height: minSide))
ciImage = ciImage.cropped(to: cropRect).samplingLinear()
ciImage = ciImage.transformed(by: CGAffineTransform(translationX: 0.0, y: -420.0))
// ciImage = ciImage.transformed(by: CGAffineTransform(translationX: -ciImage.extent.midX, y: -ciImage.extent.midY))
// ciImage = ciImage.transformed(by: CGAffineTransform(rotationAngle: -.pi / 2.0))
// ciImage = ciImage.transformed(by: CGAffineTransform(translationX: ciImage.extent.midX, y: ciImage.extent.midY))
var circleMaskFilter: CIFilter?
if let current = self.circleMaskFilter {
circleMaskFilter = current
} else {
let circleImage = generateImage(CGSize(width: minSide, height: minSide), scale: 1.0, rotatedContext: { size, context in
context.clear(CGRect(origin: .zero, size: size))
context.setFillColor(UIColor.white.cgColor)
context.fillEllipse(in: CGRect(origin: .zero, size: size))
})!
let circleMask = CIImage(image: circleImage)
if let filter = CIFilter(name: "CIBlendWithAlphaMask") {
filter.setValue(circleMask, forKey: kCIInputMaskImageKey)
self.circleMaskFilter = filter
circleMaskFilter = filter
}
}
let _ = circleMaskFilter
if let circleMaskFilter {
circleMaskFilter.setValue(ciImage, forKey: kCIInputImageKey)
if let output = circleMaskFilter.outputImage {
ciImage = output
}
}
completion(ciImage)
}
} else {
completion(nil)
}
} else if self.isAnimated {
let currentTime = CMTimeGetSeconds(time)
var tintColor: UIColor?
@ -262,7 +332,7 @@ private class MediaEditorComposerStickerEntity: MediaEditorComposerEntity {
}
}
if self.isVideo {
if self.isVideoSticker {
self.disposables.add((self.videoFrameSource.get()
|> take(1)
|> deliverOn(self.queue)).start(next: { [weak self] frameSource in
@ -371,3 +441,20 @@ private func render(width: Int, height: Int, bytesPerRow: Int, data: Data, type:
return CIImage(cvPixelBuffer: pixelBuffer, options: [.colorSpace: deviceColorSpace])
}
private extension UIImage.Orientation {
var exifOrientation: Int32 {
switch self {
case .up: return 1
case .down: return 3
case .left: return 8
case .right: return 6
case .upMirrored: return 2
case .downMirrored: return 4
case .leftMirrored: return 5
case .rightMirrored: return 7
@unknown default:
return 0
}
}
}

View File

@ -150,6 +150,8 @@ final class MediaEditorRenderer: TextureConsumer {
self.renderPasses.forEach { $0.setup(device: device, library: library) }
}
var renderPassedEnabled = true
func renderFrame() {
let device: MTLDevice?
if let renderTarget = self.renderTarget {
@ -181,9 +183,11 @@ final class MediaEditorRenderer: TextureConsumer {
return
}
for renderPass in self.renderPasses {
if let nextTexture = renderPass.process(input: texture, device: device, commandBuffer: commandBuffer) {
texture = nextTexture
if self.renderPassedEnabled {
for renderPass in self.renderPasses {
if let nextTexture = renderPass.process(input: texture, device: device, commandBuffer: commandBuffer) {
texture = nextTexture
}
}
}
self.finalTexture = texture

View File

@ -37,6 +37,7 @@ swift_library(
"//submodules/Components/BlurredBackgroundComponent",
"//submodules/AvatarNode",
"//submodules/TelegramUI/Components/ShareWithPeersScreen",
"//submodules/TelegramUI/Components/CameraButtonComponent",
],
visibility = [
"//visibility:public",

View File

@ -386,3 +386,73 @@ final class AdjustmentsComponent: Component {
}
}
final class AdjustmentsScreenComponent: Component {
typealias EnvironmentType = Empty
let toggleUneditedPreview: (Bool) -> Void
init(
toggleUneditedPreview: @escaping (Bool) -> Void
) {
self.toggleUneditedPreview = toggleUneditedPreview
}
static func ==(lhs: AdjustmentsScreenComponent, rhs: AdjustmentsScreenComponent) -> Bool {
return true
}
final class View: UIView {
enum Field {
case blacks
case shadows
case midtones
case highlights
case whites
}
private var component: AdjustmentsScreenComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
super.init(frame: frame)
let longPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.handleLongPress(_:)))
longPressGestureRecognizer.minimumPressDuration = 0.05
self.addGestureRecognizer(longPressGestureRecognizer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc func handleLongPress(_ gestureRecognizer: UIPanGestureRecognizer) {
guard let component = self.component else {
return
}
switch gestureRecognizer.state {
case .began:
component.toggleUneditedPreview(true)
case .ended, .cancelled:
component.toggleUneditedPreview(false)
default:
break
}
}
func update(component: AdjustmentsScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
self.component = component
self.state = state
return availableSize
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View File

@ -1,5 +1,6 @@
import Foundation
import UIKit
import CoreServices
import Display
import AsyncDisplayKit
import ComponentFlow
@ -23,6 +24,7 @@ import ShareWithPeersScreen
import PresentationDataUtils
import ContextUI
import BundleIconComponent
import CameraButtonComponent
enum DrawingScreenType {
case drawing
@ -41,6 +43,7 @@ final class MediaEditorScreenComponent: Component {
let isDisplayingTool: Bool
let isInteractingWithEntities: Bool
let isSavingAvailable: Bool
let hasAppeared: Bool
let isDismissing: Bool
let mediaEditor: MediaEditor?
let privacy: MediaEditorResultPrivacy
@ -54,6 +57,7 @@ final class MediaEditorScreenComponent: Component {
isDisplayingTool: Bool,
isInteractingWithEntities: Bool,
isSavingAvailable: Bool,
hasAppeared: Bool,
isDismissing: Bool,
mediaEditor: MediaEditor?,
privacy: MediaEditorResultPrivacy,
@ -66,6 +70,7 @@ final class MediaEditorScreenComponent: Component {
self.isDisplayingTool = isDisplayingTool
self.isInteractingWithEntities = isInteractingWithEntities
self.isSavingAvailable = isSavingAvailable
self.hasAppeared = hasAppeared
self.isDismissing = isDismissing
self.mediaEditor = mediaEditor
self.privacy = privacy
@ -88,6 +93,9 @@ final class MediaEditorScreenComponent: Component {
if lhs.isSavingAvailable != rhs.isSavingAvailable {
return false
}
if lhs.hasAppeared != rhs.hasAppeared {
return false
}
if lhs.isDismissing != rhs.isDismissing {
return false
}
@ -175,6 +183,8 @@ final class MediaEditorScreenComponent: Component {
deinit {
self.playerStateDisposable?.dispose()
}
var muteDidChange = false
}
func makeState() -> State {
@ -239,7 +249,7 @@ final class MediaEditorScreenComponent: Component {
case camera
case gallery
}
func animateIn(from source: TransitionAnimationSource) {
func animateIn(from source: TransitionAnimationSource, completion: @escaping () -> Void = {}) {
let buttons = [
self.drawButton,
self.textButton,
@ -269,6 +279,8 @@ final class MediaEditorScreenComponent: Component {
view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2, delay: 0.0)
})
delay += 0.03
Queue.mainQueue().after(0.45, completion)
}
}
@ -452,11 +464,28 @@ final class MediaEditorScreenComponent: Component {
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 = 10.0
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,
@ -534,6 +563,16 @@ final class MediaEditorScreenComponent: Component {
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(
@ -549,7 +588,7 @@ final class MediaEditorScreenComponent: Component {
containerSize: CGSize(width: 40.0, height: 40.0)
)
let drawButtonFrame = CGRect(
origin: CGPoint(x: floorToScreenPixels(availableSize.width / 4.0 - 3.0 - drawButtonSize.width / 2.0), y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset + 1.0),
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 {
@ -576,7 +615,7 @@ final class MediaEditorScreenComponent: Component {
containerSize: CGSize(width: 40.0, height: 40.0)
)
let textButtonFrame = CGRect(
origin: CGPoint(x: floorToScreenPixels(availableSize.width / 2.5 + 5.0 - textButtonSize.width / 2.0), y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset + 1.0),
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 {
@ -603,7 +642,7 @@ final class MediaEditorScreenComponent: Component {
containerSize: CGSize(width: 40.0, height: 40.0)
)
let stickerButtonFrame = CGRect(
origin: CGPoint(x: floorToScreenPixels(availableSize.width - availableSize.width / 2.5 - 5.0 - stickerButtonSize.width / 2.0), y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset + 1.0),
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 {
@ -630,7 +669,7 @@ final class MediaEditorScreenComponent: Component {
containerSize: CGSize(width: 40.0, height: 40.0)
)
let toolsButtonFrame = CGRect(
origin: CGPoint(x: floorToScreenPixels(availableSize.width / 4.0 * 3.0 + 3.0 - toolsButtonSize.width / 2.0), y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset + 1.0),
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 {
@ -719,6 +758,14 @@ final class MediaEditorScreenComponent: Component {
timeoutSelected = timeout != nil
}
var inputPanelAvailableWidth = previewSize.width
if case .regular = environment.metrics.widthClass {
if (self.inputPanelExternalState.isEditing || self.inputPanelExternalState.hasText) {
inputPanelAvailableWidth += 200.0
}
}
self.inputPanel.parentState = state
let inputPanelSize = self.inputPanel.update(
transition: transition,
@ -765,7 +812,7 @@ final class MediaEditorScreenComponent: Component {
bottomInset: 0.0
)),
environment: {},
containerSize: CGSize(width: availableSize.width, height: 200.0)
containerSize: CGSize(width: inputPanelAvailableWidth, height: 200.0)
)
let fadeTransition = Transition(animation: .curve(duration: 0.3, curve: .easeInOut))
@ -802,7 +849,7 @@ final class MediaEditorScreenComponent: Component {
inputPanelBottomInset = environment.inputHeight - environment.safeInsets.bottom
inputPanelOffset = inputPanelBottomInset
}
let inputPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - environment.safeInsets.bottom - inputPanelBottomInset - inputPanelSize.height - 3.0), size: inputPanelSize)
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)
@ -839,6 +886,7 @@ final class MediaEditorScreenComponent: Component {
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
)
@ -852,10 +900,18 @@ final class MediaEditorScreenComponent: Component {
environment: {},
containerSize: CGSize(width: 44.0, height: 44.0)
)
let privacyButtonFrame = CGRect(
origin: CGPoint(x: 16.0, y: environment.safeInsets.top + 20.0 - inputPanelOffset),
size: privacyButtonSize
)
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)
@ -866,10 +922,11 @@ final class MediaEditorScreenComponent: Component {
transition.setAlpha(view: privacyButtonView, alpha: displayTopButtons && !component.isDismissing && !component.isInteractingWithEntities ? 1.0 : 0.0)
}
let saveButtonSize = self.saveButton.update(
transition: transition,
component: AnyComponent(Button(
content: AnyComponent(
let saveContentComponent: AnyComponentWithIdentity<Empty>
if component.hasAppeared {
saveContentComponent = AnyComponentWithIdentity(
id: "animatedIcon",
component: AnyComponent(
LottieAnimationComponent(
animation: LottieAnimationComponent.AnimationItem(
name: "anim_storysave",
@ -877,9 +934,26 @@ final class MediaEditorScreenComponent: Component {
range: nil
),
colors: ["__allcolors__": .white],
size: CGSize(width: 33.0, height: 33.0)
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()
@ -916,22 +990,42 @@ final class MediaEditorScreenComponent: Component {
if let playerState = state.playerState, playerState.hasAudio {
let isVideoMuted = component.mediaEditor?.values.videoIsMuted ?? false
let muteButtonSize = self.muteButton.update(
transition: transition,
component: AnyComponent(Button(
content: AnyComponent(
let muteContentComponent: AnyComponentWithIdentity<Empty>
if component.hasAppeared {
muteContentComponent = AnyComponentWithIdentity(
id: "animatedIcon",
component: AnyComponent(
LottieAnimationComponent(
animation: LottieAnimationComponent.AnimationItem(
name: "anim_storymute",
mode: .animating(loop: false),
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: 33.0, height: 33.0)
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()
@ -1106,7 +1200,7 @@ final class MediaEditorScreenComponent: Component {
private let storyDimensions = CGSize(width: 1080.0, height: 1920.0)
private let storyMaxVideoDuration: Double = 60.0
public final class MediaEditorScreen: ViewController {
public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate {
public enum TransitionIn {
public final class GalleryTransitionIn {
public weak var sourceView: UIView?
@ -1166,6 +1260,7 @@ public final class MediaEditorScreen: ViewController {
private var wasPlaying = false
private let backgroundDimView: UIView
fileprivate let containerView: UIView
fileprivate let componentHost: ComponentView<ViewControllerComponentContainer.Environment>
fileprivate let storyPreview: ComponentView<Empty>
fileprivate let toolValue: ComponentView<Empty>
@ -1190,6 +1285,8 @@ public final class MediaEditorScreen: ViewController {
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
@ -1207,6 +1304,9 @@ public final class MediaEditorScreen: ViewController {
self.backgroundDimView.isHidden = true
self.backgroundDimView.backgroundColor = .black
self.containerView = UIView()
self.containerView.clipsToBounds = true
self.componentHost = ComponentView<ViewControllerComponentContainer.Environment>()
self.storyPreview = ComponentView<Empty>()
self.toolValue = ComponentView<Empty>()
@ -1241,7 +1341,8 @@ public final class MediaEditorScreen: ViewController {
self.backgroundColor = .clear
self.view.addSubview(self.backgroundDimView)
self.view.addSubview(self.previewContainerView)
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)
@ -1276,7 +1377,7 @@ public final class MediaEditorScreen: ViewController {
areUnicodeEmojiEnabled: true,
areCustomEmojiEnabled: true,
chatPeerId: controller.context.account.peerId,
hasSearch: false,
hasSearch: true,
forceHasPremium: true
)
@ -1287,7 +1388,7 @@ public final class MediaEditorScreen: ViewController {
stickerNamespaces: [Namespaces.ItemCollection.CloudStickerPacks],
stickerOrderedItemListCollectionIds: [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers, Namespaces.OrderedItemList.CloudAllPremiumStickers],
chatPeerId: controller.context.account.peerId,
hasSearch: false,
hasSearch: true,
hasTrending: true,
forceHasPremium: true
)
@ -1370,8 +1471,8 @@ public final class MediaEditorScreen: ViewController {
mediaEntity.scale = storyDimensions.width / fittedSize.width
}
self.entitiesView.add(mediaEntity, announce: false)
if case let .image(_, _, additionalImage) = subject, let additionalImage {
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)
@ -1386,8 +1487,15 @@ public final class MediaEditorScreen: ViewController {
imageEntity.referenceDrawingSize = storyDimensions
imageEntity.scale = 1.49
imageEntity.mirrored = true
imageEntity.position = CGPoint(x: storyDimensions.width - 224.0, y: storyDimensions.height - 403.0)
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
@ -1451,6 +1559,31 @@ public final class MediaEditorScreen: ViewController {
}
})
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() {
@ -1679,13 +1812,19 @@ public final class MediaEditorScreen: ViewController {
}
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)
view.animateIn(from: .camera, completion: completion)
}
if let subject = self.subject, case let .video(_, transitionImage, _) = subject, let transitionImage {
if let subject = self.subject, case let .video(_, transitionImage, _, _, _, _) = subject, let transitionImage {
self.setupTransitionImage(transitionImage)
}
case let .gallery(transitionIn):
@ -1703,7 +1842,9 @@ public final class MediaEditorScreen: ViewController {
let duration: Double = 0.4
self.previewContainerView.layer.animatePosition(from: sourceLocalFrame.center, to: self.previewContainerView.center, duration: duration, timingFunction: kCAMediaTimingFunctionSpring)
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)
@ -1719,7 +1860,7 @@ public final class MediaEditorScreen: ViewController {
}
} else {
if let view = self.componentHost.view as? MediaEditorScreenComponent.View {
view.animateIn(from: .camera)
view.animateIn(from: .camera, completion: completion)
}
}
@ -1778,19 +1919,12 @@ public final class MediaEditorScreen: ViewController {
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)
let snapshotTransform = CATransform3DMakeScale(0.001, snapshotScale, 1.0)
//snapshotTransform.m34 = 1.0 / -500
//snapshotTransform = CATransform3DRotate(snapshotTransform, -90.0 * .pi / 180.0, 0.0, 1.0, 0.0)
snapshotView.layer.transform = CATransform3DMakeScale(snapshotScale, snapshotScale, 1.0)
let targetTransform = CATransform3DMakeScale(snapshotScale, snapshotScale, 1.0)
//snapshotTransform
//targetTransform = CATransform3DRotate(targetTransform, 0.0, 0.0, 1.0, 0.0)
snapshotView.layer.transform = snapshotTransform
snapshotView.alpha = 0.0
Queue.mainQueue().after(0.15) {
snapshotView.layer.transform = targetTransform
snapshotView.layer.animate(from: NSValue(caTransform3D: snapshotTransform), to: NSValue(caTransform3D: targetTransform), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25)
snapshotView.alpha = 1.0
snapshotView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
}
self.previewContainerView.addSubview(snapshotView)
@ -2036,6 +2170,23 @@ public final class MediaEditorScreen: ViewController {
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 {
@ -2045,24 +2196,44 @@ public final class MediaEditorScreen: ViewController {
return result
}
func requestUpdate(transition: Transition = .immediate) {
func requestUpdate(hasAppeared: Bool = false, transition: Transition = .immediate) {
if let layout = self.validLayout {
self.containerLayoutUpdated(layout: layout, transition: transition)
self.containerLayoutUpdated(layout: layout, hasAppeared: hasAppeared, transition: transition)
}
}
private var drawingScreen: DrawingScreen?
func containerLayoutUpdated(layout: ContainerViewLayout, forceUpdate: Bool = false, animateOut: Bool = false, transition: Transition) {
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 previewSize = CGSize(width: layout.size.width, height: floorToScreenPixels(layout.size.width * 1.77778))
let topInset: CGFloat = (layout.statusBarHeight ?? 0.0) + 12.0 //floorToScreenPixels(layout.size.height - previewSize.height) / 2.0
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,
@ -2072,7 +2243,7 @@ public final class MediaEditorScreen: ViewController {
bottom: bottomInset,
right: layout.safeInsets.right
),
inputHeight: layout.inputHeight ?? 0.0,
inputHeight: inputHeight,
metrics: layout.metrics,
deviceMetrics: layout.deviceMetrics,
orientation: nil,
@ -2084,6 +2255,10 @@ public final class MediaEditorScreen: ViewController {
return self?.controller
}
)
if hasAppeared && !self.hasAppeared {
self.hasAppeared = hasAppeared
}
let componentSize = self.componentHost.update(
transition: transition,
@ -2093,6 +2268,7 @@ public final class MediaEditorScreen: ViewController {
isDisplayingTool: self.isDisplayingTool,
isInteractingWithEntities: self.isInteractingWithEntities,
isSavingAvailable: controller.isSavingAvailable,
hasAppeared: self.hasAppeared,
isDismissing: self.isDismissing,
mediaEditor: self.mediaEditor,
privacy: controller.state.privacy,
@ -2113,14 +2289,24 @@ public final class MediaEditorScreen: ViewController {
case .sticker:
let controller = StickerPickerScreen(context: self.context, inputData: self.stickerPickerInputData.get())
controller.completion = { [weak self] file in
if let self, let file {
let stickerEntity = DrawingStickerEntity(content: .file(file))
self.interaction?.insertEntity(stickerEntity)
self.controller?.isSavingAvailable = true
self.controller?.requestLayout(transition: .immediate)
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:
@ -2189,12 +2375,12 @@ public final class MediaEditorScreen: ViewController {
environment: {
environment
},
forceUpdate: forceUpdate || animateOut,
forceUpdate: forceUpdate,
containerSize: layout.size
)
if let componentView = self.componentHost.view {
if componentView.superview == nil {
self.view.insertSubview(componentView, at: 3)
self.containerView.addSubview(componentView)
componentView.clipsToBounds = true
}
transition.setFrame(view: componentView, frame: CGRect(origin: CGPoint(x: 0.0, y: self.dismissOffset), size: componentSize))
@ -2248,15 +2434,20 @@ public final class MediaEditorScreen: ViewController {
transition.setAlpha(view: self.backgroundDimView, alpha: self.isDismissing ? 0.0 : 1.0)
var bottomInputOffset: CGFloat = 0.0
if let inputHeight = layout.inputHeight, inputHeight > 0.0 {
if self.entitiesView.selectedEntityView != nil || self.isDisplayingTool {
bottomInputOffset = inputHeight / 2.0
} else {
bottomInputOffset = inputHeight - bottomInset - 17.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
}
}
}
let previewFrame = CGRect(origin: CGPoint(x: 0.0, y: topInset - bottomInputOffset + self.dismissOffset), size: previewSize)
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)
@ -2278,15 +2469,35 @@ public final class MediaEditorScreen: ViewController {
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?)
case video(String, UIImage?, PixelDimensions)
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):
case let .image(_, dimensions, _, _), let .video(_, _, _, _, dimensions, _):
return dimensions
case let .asset(asset):
return PixelDimensions(width: Int32(asset.pixelWidth), height: Int32(asset.pixelHeight))
@ -2297,9 +2508,9 @@ public final class MediaEditorScreen: ViewController {
var editorSubject: MediaEditor.Subject {
switch self {
case let .image(image, dimensions, _):
case let .image(image, dimensions, _, _):
return .image(image, dimensions)
case let .video(videoPath, transitionImage, dimensions):
case let .video(videoPath, transitionImage, _, _, dimensions, _):
return .video(videoPath, transitionImage, dimensions)
case let .asset(asset):
return .asset(asset)
@ -2310,9 +2521,9 @@ public final class MediaEditorScreen: ViewController {
var mediaContent: DrawingMediaEntity.Content {
switch self {
case let .image(image, dimensions, _):
case let .image(image, dimensions, _, _):
return .image(image, dimensions)
case let .video(videoPath, _, dimensions):
case let .video(videoPath, _, _, _, dimensions, _):
return .video(videoPath, dimensions)
case let .asset(asset):
return .asset(asset)
@ -2381,6 +2592,9 @@ public final class MediaEditorScreen: ViewController {
self.displayNode = Node(controller: self)
super.displayNodeDidLoad()
let dropInteraction = UIDropInteraction(delegate: self)
self.displayNode.view.addInteraction(dropInteraction)
}
func openPrivacySettings() {
@ -2735,9 +2949,9 @@ public final class MediaEditorScreen: ViewController {
}
switch subject {
case let .image(image, dimensions, _):
case let .image(image, dimensions, _, _):
saveImageDraft(image, dimensions)
case let .video(path, _, dimensions):
case let .video(path, _, _, _, dimensions, _):
saveVideoDraft(path, dimensions)
case let .asset(asset):
if asset.mediaType == .video {
@ -2780,6 +2994,7 @@ public final class MediaEditorScreen: ViewController {
self.dismissAllTooltips()
mediaEditor.seek(0.0, andPlay: false)
mediaEditor.invalidate()
self.node.entitiesView.invalidate()
@ -2802,14 +3017,14 @@ public final class MediaEditorScreen: ViewController {
let videoResult: Result.VideoResult
let duration: Double
switch subject {
case let .image(image, _, _):
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, _, _):
case let .video(path, _, _, _, _, _):
videoResult = .videoFile(path: path)
if let videoTrimRange = mediaEditor.values.videoTrimRange {
duration = videoTrimRange.upperBound - videoTrimRange.lowerBound
@ -2840,14 +3055,20 @@ public final class MediaEditorScreen: ViewController {
duration = 5.0
}
}
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()
}
})
})
// 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)
@ -2925,10 +3146,10 @@ public final class MediaEditorScreen: ViewController {
let exportSubject: Signal<MediaEditorVideoExport.Subject, NoError>
switch subject {
case let .video(path, _, _):
case let .video(path, _, _, _, _, _):
let asset = AVURLAsset(url: NSURL(fileURLWithPath: path) as URL)
exportSubject = .single(.video(asset))
case let .image(image, _, _):
case let .image(image, _, _, _):
exportSubject = .single(.image(image))
case let .asset(asset):
exportSubject = Signal { subscriber in
@ -3061,21 +3282,60 @@ public final class MediaEditorScreen: ViewController {
(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
}
@ -3106,7 +3366,7 @@ final class PrivacyButtonComponent: CombinedComponent {
let backgroundSize = CGSize(width: text.size.width + 38.0, height: 30.0)
let background = background.update(
component: BlurredBackgroundComponent(color: UIColor(white: 0.0, alpha: 0.5)),
component: BlurredBackgroundComponent(color: context.component.backgroundColor),
availableSize: backgroundSize,
transition: .immediate
)

View File

@ -323,16 +323,33 @@ private final class MediaToolsScreenComponent: Component {
self.component = component
self.state = state
let isTablet: Bool
if case .regular = environment.metrics.widthClass {
isTablet = true
} else {
isTablet = false
}
let mediaEditor = (environment.controller() as? MediaToolsScreen)?.mediaEditor
let sectionUpdated = component.sectionUpdated
let previewContainerFrame = CGRect(origin: CGPoint(x: 0.0, y: environment.safeInsets.top), size: CGSize(width: availableSize.width, height: availableSize.height - environment.safeInsets.top - environment.safeInsets.bottom))
let buttonsContainerFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - environment.safeInsets.bottom), size: CGSize(width: availableSize.width, height: environment.safeInsets.bottom))
let buttonSideInset: CGFloat = 10.0
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 previewContainerFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - previewSize.width) / 2.0), y: environment.safeInsets.top), size: CGSize(width: previewSize.width, height: availableSize.height - environment.safeInsets.top - environment.safeInsets.bottom))
let buttonsContainerFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - environment.safeInsets.bottom), size: CGSize(width: availableSize.width, height: environment.safeInsets.bottom))
let cancelButtonSize = self.cancelButton.update(
transition: transition,
component: AnyComponent(Button(
@ -396,6 +413,16 @@ private final class MediaToolsScreenComponent: Component {
transition.setFrame(view: doneButtonView, frame: doneButtonFrame)
}
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 adjustmentsButtonSize = self.adjustmentsButton.update(
transition: transition,
component: AnyComponent(Button(
@ -412,7 +439,7 @@ private final class MediaToolsScreenComponent: Component {
containerSize: CGSize(width: 40.0, height: 40.0)
)
let adjustmentsButtonFrame = CGRect(
origin: CGPoint(x: floorToScreenPixels(availableSize.width / 4.0 - 3.0 - adjustmentsButtonSize.width / 2.0), y: buttonBottomInset),
origin: CGPoint(x: buttonsLeftOffset + floorToScreenPixels(buttonsAvailableWidth / 4.0 - 3.0 - adjustmentsButtonSize.width / 2.0), y: buttonBottomInset),
size: adjustmentsButtonSize
)
if let adjustmentsButtonView = self.adjustmentsButton.view {
@ -438,7 +465,7 @@ private final class MediaToolsScreenComponent: Component {
containerSize: CGSize(width: 40.0, height: 40.0)
)
let tintButtonFrame = CGRect(
origin: CGPoint(x: floorToScreenPixels(availableSize.width / 2.5 + 5.0 - tintButtonSize.width / 2.0), y: buttonBottomInset),
origin: CGPoint(x: buttonsLeftOffset + floorToScreenPixels(buttonsAvailableWidth / 2.5 + 5.0 - tintButtonSize.width / 2.0), y: buttonBottomInset),
size: tintButtonSize
)
if let tintButtonView = self.tintButton.view {
@ -464,7 +491,7 @@ private final class MediaToolsScreenComponent: Component {
containerSize: CGSize(width: 40.0, height: 40.0)
)
let blurButtonFrame = CGRect(
origin: CGPoint(x: floorToScreenPixels(availableSize.width - availableSize.width / 2.5 - 5.0 - blurButtonSize.width / 2.0), y: buttonBottomInset),
origin: CGPoint(x: floorToScreenPixels(availableSize.width - buttonsLeftOffset - buttonsAvailableWidth / 2.5 - 5.0 - blurButtonSize.width / 2.0), y: buttonBottomInset),
size: blurButtonSize
)
if let blurButtonView = self.blurButton.view {
@ -490,7 +517,7 @@ private final class MediaToolsScreenComponent: Component {
containerSize: CGSize(width: 40.0, height: 40.0)
)
let curvesButtonFrame = CGRect(
origin: CGPoint(x: floorToScreenPixels(availableSize.width / 4.0 * 3.0 + 3.0 - curvesButtonSize.width / 2.0), y: buttonBottomInset),
origin: CGPoint(x: buttonsLeftOffset + floorToScreenPixels(buttonsAvailableWidth / 4.0 * 3.0 + 3.0 - curvesButtonSize.width / 2.0), y: buttonBottomInset),
size: curvesButtonSize
)
if let curvesButtonView = self.curvesButton.view {
@ -640,10 +667,31 @@ private final class MediaToolsScreenComponent: Component {
}
)),
environment: {},
containerSize: availableSize
containerSize: previewContainerFrame.size
)
let adjustmentsToolScreen: ComponentView<Empty>
if let current = self.toolScreen, !sectionChanged {
adjustmentsToolScreen = current
} else {
adjustmentsToolScreen = ComponentView<Empty>()
self.toolScreen = adjustmentsToolScreen
}
toolScreen = adjustmentsToolScreen
screenSize = adjustmentsToolScreen.update(
transition: optionsTransition,
component: AnyComponent(
AdjustmentsScreenComponent(
toggleUneditedPreview: { preview in
if let controller = environment.controller() as? MediaToolsScreen {
controller.mediaEditor.setPreviewUnedited(preview)
}
}
)
),
environment: {},
containerSize: CGSize(width: previewContainerFrame.width, height: previewContainerFrame.height - optionsSize.height)
)
screenSize = previewContainerFrame.size
self.toolScreen = nil
case .tint:
self.curvesState = nil
optionsSize = self.toolOptions.update(
@ -676,10 +724,31 @@ private final class MediaToolsScreenComponent: Component {
}
)),
environment: {},
containerSize: availableSize
containerSize: previewContainerFrame.size
)
let tintToolScreen: ComponentView<Empty>
if let current = self.toolScreen, !sectionChanged {
tintToolScreen = current
} else {
tintToolScreen = ComponentView<Empty>()
self.toolScreen = tintToolScreen
}
toolScreen = tintToolScreen
screenSize = tintToolScreen.update(
transition: optionsTransition,
component: AnyComponent(
AdjustmentsScreenComponent(
toggleUneditedPreview: { preview in
if let controller = environment.controller() as? MediaToolsScreen {
controller.mediaEditor.setPreviewUnedited(preview)
}
}
)
),
environment: {},
containerSize: CGSize(width: previewContainerFrame.width, height: previewContainerFrame.height - optionsSize.height)
)
screenSize = previewContainerFrame.size
self.toolScreen = nil
case .blur:
self.curvesState = nil
optionsSize = self.toolOptions.update(
@ -706,7 +775,7 @@ private final class MediaToolsScreenComponent: Component {
}
)),
environment: {},
containerSize: availableSize
containerSize: previewContainerFrame.size
)
let blurToolScreen: ComponentView<Empty>
@ -764,7 +833,7 @@ private final class MediaToolsScreenComponent: Component {
internalState: internalState
)),
environment: {},
containerSize: availableSize
containerSize: previewContainerFrame.size
)
let curvesToolScreen: ComponentView<Empty>
@ -917,9 +986,22 @@ public final class MediaToolsScreen: ViewController {
}
let isFirstTime = self.validLayout == nil
self.validLayout = layout
let isTablet: Bool
if case .regular = layout.metrics.widthClass {
isTablet = true
} else {
isTablet = false
}
let previewSize = CGSize(width: layout.size.width, height: floorToScreenPixels(layout.size.width * 1.77778))
let previewSize: CGSize
let topInset: CGFloat = (layout.statusBarHeight ?? 0.0) + 12.0
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
let environment = ViewControllerComponentContainer.Environment(
@ -944,13 +1026,6 @@ public final class MediaToolsScreen: ViewController {
}
)
// var transition = transition
// if isFirstTime {
// transition = transition.withUserData(CameraScreenTransition.animateIn)
// } else if animateOut {
// transition = transition.withUserData(CameraScreenTransition.animateOut)
// }
let componentSize = self.componentHost.update(
transition: transition,
component: AnyComponent(

View File

@ -1,7 +1,7 @@
{
"images" : [
{
"filename" : "off shadow.pdf",
"filename" : "off.pdf",
"idiom" : "universal"
}
],

View File

@ -0,0 +1,378 @@
%PDF-1.7
1 0 obj
<< /Type /XObject
/Length 2 0 R
/Group << /Type /Group
/S /Transparency
>>
/Subtype /Form
/Resources << >>
/BBox [ 0.000000 0.000000 512.000000 512.000000 ]
>>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 187.691406 139.141342 cm
1.000000 1.000000 1.000000 scn
112.318077 230.817108 m
114.451881 236.621613 106.507004 239.951096 102.709404 235.070618 c
70.894165 194.178741 37.289165 151.643738 1.607707 108.848358 c
-1.328853 105.327118 -0.157611 101.418579 4.426768 101.506943 c
20.839167 101.823730 57.499165 100.883728 57.172985 100.331940 c
57.264164 100.648727 34.854565 40.328934 24.311525 11.312073 c
22.424946 6.120453 29.099884 2.524017 31.687706 5.647629 c
67.369164 48.713730 102.736664 92.306229 134.258621 130.929901 c
138.408722 136.015289 136.225098 141.255798 130.969559 141.268005 c
115.661659 141.303726 79.236656 141.303726 79.280838 141.150513 c
79.236656 141.303726 103.559158 206.986237 112.318077 230.817108 c
h
f
n
Q
q
1.000000 0.000000 -0.000000 1.000000 187.691406 134.211075 cm
1.000000 1.000000 1.000000 scn
112.318077 235.747375 m
108.788986 237.044708 l
108.788910 237.044510 l
112.318077 235.747375 l
h
102.709404 240.000885 m
99.741928 242.309921 l
99.741806 242.309769 l
102.709404 240.000885 l
h
1.607707 113.778625 m
4.495335 111.370468 l
4.495601 111.370789 l
1.607707 113.778625 l
h
4.426768 106.437210 m
4.499225 102.677917 l
4.499327 102.677917 l
4.426768 106.437210 l
h
57.172985 105.262207 m
53.559685 106.302246 l
53.036144 104.483353 53.946251 102.558838 55.684322 101.809464 c
57.422398 101.060089 59.446564 101.719498 60.409737 103.348831 c
57.172985 105.262207 l
h
24.311525 16.242340 m
27.845428 14.958160 l
27.845484 14.958298 l
24.311525 16.242340 l
h
31.687706 10.577896 m
28.792362 12.976761 l
28.792278 12.976669 l
31.687706 10.577896 l
h
134.258621 135.860168 m
137.171616 133.482773 l
137.171692 133.482849 l
134.258621 135.860168 l
h
130.969559 146.198273 m
130.960785 142.438293 l
130.960815 142.438293 l
130.969559 146.198273 l
h
75.668030 145.039047 m
76.243362 143.043747 78.327271 141.892639 80.322571 142.467957 c
82.317871 143.043304 83.468979 145.127197 82.893646 147.122498 c
75.668030 145.039047 l
h
115.847176 234.450043 m
116.701851 236.774994 116.619583 239.131042 115.641846 241.171738 c
114.692940 243.152237 113.039856 244.550476 111.229118 245.332687 c
107.622391 246.890762 102.796585 246.235611 99.741928 242.309921 c
105.676872 237.691833 l
106.419823 238.646622 107.465248 238.766968 108.246910 238.429306 c
108.630371 238.263657 108.800774 238.046204 108.860069 237.922424 c
108.890541 237.858841 109.001205 237.622009 108.788986 237.044708 c
115.847176 234.450043 l
h
99.741806 242.309769 m
67.937111 201.431442 34.361767 158.934448 -1.280186 116.186462 c
4.495601 111.370789 l
40.216564 154.213562 73.851219 196.786560 105.676994 237.691986 c
99.741806 242.309769 l
h
-1.279920 116.186783 m
-3.258407 113.814362 -4.442426 110.586304 -3.346803 107.497391 c
-2.147628 104.116531 1.122458 102.612823 4.499225 102.677917 c
4.354311 110.196518 l
3.731298 110.184509 3.509113 110.313995 3.518988 110.308289 c
3.525570 110.304489 3.570336 110.276184 3.624385 110.211868 c
3.678738 110.147186 3.717865 110.075272 3.740574 110.011246 c
3.787205 109.879776 3.727518 109.904007 3.795444 110.156387 c
3.863918 110.410812 4.051676 110.838470 4.495335 111.370468 c
-1.279920 116.186783 l
h
4.499327 102.677917 m
12.622606 102.834702 25.825699 102.680527 36.987919 102.416473 c
42.562939 102.284592 47.599640 102.125977 51.211418 101.966782 c
53.024452 101.886871 54.443344 101.808304 55.382313 101.735397 c
55.867409 101.697739 56.148544 101.667191 56.272243 101.649155 c
56.369865 101.634918 56.239033 101.647354 56.024120 101.711334 c
55.987492 101.722244 55.526577 101.847748 55.024200 102.194824 c
54.785992 102.359390 54.143162 102.837692 53.738750 103.746338 c
53.220856 104.909958 53.351353 106.186188 53.936230 107.175598 c
60.409737 103.348831 l
60.999710 104.346863 61.130787 105.631760 60.609016 106.804092 c
60.200726 107.721451 59.549557 108.208511 59.298603 108.381897 c
58.770741 108.746567 58.266712 108.889877 58.169785 108.918732 c
57.834274 109.018616 57.514095 109.067596 57.357121 109.090485 c
56.971634 109.146683 56.482571 109.192612 55.964401 109.232834 c
54.896851 109.315720 53.376972 109.398636 51.542557 109.479492 c
47.859436 109.641830 42.766521 109.801880 37.165764 109.934372 c
25.976433 110.199066 12.643328 110.356506 4.354208 110.196518 c
4.499327 102.677917 l
h
60.786282 104.222183 m
60.809654 104.307053 60.897423 104.745377 60.929855 105.103439 c
58.742435 108.680450 54.195972 107.559937 53.865387 107.051208 c
53.814571 106.953857 53.747608 106.813751 53.728859 106.771683 c
53.718666 106.748230 53.702507 106.710159 53.696033 106.694580 c
53.676624 106.647598 53.659908 106.604401 53.655502 106.593048 c
53.640240 106.553711 53.620911 106.502945 53.600471 106.448990 c
53.557949 106.336746 53.495045 106.169449 53.413906 105.952942 c
53.251022 105.518295 53.008900 104.869919 52.696381 104.031509 c
52.071083 102.353989 51.161480 99.908981 50.034843 96.876450 c
47.781475 90.811127 44.658810 82.392609 41.203846 73.057465 c
34.294983 54.390060 26.053322 32.046463 20.777569 17.526367 c
27.845484 14.958298 l
33.112766 29.455078 41.347427 51.779800 48.256329 70.447311 c
51.710243 79.779633 54.831825 88.195221 57.084080 94.257538 c
58.210258 97.288834 59.118813 99.731033 59.742771 101.404968 c
60.054882 102.242279 60.295158 102.885696 60.455673 103.314011 c
60.536228 103.528976 60.595272 103.685944 60.632847 103.785141 c
60.652458 103.836914 60.662716 103.863678 60.666313 103.872955 c
60.671341 103.885910 60.661373 103.859787 60.646000 103.822586 c
60.640533 103.809433 60.625214 103.773346 60.615692 103.751419 c
60.597614 103.710876 60.531147 103.571838 60.480659 103.475098 c
60.150402 102.966980 55.604095 101.846619 53.416664 105.423325 c
53.449085 105.781067 53.536671 106.218613 53.559685 106.302246 c
60.786282 104.222183 l
h
20.777622 17.526520 m
19.123594 12.974854 21.347857 9.066895 24.252548 7.152451 c
26.852655 5.438766 31.562777 4.533417 34.583130 8.179123 c
28.792278 12.976669 l
28.944626 13.160553 29.138533 13.175156 29.096033 13.174225 c
28.995413 13.172028 28.716915 13.216476 28.390881 13.431351 c
28.077776 13.637726 27.886385 13.893021 27.802916 14.105072 c
27.740448 14.263748 27.684525 14.515366 27.845428 14.958160 c
20.777622 17.526520 l
h
34.583050 8.179031 m
70.276260 51.259308 105.658882 94.870392 137.171616 133.482773 c
131.345612 138.237549 l
99.814445 99.602600 64.462067 56.028687 28.792362 12.976761 c
34.583050 8.179031 l
h
137.171692 133.482849 m
139.813660 136.720245 140.928268 140.649231 139.688675 144.177872 c
138.396896 147.855057 134.944427 149.949036 130.978302 149.958267 c
130.960815 142.438293 l
131.647614 142.436691 132.038910 142.268158 132.232162 142.142303 c
132.415222 142.023071 132.526093 141.877975 132.593735 141.685455 c
132.722305 141.319427 132.853668 140.085480 131.345535 138.237473 c
137.171692 133.482849 l
h
130.978333 149.958267 m
123.319649 149.976135 110.381828 149.985077 99.363159 149.970367 c
93.854500 149.963028 88.820534 149.949768 85.164253 149.928741 c
83.337914 149.918228 81.845848 149.905731 80.807587 149.890900 c
80.292084 149.883545 79.869400 149.875336 79.568008 149.865891 c
79.424759 149.861389 79.269722 149.855469 79.134247 149.846527 c
79.081985 149.843094 78.947746 149.834030 78.793877 149.812897 c
78.748192 149.806641 78.519775 149.776611 78.242943 149.696625 c
78.151802 149.670319 77.732826 149.552307 77.269096 149.258392 c
77.008621 149.077515 76.415733 148.516479 76.117638 148.114105 c
75.779449 147.451813 75.526001 145.882538 75.668030 145.039047 c
82.893646 147.122498 l
83.035507 146.279617 82.782158 144.710938 82.444359 144.049255 c
82.146645 143.647461 81.554413 143.087006 81.294876 142.906738 c
80.833023 142.614014 80.417023 142.497192 80.329964 142.472046 c
80.061295 142.394440 79.845428 142.366730 79.816643 142.362793 c
79.744957 142.352936 79.691727 142.348007 79.672020 142.346252 c
79.647842 142.344086 79.632370 142.343048 79.629318 142.342850 c
79.623993 142.342499 79.635231 142.343277 79.669975 142.344757 c
79.702553 142.346161 79.746620 142.347794 79.803734 142.349579 c
80.038528 142.356949 80.408836 142.364441 80.914993 142.371674 c
81.920067 142.386017 83.386047 142.398376 85.207527 142.408859 c
88.846886 142.429810 93.867622 142.443039 99.373184 142.450378 c
110.382942 142.465057 123.311569 142.456146 130.960785 142.438293 c
130.978333 149.958267 l
h
82.893646 147.122498 m
83.006897 146.584534 83.032082 145.826355 83.012100 145.618286 c
82.997322 145.512054 82.967239 145.342407 82.954086 145.279800 c
82.929825 145.169220 82.905975 145.085770 82.899849 145.064316 c
82.890221 145.030594 82.882523 145.005646 82.879196 144.994980 c
82.875359 144.982635 82.872734 144.974564 82.872002 144.972305 c
82.871216 144.969879 82.878197 144.991119 82.898605 145.049850 c
82.937004 145.160370 82.999245 145.335754 83.087128 145.580505 c
83.261513 146.066162 83.522041 146.783661 83.860504 147.710876 c
84.536697 149.563293 85.515999 152.230896 86.721222 155.506119 c
89.131294 162.055573 92.440521 171.023163 96.025543 180.732590 c
103.194412 200.148254 111.466515 222.531326 115.847252 234.450241 c
108.788910 237.044510 l
104.410728 225.132553 96.142120 202.758926 88.971054 183.337326 c
85.386108 173.628098 82.075554 164.656921 79.663872 158.103088 c
78.458214 154.826675 77.476135 152.151520 76.796440 150.289505 c
76.456955 149.359497 76.190903 148.626862 76.009583 148.121918 c
75.919609 147.871353 75.846710 147.666306 75.795128 147.517838 c
75.770538 147.447083 75.743820 147.369171 75.720924 147.298782 c
75.711311 147.269241 75.690361 147.204376 75.668648 147.128311 c
75.659912 147.097717 75.634186 147.007080 75.608788 146.891327 c
75.595062 146.826141 75.564598 146.654388 75.549614 146.546570 c
75.529434 146.336899 75.554611 145.577637 75.668030 145.039047 c
82.893646 147.122498 l
h
f
n
Q
endstream
endobj
2 0 obj
9809
endobj
3 0 obj
<< /Type /XObject
/Length 4 0 R
/Group << /Type /Group
/S /Transparency
>>
/Subtype /Form
/Resources << >>
/BBox [ 0.000000 0.000000 512.000000 512.000000 ]
>>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 15.562500 15.437378 cm
0.000000 0.000000 0.000000 scn
0.000000 0.000031 m
0.000000 451.298706 l
303.743164 147.476624 l
311.959686 139.257233 325.283234 139.257233 333.499786 147.476624 c
341.716339 155.695953 341.716339 169.021393 333.499786 177.240784 c
29.756639 481.062866 l
480.937866 481.062866 l
480.937866 0.000031 l
0.000000 0.000031 l
h
f
n
Q
endstream
endobj
4 0 obj
420
endobj
5 0 obj
<< /XObject << /X1 1 0 R >>
/ExtGState << /E1 << /SMask << /Type /Mask
/G 3 0 R
/S /Alpha
>>
/Type /ExtGState
>> >>
>>
endobj
6 0 obj
<< /Length 7 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
/E1 gs
/X1 Do
Q
q
1.000000 0.000000 -0.000000 1.000000 177.791992 157.981934 cm
1.000000 1.000000 1.000000 scn
150.035095 13.429123 m
153.559174 9.905029 159.272858 9.905029 162.796951 13.429123 c
166.321045 16.953217 166.321045 22.666901 162.796951 26.190979 c
150.035095 13.429123 l
h
6.380932 182.607010 m
2.856840 186.131104 -2.856840 186.131104 -6.380932 182.607010 c
-9.905023 179.082916 -9.905023 173.369232 -6.380932 169.845139 c
6.380932 182.607010 l
h
162.796951 26.190979 m
6.380932 182.607010 l
-6.380932 169.845139 l
150.035095 13.429123 l
162.796951 26.190979 l
h
f
n
Q
endstream
endobj
7 0 obj
614
endobj
8 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 512.000000 512.000000 ]
/Resources 5 0 R
/Contents 6 0 R
/Parent 9 0 R
>>
endobj
9 0 obj
<< /Kids [ 8 0 R ]
/Count 1
/Type /Pages
>>
endobj
10 0 obj
<< /Pages 9 0 R
/Type /Catalog
>>
endobj
xref
0 11
0000000000 65535 f
0000000010 00000 n
0000010069 00000 n
0000010092 00000 n
0000010762 00000 n
0000010784 00000 n
0000011082 00000 n
0000011752 00000 n
0000011774 00000 n
0000011949 00000 n
0000012023 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 10 0 R
/Size 11
>>
startxref
12083
%%EOF

View File

@ -8,5 +8,8 @@
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

View File

@ -8,5 +8,8 @@
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

View File

@ -1,7 +1,7 @@
{
"images" : [
{
"filename" : "flash.pdf",
"filename" : "magnifying.pdf",
"idiom" : "universal"
}
],

View File

@ -0,0 +1,126 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 13.500000 12.074233 cm
1.000000 1.000000 1.000000 scn
0.665000 7.425767 m
0.665000 7.793036 0.367269 8.090767 0.000000 8.090767 c
-0.367269 8.090767 -0.665000 7.793036 -0.665000 7.425767 c
0.665000 7.425767 l
h
-0.665000 1.330000 m
-0.665000 0.962730 -0.367269 0.665000 0.000000 0.665000 c
0.367269 0.665000 0.665000 0.962730 0.665000 1.330000 c
-0.665000 1.330000 l
h
-0.665000 7.425767 m
-0.665000 1.330000 l
0.665000 1.330000 l
0.665000 7.425767 l
-0.665000 7.425767 l
h
f
n
Q
q
0.000000 1.000000 -1.000000 0.000000 11.782148 16.452118 cm
1.000000 1.000000 1.000000 scn
0.665000 1.330000 m
0.665000 1.697269 0.367269 1.995000 0.000000 1.995000 c
-0.367269 1.995000 -0.665000 1.697269 -0.665000 1.330000 c
0.665000 1.330000 l
h
-0.665000 -4.765767 m
-0.665000 -5.133037 -0.367269 -5.430767 0.000000 -5.430767 c
0.367269 -5.430767 0.665000 -5.133037 0.665000 -4.765767 c
-0.665000 -4.765767 l
h
-0.665000 1.330000 m
-0.665000 -4.765767 l
0.665000 -4.765767 l
0.665000 1.330000 l
-0.665000 1.330000 l
h
f
n
Q
q
1.000000 0.000000 -0.000000 1.000000 5.334991 4.588974 cm
1.000000 1.000000 1.000000 scn
1.330000 11.911035 m
1.330000 15.685901 4.390134 18.746035 8.165000 18.746035 c
11.939866 18.746035 15.000000 15.685901 15.000000 11.911035 c
15.000000 8.136168 11.939866 5.076035 8.165000 5.076035 c
4.390134 5.076035 1.330000 8.136168 1.330000 11.911035 c
h
8.165000 20.076035 m
3.655595 20.076035 0.000000 16.420439 0.000000 11.911035 c
0.000000 7.401629 3.655595 3.746035 8.165000 3.746035 c
10.120762 3.746035 11.915919 4.433660 13.321901 5.580336 c
18.578102 0.324135 l
18.902237 0.000000 19.427763 0.000000 19.751900 0.324135 c
20.076035 0.648272 20.076035 1.173798 19.751900 1.497932 c
14.495699 6.754133 l
15.642375 8.160115 16.330000 9.955273 16.330000 11.911035 c
16.330000 16.420439 12.674405 20.076035 8.165000 20.076035 c
h
f*
n
Q
endstream
endobj
3 0 obj
1911
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 30.000000 30.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000002001 00000 n
0000002024 00000 n
0000002197 00000 n
0000002271 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
2330
%%EOF

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "speaker_30.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,163 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 2.669937 5.003574 cm
1.000000 1.000000 1.000000 scn
11.351625 18.250841 m
11.322918 18.234249 11.256240 18.192322 11.141315 18.096107 c
10.916164 17.907612 10.625579 17.618145 10.185589 17.178154 c
6.845894 13.838459 l
6.818897 13.811405 l
6.713208 13.705383 6.593981 13.585779 6.453676 13.489171 c
6.241572 13.343126 6.001034 13.243492 5.747785 13.196781 c
5.580265 13.165883 5.411390 13.166149 5.261689 13.166386 c
5.261677 13.166386 l
5.223461 13.166426 l
4.830062 13.166426 l
3.846654 13.166426 3.517771 13.159296 3.268425 13.092484 c
2.519576 12.891830 1.934657 12.306912 1.734004 11.558063 c
1.667192 11.308718 1.660063 10.979834 1.660063 9.996426 c
1.660063 9.013018 1.667192 8.684134 1.734004 8.434789 c
1.934657 7.685939 2.519576 7.101020 3.268425 6.900367 c
3.517771 6.833554 3.846654 6.826426 4.830062 6.826426 c
5.223460 6.826426 l
5.261672 6.826466 l
5.411377 6.826702 5.580258 6.826969 5.747785 6.796069 c
6.001034 6.749359 6.241573 6.649724 6.453676 6.503679 c
6.593977 6.407075 6.713201 6.287476 6.818888 6.181455 c
6.818899 6.181443 l
6.845895 6.154389 l
10.185591 2.814688 l
10.625580 2.374697 10.916165 2.085230 11.141315 1.896734 c
11.256239 1.800520 11.322917 1.758595 11.351624 1.742002 c
11.381638 1.745842 11.410119 1.757639 11.434055 1.776148 c
11.442623 1.808182 11.460124 1.884975 11.473353 2.034269 c
11.499272 2.326761 11.500063 2.736921 11.500063 3.359161 c
11.500063 16.633684 l
11.500063 17.255922 11.499272 17.666082 11.473353 17.958572 c
11.460124 18.107864 11.442620 18.184664 11.434053 18.216698 c
11.410115 18.235209 11.381640 18.247004 11.351625 18.250841 c
h
11.186482 19.906570 m
11.770469 19.952532 12.341165 19.716143 12.721605 19.270702 c
13.023456 18.917282 13.095314 18.461252 13.126874 18.105099 c
13.160094 17.730204 13.160080 17.245285 13.160063 16.673033 c
13.160063 16.672995 l
13.160063 16.633684 l
13.160063 3.359161 l
13.160063 3.319851 l
13.160063 3.319817 l
13.160080 2.747561 13.160094 2.262640 13.126874 1.887745 c
13.095314 1.531590 13.023456 1.075560 12.721605 0.722139 c
12.341164 0.276699 11.770468 0.040310 11.186481 0.086271 c
10.723136 0.122738 10.349862 0.394388 10.075707 0.623911 c
9.787114 0.865520 9.444218 1.208439 9.039564 1.613119 c
9.011792 1.640892 l
5.672097 4.980594 l
5.601249 5.051442 5.562954 5.089569 5.533292 5.117202 c
5.520135 5.129457 5.513000 5.135533 5.510193 5.137844 c
5.491690 5.150188 5.470943 5.158781 5.449131 5.163136 c
5.445509 5.163486 5.436167 5.164236 5.418206 5.164872 c
5.377693 5.166306 5.323654 5.166426 5.223460 5.166426 c
4.830062 5.166426 l
4.706686 5.166393 l
3.896474 5.166075 3.327978 5.165852 2.838786 5.296929 c
1.517083 5.651079 0.484716 6.683446 0.130567 8.005149 c
-0.000511 8.494340 -0.000288 9.062836 0.000031 9.873044 c
0.000063 9.996426 l
0.000031 10.119807 l
-0.000288 10.930016 -0.000511 11.498511 0.130567 11.987702 c
0.484716 13.309405 1.517083 14.341772 2.838786 14.695921 c
3.327976 14.827000 3.896471 14.826777 4.706679 14.826458 c
4.830062 14.826426 l
5.223461 14.826426 l
5.323655 14.826426 5.377693 14.826544 5.418207 14.827979 c
5.436173 14.828615 5.445515 14.829365 5.449134 14.829716 c
5.470947 14.834070 5.491693 14.842665 5.510196 14.855009 c
5.513005 14.857321 5.520141 14.863398 5.533292 14.875648 c
5.562954 14.903282 5.601249 14.941408 5.672096 15.012257 c
9.011791 18.351952 l
9.039572 18.379732 l
9.444222 18.784409 9.787117 19.127323 10.075708 19.368931 c
10.349863 19.598454 10.723137 19.870104 11.186482 19.906570 c
h
16.659830 15.076297 m
17.030680 15.345736 17.549738 15.263525 17.819178 14.892674 c
18.853163 13.469513 19.410063 11.755547 19.410063 9.996424 c
19.410063 8.237302 18.853161 6.523335 17.819174 5.100175 c
17.549736 4.729324 17.030680 4.647114 16.659830 4.916553 c
16.288979 5.185991 16.206768 5.705048 16.476206 6.075898 c
17.304140 7.215451 17.750063 8.587859 17.750063 9.996425 c
17.750063 11.404989 17.304140 12.777397 16.476208 13.916951 c
16.206770 14.287802 16.288980 14.806858 16.659830 15.076297 c
h
21.864262 17.831598 m
21.594824 18.202450 21.075768 18.284660 20.704916 18.015221 c
20.334066 17.745783 20.251856 17.226727 20.521294 16.855877 c
21.969868 14.862084 22.750063 12.460885 22.750063 9.996424 c
22.750063 7.531962 21.969866 5.130764 20.521292 3.136972 c
20.251852 2.766121 20.334063 2.247065 20.704914 1.977627 c
21.075764 1.708187 21.594822 1.790398 21.864260 2.161249 c
23.518887 4.438646 24.410063 7.181405 24.410063 9.996424 c
24.410063 12.811441 23.518888 15.554200 21.864262 17.831598 c
h
f*
n
Q
endstream
endobj
3 0 obj
4546
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 30.000000 30.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000004636 00000 n
0000004659 00000 n
0000004832 00000 n
0000004906 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
4965
%%EOF

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "squareandarrow_30.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,96 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 5.170449 5.169796 cm
1.000000 1.000000 1.000000 scn
9.830000 21.659878 m
10.288396 21.659878 10.660000 21.288275 10.660000 20.829878 c
10.660000 9.833675 l
14.743101 13.916777 l
15.067237 14.240911 15.592763 14.240911 15.916899 13.916777 c
16.241034 13.592642 16.241034 13.067114 15.916899 12.742979 c
10.416899 7.242979 l
10.092764 6.918844 9.567236 6.918844 9.243101 7.242979 c
3.743101 12.742979 l
3.418966 13.067114 3.418966 13.592642 3.743101 13.916777 c
4.067236 14.240911 4.592763 14.240911 4.916899 13.916777 c
9.000000 9.833675 l
9.000000 20.829878 l
9.000000 21.288275 9.371604 21.659878 9.830000 21.659878 c
h
1.660000 5.830000 m
1.660000 6.288396 1.288396 6.660000 0.830000 6.660000 c
0.371604 6.660000 0.000000 6.288396 0.000000 5.830000 c
0.000000 4.580000 l
0.000000 2.050535 2.050535 0.000000 4.579999 0.000000 c
15.080000 0.000000 l
17.609465 0.000000 19.660000 2.050535 19.660000 4.580000 c
19.660000 5.830000 l
19.660000 6.288396 19.288397 6.660000 18.830000 6.660000 c
18.371603 6.660000 18.000000 6.288396 18.000000 5.830000 c
18.000000 4.580000 l
18.000000 2.967329 16.692673 1.660000 15.080000 1.660000 c
4.579999 1.660000 l
2.967328 1.660000 1.660000 2.967329 1.660000 4.580000 c
1.660000 5.830000 l
h
f*
n
Q
endstream
endobj
3 0 obj
1300
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 30.000000 30.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000001390 00000 n
0000001413 00000 n
0000001586 00000 n
0000001660 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
1719
%%EOF

View File

@ -1829,8 +1829,8 @@ public final class SharedAccountContextImpl: SharedAccountContext {
return StickerPackScreen(context: context, updatedPresentationData: updatedPresentationData, mainStickerPack: mainStickerPack, stickerPacks: stickerPacks, loadedStickerPacks: loadedStickerPacks, parentNavigationController: parentNavigationController, sendSticker: sendSticker)
}
public func makeMediaPickerScreen(context: AccountContext, completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping () -> (UIView, CGRect)?, @escaping () -> Void) -> Void, dismissed: @escaping () -> Void) -> ViewController {
return storyMediaPickerController(context: context, completion: completion, dismissed: dismissed)
public func makeMediaPickerScreen(context: AccountContext, getSourceRect: @escaping () -> CGRect, completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping () -> (UIView, CGRect)?, @escaping () -> Void) -> Void, dismissed: @escaping () -> Void) -> ViewController {
return storyMediaPickerController(context: context, getSourceRect: getSourceRect, completion: completion, dismissed: dismissed)
}
public func makeProxySettingsController(sharedContext: SharedAccountContext, account: UnauthorizedAccount) -> ViewController {

View File

@ -27,6 +27,7 @@ import LocalMediaResources
import ShareWithPeersScreen
import ImageCompression
import TextFormat
import UndoUI
private class DetailsChatPlaceholderNode: ASDisplayNode, NavigationDetailsPlaceholderNode {
private var presentationData: PresentationData
@ -254,6 +255,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
controller.view.endEditing(true)
let context = self.context
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
var presentImpl: ((ViewController) -> Void)?
var returnToCameraImpl: (() -> Void)?
@ -287,13 +289,25 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
completion: { result, resultTransition, dismissed in
let subject: Signal<MediaEditorScreen.Subject?, NoError> = result
|> map { value -> MediaEditorScreen.Subject? in
func editorPIPPosition(_ position: CameraScreen.PIPPosition) -> MediaEditorScreen.PIPPosition {
switch position {
case .topLeft:
return .topLeft
case .topRight:
return .topRight
case .bottomLeft:
return .bottomLeft
case .bottomRight:
return .bottomRight
}
}
switch value {
case .pendingImage:
return nil
case let .image(image, additionalImage):
return .image(image, PixelDimensions(image.size), additionalImage)
case let .video(path, transitionImage, dimensions):
return .video(path, transitionImage, dimensions)
case let .image(image, additionalImage, pipPosition):
return .image(image, PixelDimensions(image.size), additionalImage, editorPIPPosition(pipPosition))
case let .video(path, transitionImage, additionalPath, additionalTransitionImage, dimensions, pipPosition):
return .video(path, transitionImage, additionalPath, additionalTransitionImage, dimensions, editorPIPPosition(pipPosition))
case let .asset(asset):
return .asset(asset)
case let .draft(draft):
@ -362,6 +376,17 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
Queue.mainQueue().after(0.2) {
chatListController.updateStoryUploadProgress(nil)
}
let undoOverlayController = UndoOverlayController(presentationData: presentationData, content: .image(image: image, title: nil, text: "Story successfully uploaded", round: false, undoText: "View"), elevatedLayout: false, action: { action in
switch action {
case .undo:
break
default:
break
}
return true
})
chatListController.present(undoOverlayController, in: .current)
}
}
})
@ -420,7 +445,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
commit({})
}
}
case let .video(content, _, values, duration, dimensions, caption):
case let .video(content, image, values, duration, dimensions, caption):
let adjustments: VideoMediaResourceAdjustments
if let valuesData = try? JSONEncoder().encode(values) {
let data = MemoryBuffer(data: valuesData)
@ -451,6 +476,19 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
Queue.mainQueue().after(0.2) {
chatListController.updateStoryUploadProgress(nil)
}
if let image {
let undoOverlayController = UndoOverlayController(presentationData: presentationData, content: .image(image: image, title: nil, text: "Story successfully uploaded", round: false, undoText: "View"), elevatedLayout: false, action: { action in
switch action {
case .undo:
break
default:
break
}
return true
})
chatListController.present(undoOverlayController, in: .current)
}
}
}
})