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 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 func makeProxySettingsController(sharedContext: SharedAccountContext, account: UnauthorizedAccount) -> ViewController

View File

@ -313,6 +313,9 @@ public enum ChatTextInputStateTextAttributeType: Codable, Equatable {
case textMention(EnginePeer.Id) case textMention(EnginePeer.Id)
case textUrl(String) case textUrl(String)
case customEmoji(stickerPack: StickerPackReference?, fileId: Int64) case customEmoji(stickerPack: StickerPackReference?, fileId: Int64)
case strikethrough
case underline
case spoiler
public init(from decoder: Decoder) throws { public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: StringCodingKey.self) 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 stickerPack = try container.decodeIfPresent(StickerPackReference.self, forKey: "s")
let fileId = try container.decode(Int64.self, forKey: "f") let fileId = try container.decode(Int64.self, forKey: "f")
self = .customEmoji(stickerPack: stickerPack, fileId: fileId) self = .customEmoji(stickerPack: stickerPack, fileId: fileId)
case 6:
self = .strikethrough
case 7:
self = .underline
case 8:
self = .spoiler
default: default:
assertionFailure() assertionFailure()
self = .bold self = .bold
@ -359,6 +368,12 @@ public enum ChatTextInputStateTextAttributeType: Codable, Equatable {
try container.encode(5 as Int32, forKey: "t") try container.encode(5 as Int32, forKey: "t")
try container.encodeIfPresent(stickerPack, forKey: "s") try container.encodeIfPresent(stickerPack, forKey: "s")
try container.encode(fileId, forKey: "f") 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))) parsedAttributes.append(ChatTextInputStateTextAttribute(type: .textUrl(value.url), range: range.location ..< (range.location + range.length)))
} else if key == ChatTextInputAttributes.customEmoji, let value = value as? ChatTextInputTextCustomEmojiAttribute { } 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))) 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)) result.addAttribute(ChatTextInputAttributes.textUrl, value: ChatTextInputTextUrlAttribute(url: url), range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count))
case let .customEmoji(_, fileId): 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)) 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 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 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) let _ = self?.container.dismiss(transition: .immediate, completion: completion)
self?.animating = false self?.animating = false
self?.layer.removeAllAnimations()
}) })
} else { } else {
let positionTransition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut) let positionTransition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut)
@ -745,12 +746,12 @@ public class AttachmentController: ViewController {
let position: CGPoint let position: CGPoint
let positionY = layout.size.height - size.height - insets.bottom - 40.0 let positionY = layout.size.height - size.height - insets.bottom - 40.0
if let sourceRect = controller.getSourceRect?() { 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 { } else {
position = CGPoint(x: masterWidth - 174.0, y: positionY) 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) var containerY = floorToScreenPixels((layout.size.height - size.height) / 2.0)
if let inputHeight = layout.inputHeight, inputHeight > 88.0 { if let inputHeight = layout.inputHeight, inputHeight > 88.0 {
containerY = layout.size.height - inputHeight - size.height - 80.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") fatalError("init(coder:) has not been implemented")
} }
public var forceSourceRect = false
fileprivate var isStandalone: Bool { fileprivate var isStandalone: Bool {
return self.buttons.contains(.standalone) return self.buttons.contains(.standalone)
} }

View File

@ -37,12 +37,15 @@ final class CameraDeviceContext {
private weak var session: CameraSession? private weak var session: CameraSession?
private weak var previewView: CameraSimplePreviewView? private weak var previewView: CameraSimplePreviewView?
private let exclusive: Bool
let device = CameraDevice() let device = CameraDevice()
let input = CameraInput() let input = CameraInput()
let output = CameraOutput() let output = CameraOutput()
init(session: CameraSession) { init(session: CameraSession, exclusive: Bool) {
self.session = session self.session = session
self.exclusive = exclusive
} }
func configure(position: Camera.Position, previewView: CameraSimplePreviewView?, audio: Bool, photo: Bool, metadata: Bool) { func configure(position: Camera.Position, previewView: CameraSimplePreviewView?, audio: Bool, photo: Bool, metadata: Bool) {
@ -81,6 +84,9 @@ final class CameraDeviceContext {
} }
private var preferredMaxFrameRate: Double { private var preferredMaxFrameRate: Double {
if !self.exclusive {
return 30.0
}
switch DeviceModel.current { switch DeviceModel.current {
case .iPhone14ProMax, .iPhone13ProMax: case .iPhone14ProMax, .iPhone13ProMax:
return 60.0 return 60.0
@ -95,7 +101,7 @@ private final class CameraContext {
private let session: CameraSession private let session: CameraSession
private let mainDeviceContext: CameraDeviceContext private var mainDeviceContext: CameraDeviceContext
private var additionalDeviceContext: CameraDeviceContext? private var additionalDeviceContext: CameraDeviceContext?
private let cameraImageContext = CIContext() private let cameraImageContext = CIContext()
@ -162,7 +168,7 @@ private final class CameraContext {
self.simplePreviewView = previewView self.simplePreviewView = previewView
self.secondaryPreviewView = secondaryPreviewView self.secondaryPreviewView = secondaryPreviewView
self.mainDeviceContext = CameraDeviceContext(session: session) self.mainDeviceContext = CameraDeviceContext(session: session, exclusive: true)
self.configure { self.configure {
self.mainDeviceContext.configure(position: configuration.position, previewView: self.simplePreviewView, audio: configuration.audio, photo: configuration.photo, metadata: configuration.metadata) 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 self.modeChange = .dualCamera
if enabled { if enabled {
self.configure { 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.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 self.additionalDeviceContext?.output.processSampleBuffer = { [weak self] sampleBuffer, pixelBuffer, connection in
guard let self else { guard let self else {
return return
@ -325,9 +351,29 @@ private final class CameraContext {
} }
} else { } else {
self.configure { 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?.invalidate()
self.additionalDeviceContext = nil 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) { self.queue.after(0.5) {
@ -394,11 +440,33 @@ private final class CameraContext {
} }
public func startRecording() -> Signal<Double, NoError> { 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> { public func stopRecording() -> Signal<VideoCaptureResult, NoError> {
return self.mainDeviceContext.output.stopRecording() 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> { 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 return Signal { subscriber in
let disposable = MetaDisposable() let disposable = MetaDisposable()
self.queue.async { self.queue.async {

View File

@ -6,6 +6,28 @@ import CoreImage
import Vision import Vision
import VideoToolbox 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 struct CameraCode: Equatable {
public enum CodeType { public enum CodeType {
case qr 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> { func startRecording() -> Signal<Double, NoError> {
guard self.videoRecorder == nil else { guard self.videoRecorder == nil else {
return .complete() return .complete()
@ -288,18 +310,16 @@ final class CameraOutput: NSObject {
guard let videoSettings = self.videoOutput.recommendedVideoSettings(forVideoCodecType: codecType, assetWriterOutputFileType: .mp4) else { guard let videoSettings = self.videoOutput.recommendedVideoSettings(forVideoCodecType: codecType, assetWriterOutputFileType: .mp4) else {
return .complete() return .complete()
} }
guard let audioSettings = self.audioOutput.recommendedAudioSettingsForAssetWriter(writingTo: .mp4) else { let audioSettings = self.audioOutput.recommendedAudioSettingsForAssetWriter(writingTo: .mp4) ?? [:]
return .complete()
}
let outputFileName = NSUUID().uuidString let outputFileName = NSUUID().uuidString
let outputFilePath = NSTemporaryDirectory() + outputFileName + ".mp4" let outputFilePath = NSTemporaryDirectory() + outputFileName + ".mp4"
let outputFileURL = URL(fileURLWithPath: outputFilePath) 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 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 { if case let .success(transitionImage) = result {
self?.recordingCompletionPipe.putNext((outputFilePath, transitionImage)) self?.recordingCompletionPipe.putNext(.finished((outputFilePath, transitionImage!), nil, CACurrentMediaTime()))
} else { } 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() self.videoRecorder?.stop()
return self.recordingCompletionPipe.signal() 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 return bandingStart + (1.0 - (1.0 / ((bandedOffset * coefficient / range) + 1.0))) * range
} }
let cameraIsAlreadyOpened = self.controller?.hasStoryCameraTransition ?? false if case .compact = layout.metrics.widthClass {
if selectedIndex <= 0 && translation.x > 0.0 { let cameraIsAlreadyOpened = self.controller?.hasStoryCameraTransition ?? false
transitionFraction = 0.0 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)
}
self.controller?.storyCameraPanGestureChanged(transitionFraction: translation.x / layout.size.width) if cameraIsAlreadyOpened {
} else if translation.x <= 0.0 && cameraIsAlreadyOpened { transitionFraction = 0.0
self.controller?.storyCameraPanGestureChanged(transitionFraction: 0.0) return
} }
if cameraIsAlreadyOpened {
transitionFraction = 0.0
return
} }
if selectedIndex >= maxFilterIndex && translation.x < 0.0 { if selectedIndex >= maxFilterIndex && translation.x < 0.0 {

View File

@ -321,7 +321,7 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
if setup { if setup {
text.referenceDrawingSize = self.size text.referenceDrawingSize = self.size
text.width = floor(self.size.width * 0.9) text.width = floor(self.size.width * 0.9)
text.fontSize = 0.3 text.fontSize = 0.08
text.scale = zoomScale text.scale = zoomScale
} }
} }
@ -415,9 +415,16 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
let newEntity = entity.duplicate() let newEntity = entity.duplicate()
self.prepareNewEntity(newEntity, setup: false, relativeTo: entity) 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() 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.containerView = self
view.update() view.update()
self.addSubview(view) self.addSubview(view)
@ -516,6 +523,12 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
return nil return nil
} }
public func eachView(_ f: (DrawingEntityView) -> Void) {
for case let view as DrawingEntityView in self.subviews {
f(view)
}
}
public func play() { public func play() {
for case let view as DrawingEntityView in self.subviews { for case let view as DrawingEntityView in self.subviews {
view.play() view.play()
@ -700,15 +713,15 @@ public class DrawingEntityView: UIView {
return self.bounds 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.isVisible = true
self.applyVisibility() self.applyVisibility()
} }
override func pause() { public override func pause() {
self.isVisible = false self.isVisible = false
self.applyVisibility() self.applyVisibility()
} }
override func seek(to timestamp: Double) { public override func seek(to timestamp: Double) {
self.isVisible = false self.isVisible = false
self.isPlaying = false self.isPlaying = false

View File

@ -738,7 +738,7 @@ private final class DrawingScreenComponent: CombinedComponent {
areUnicodeEmojiEnabled: true, areUnicodeEmojiEnabled: true,
areCustomEmojiEnabled: true, areCustomEmojiEnabled: true,
chatPeerId: context.account.peerId, chatPeerId: context.account.peerId,
hasSearch: false, hasSearch: true,
forceHasPremium: true forceHasPremium: true
) )
@ -749,7 +749,7 @@ private final class DrawingScreenComponent: CombinedComponent {
stickerNamespaces: [Namespaces.ItemCollection.CloudStickerPacks], stickerNamespaces: [Namespaces.ItemCollection.CloudStickerPacks],
stickerOrderedItemListCollectionIds: [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers, Namespaces.OrderedItemList.CloudAllPremiumStickers], stickerOrderedItemListCollectionIds: [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers, Namespaces.OrderedItemList.CloudAllPremiumStickers],
chatPeerId: context.account.peerId, chatPeerId: context.account.peerId,
hasSearch: false, hasSearch: true,
hasTrending: true, hasTrending: true,
forceHasPremium: true forceHasPremium: true
) )
@ -761,7 +761,7 @@ private final class DrawingScreenComponent: CombinedComponent {
stickerNamespaces: [Namespaces.ItemCollection.CloudMaskPacks], stickerNamespaces: [Namespaces.ItemCollection.CloudMaskPacks],
stickerOrderedItemListCollectionIds: [], stickerOrderedItemListCollectionIds: [],
chatPeerId: context.account.peerId, chatPeerId: context.account.peerId,
hasSearch: false, hasSearch: true,
hasTrending: false, hasTrending: false,
forceHasPremium: true forceHasPremium: true
) )
@ -1153,8 +1153,14 @@ private final class DrawingScreenComponent: CombinedComponent {
controlsAreVisible = false 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 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 let previewBottomInset = context.availableSize.height - previewSize.height - previewTopInset
var topInset = environment.safeInsets.top + 31.0 var topInset = environment.safeInsets.top + 31.0
@ -1646,7 +1652,7 @@ private final class DrawingScreenComponent: CombinedComponent {
transition: .immediate transition: .immediate
) )
context.add(fillButton 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)) .appear(.default(scale: true))
.disappear(.default(scale: true)) .disappear(.default(scale: true))
) )
@ -1678,7 +1684,7 @@ private final class DrawingScreenComponent: CombinedComponent {
transition: .immediate transition: .immediate
) )
context.add(flipButton 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)) .appear(.default(scale: true))
.disappear(.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) .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) 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 { if component.sourceHint == .storyEditor {
doneButtonPosition.x = doneButtonPosition.x - 2.0 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) doneButtonPosition.y = floorToScreenPixels(context.availableSize.height - previewBottomInset + 3.0 + doneButton.size.height / 2.0)
} }
context.add(doneButton 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) 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 { if component.sourceHint == .storyEditor {
backButtonPosition.x = backButtonPosition.x + 2.0 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) backButtonPosition.y = floorToScreenPixels(context.availableSize.height - previewBottomInset + 3.0 + backButton.size.height / 2.0)
} }
context.add(backButton context.add(backButton

View File

@ -1,5 +1,6 @@
import Foundation import Foundation
import UIKit import UIKit
import AVFoundation
import Display import Display
import SwiftSignalKit import SwiftSignalKit
import TelegramCore import TelegramCore
@ -21,6 +22,10 @@ final class DrawingStickerEntityView: DrawingEntityView {
private let imageNode: TransformImageNode private let imageNode: TransformImageNode
private var animationNode: AnimatedStickerNode? private var animationNode: AnimatedStickerNode?
private var videoPlayer: AVPlayer?
private var videoLayer: AVPlayerLayer?
private var videoImageView: UIImageView?
private var didSetUpAnimationNode = false private var didSetUpAnimationNode = false
private let stickerFetchedDisposable = MetaDisposable() private let stickerFetchedDisposable = MetaDisposable()
private let cachedDisposable = 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 { private var dimensions: CGSize {
switch self.stickerEntity.content { switch self.stickerEntity.content {
case let .file(file): case let .file(file):
return file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0) return file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0)
case let .image(image): case let .image(image):
return image.size 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 return context
})) }))
self.setNeedsLayout() 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() { override func play() {
self.isVisible = true self.isVisible = true
self.applyVisibility() 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() { override func pause() {
self.isVisible = false self.isVisible = false
self.applyVisibility() self.applyVisibility()
if let player = self.videoPlayer {
player.pause()
}
} }
override func seek(to timestamp: Double) { override func seek(to timestamp: Double) {
self.isVisible = false self.isVisible = false
self.isPlaying = false self.isPlaying = false
self.animationNode?.seekTo(.timestamp(timestamp)) 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() { override func resetToStart() {
@ -184,10 +245,11 @@ final class DrawingStickerEntityView: DrawingEntityView {
let boundingSize = CGSize(width: sideSize, height: sideSize) let boundingSize = CGSize(width: sideSize, height: sideSize)
let imageSize = self.dimensions.aspectFitted(boundingSize) 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.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 { 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) animationNode.updateLayout(size: imageSize)
if !self.didApplyVisibility { if !self.didApplyVisibility {
@ -195,6 +257,16 @@ final class DrawingStickerEntityView: DrawingEntityView {
self.applyVisibility() 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) self.update(animated: false)
} }
} }
@ -226,13 +298,19 @@ final class DrawingStickerEntityView: DrawingEntityView {
UIView.animate(withDuration: 0.25, animations: { UIView.animate(withDuration: 0.25, animations: {
self.imageNode.transform = animationTargetTransform self.imageNode.transform = animationTargetTransform
self.animationNode?.transform = animationTargetTransform self.animationNode?.transform = animationTargetTransform
self.videoLayer?.transform = animationTargetTransform
}, completion: { finished in }, completion: { finished in
self.imageNode.transform = staticTransform self.imageNode.transform = staticTransform
self.animationNode?.transform = staticTransform self.animationNode?.transform = staticTransform
self.videoLayer?.transform = staticTransform
}) })
} else { } else {
CATransaction.begin()
CATransaction.setDisableActions(true)
self.imageNode.transform = staticTransform self.imageNode.transform = staticTransform
self.animationNode?.transform = staticTransform self.animationNode?.transform = staticTransform
self.videoLayer?.transform = staticTransform
CATransaction.commit()
} }
super.update(animated: animated) super.update(animated: animated)

View File

@ -35,6 +35,7 @@ public struct StickerPickerInputData: Equatable {
private final class StickerSelectionComponent: Component { private final class StickerSelectionComponent: Component {
typealias EnvironmentType = Empty typealias EnvironmentType = Empty
let context: AccountContext
let theme: PresentationTheme let theme: PresentationTheme
let strings: PresentationStrings let strings: PresentationStrings
let deviceMetrics: DeviceMetrics let deviceMetrics: DeviceMetrics
@ -44,6 +45,7 @@ private final class StickerSelectionComponent: Component {
let separatorColor: UIColor let separatorColor: UIColor
init( init(
context: AccountContext,
theme: PresentationTheme, theme: PresentationTheme,
strings: PresentationStrings, strings: PresentationStrings,
deviceMetrics: DeviceMetrics, deviceMetrics: DeviceMetrics,
@ -52,6 +54,7 @@ private final class StickerSelectionComponent: Component {
backgroundColor: UIColor, backgroundColor: UIColor,
separatorColor: UIColor separatorColor: UIColor
) { ) {
self.context = context
self.theme = theme self.theme = theme
self.strings = strings self.strings = strings
self.deviceMetrics = deviceMetrics self.deviceMetrics = deviceMetrics
@ -129,6 +132,7 @@ private final class StickerSelectionComponent: Component {
let topPanelHeight: CGFloat = 42.0 let topPanelHeight: CGFloat = 42.0
//let context = component.context
let keyboardSize = self.keyboardView.update( let keyboardSize = self.keyboardView.update(
transition: transition.withUserData(EmojiPagerContentComponent.SynchronousLoadBehavior(isDisabled: true)), transition: transition.withUserData(EmojiPagerContentComponent.SynchronousLoadBehavior(isDisabled: true)),
component: AnyComponent(EntityKeyboardComponent( component: AnyComponent(EntityKeyboardComponent(
@ -153,7 +157,44 @@ private final class StickerSelectionComponent: Component {
switchToTextInput: {}, switchToTextInput: {},
switchToGifSubject: { _ in }, switchToGifSubject: { _ in },
reorderItems: { _, _ 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 }, contentIdUpdated: { _ in },
deviceMetrics: component.deviceMetrics, deviceMetrics: component.deviceMetrics,
hiddenInputHeight: 0.0, hiddenInputHeight: 0.0,
@ -225,6 +266,39 @@ public class StickerPickerScreen: ViewController {
fileprivate var temporaryDismiss = false 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) { init(context: AccountContext, controller: StickerPickerScreen, theme: PresentationTheme) {
self.presentationData = context.sharedContext.currentPresentationData.with { $0 } self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.controller = controller self.controller = controller
@ -249,8 +323,52 @@ public class StickerPickerScreen: ViewController {
self.wrappingView.addSubview(self.containerView) self.wrappingView.addSubview(self.containerView)
self.containerView.addSubview(self.hostView) 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 { 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) strongSelf.updateContent(inputData)
} }
})) }))
@ -258,6 +376,8 @@ public class StickerPickerScreen: ViewController {
deinit { deinit {
self.contentDisposable.dispose() self.contentDisposable.dispose()
self.emojiSearchDisposable.dispose()
self.stickerSearchDisposable.dispose()
} }
func updateContent(_ content: StickerPickerInputData) { func updateContent(_ content: StickerPickerInputData) {
@ -363,9 +483,224 @@ public class StickerPickerScreen: ViewController {
navigationController: { [weak self] in navigationController: { [weak self] in
return self?.controller?.navigationController as? NavigationController 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 updateScrollingToItemGroup: { [weak self] in
self?.update(isExpanded: true, transition: .animated(duration: 0.4, curve: .spring)) self?.update(isExpanded: true, transition: .animated(duration: 0.4, curve: .spring))
@ -570,7 +905,95 @@ public class StickerPickerScreen: ViewController {
}, },
requestUpdate: { _ in 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 updateScrollingToItemGroup: { [weak self] in
self?.update(isExpanded: true, transition: .animated(duration: 0.4, curve: .spring)) 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) { func containerLayoutUpdated(layout: ContainerViewLayout, navigationHeight: CGFloat, transition: Transition) {
guard let controller = self.controller else {
return
}
self.currentLayout = (layout, navigationHeight) 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)) 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) 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) self.controller?.updateModalStyleOverlayTransitionFactor(modalProgress, transition: transition.containedViewLayoutTransition)
let clipFrame: CGRect let clipFrame: CGRect
@ -761,6 +1190,7 @@ public class StickerPickerScreen: ViewController {
transition: stickersTransition, transition: stickersTransition,
component: AnyComponent( component: AnyComponent(
StickerSelectionComponent( StickerSelectionComponent(
context: controller.context,
theme: self.theme, theme: self.theme,
strings: self.presentationData.strings, strings: self.presentationData.strings,
deviceMetrics: layout.deviceMetrics, deviceMetrics: layout.deviceMetrics,

View File

@ -146,6 +146,8 @@ private class LegacyPaintStickerEntity: LegacyPaintEntity {
case let .image(image): case let .image(image):
self.file = nil self.file = nil
self.imagePromise.set(.single(image)) 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) let transaction = MediaPickerGridTransaction(previousList: previousEntries, list: entries, context: controller.context, interaction: interaction, theme: self.presentationData.theme, scrollToItem: scrollToItem)
self.enqueueTransaction(transaction) self.enqueueTransaction(transaction)
if !self.didSetReady {
updateLayout = true
}
if updateLayout, let (layout, navigationBarHeight) = self.validLayout { if updateLayout, let (layout, navigationBarHeight) = self.validLayout {
self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: previousState == nil ? .immediate : .animated(duration: 0.2, curve: .easeInOut)) 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( public func storyMediaPickerController(
context: AccountContext, context: AccountContext,
getSourceRect: @escaping () -> CGRect,
completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping () -> (UIView, CGRect)?, @escaping () -> Void) -> Void, completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping () -> (UIView, CGRect)?, @escaping () -> Void) -> Void,
dismissed: @escaping () -> Void dismissed: @escaping () -> Void
) -> ViewController { ) -> 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: { let controller = AttachmentController(context: context, updatedPresentationData: updatedPresentationData, chatLocation: nil, buttons: [.standalone], initialButton: .standalone, fromMenu: false, hasTextInput: false, makeEntityInputView: {
return nil return nil
}) })
controller.forceSourceRect = true
controller.getSourceRect = getSourceRect
controller.requestController = { _, present in 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) 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 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 UIKit
import ComponentFlow import ComponentFlow
final class CameraButton: Component { public final class CameraButton: Component {
let content: AnyComponentWithIdentity<Empty> let content: AnyComponentWithIdentity<Empty>
let minSize: CGSize? let minSize: CGSize?
let tag: AnyObject? let tag: AnyObject?
let isEnabled: Bool let isEnabled: Bool
let action: () -> Void let action: () -> Void
init( public init(
content: AnyComponentWithIdentity<Empty>, content: AnyComponentWithIdentity<Empty>,
minSize: CGSize? = nil, minSize: CGSize? = nil,
tag: AnyObject? = nil, tag: AnyObject? = nil,
@ -23,7 +23,7 @@ final class CameraButton: Component {
self.action = action self.action = action
} }
func tagged(_ tag: AnyObject) -> CameraButton { public func tagged(_ tag: AnyObject) -> CameraButton {
return CameraButton( return CameraButton(
content: self.content, content: self.content,
minSize: self.minSize, 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 { if lhs.content != rhs.content {
return false return false
} }
@ -49,8 +49,8 @@ final class CameraButton: Component {
return true return true
} }
final class View: UIButton, ComponentTaggedView { public final class View: UIButton, ComponentTaggedView {
private var contentView: ComponentHostView<Empty> public var contentView: ComponentHostView<Empty>
private var component: CameraButton? private var component: CameraButton?
private var currentIsHighlighted: Bool = false { private var currentIsHighlighted: Bool = false {
@ -74,7 +74,7 @@ final class CameraButton: Component {
transition.setScale(view: self, scale: scale) transition.setScale(view: self, scale: scale)
} }
override init(frame: CGRect) { public override init(frame: CGRect) {
self.contentView = ComponentHostView<Empty>() self.contentView = ComponentHostView<Empty>()
self.contentView.isUserInteractionEnabled = false self.contentView.isUserInteractionEnabled = false
self.contentView.layer.allowsGroupOpacity = true self.contentView.layer.allowsGroupOpacity = true
@ -104,19 +104,19 @@ final class CameraButton: Component {
self.component?.action() 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 self.currentIsHighlighted = true
return super.beginTracking(touch, with: event) 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 self.currentIsHighlighted = false
super.endTracking(touch, with: event) super.endTracking(touch, with: event)
} }
override func cancelTracking(with event: UIEvent?) { public override func cancelTracking(with event: UIEvent?) {
self.currentIsHighlighted = false self.currentIsHighlighted = false
super.cancelTracking(with: event) super.cancelTracking(with: event)
@ -155,11 +155,11 @@ final class CameraButton: Component {
} }
} }
func makeView() -> View { public func makeView() -> View {
return View(frame: CGRect()) 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.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
} }
} }

View File

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

View File

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

View File

@ -18,6 +18,7 @@ import LottieAnimationComponent
import TooltipUI import TooltipUI
import MediaEditor import MediaEditor
import BundleIconComponent import BundleIconComponent
import CameraButtonComponent
let videoRedColor = UIColor(rgb: 0xff3b30) let videoRedColor = UIColor(rgb: 0xff3b30)
@ -86,6 +87,8 @@ private final class CameraScreenComponent: CombinedComponent {
let camera: Camera let camera: Camera
let updateState: ActionSlot<CameraState> let updateState: ActionSlot<CameraState>
let hasAppeared: Bool let hasAppeared: Bool
let panelWidth: CGFloat
let flipAnimationAction: ActionSlot<Void>
let present: (ViewController) -> Void let present: (ViewController) -> Void
let push: (ViewController) -> Void let push: (ViewController) -> Void
let completion: ActionSlot<Signal<CameraScreen.Result, NoError>> let completion: ActionSlot<Signal<CameraScreen.Result, NoError>>
@ -95,6 +98,8 @@ private final class CameraScreenComponent: CombinedComponent {
camera: Camera, camera: Camera,
updateState: ActionSlot<CameraState>, updateState: ActionSlot<CameraState>,
hasAppeared: Bool, hasAppeared: Bool,
panelWidth: CGFloat,
flipAnimationAction: ActionSlot<Void>,
present: @escaping (ViewController) -> Void, present: @escaping (ViewController) -> Void,
push: @escaping (ViewController) -> Void, push: @escaping (ViewController) -> Void,
completion: ActionSlot<Signal<CameraScreen.Result, NoError>> completion: ActionSlot<Signal<CameraScreen.Result, NoError>>
@ -103,6 +108,8 @@ private final class CameraScreenComponent: CombinedComponent {
self.camera = camera self.camera = camera
self.updateState = updateState self.updateState = updateState
self.hasAppeared = hasAppeared self.hasAppeared = hasAppeared
self.panelWidth = panelWidth
self.flipAnimationAction = flipAnimationAction
self.present = present self.present = present
self.push = push self.push = push
self.completion = completion self.completion = completion
@ -115,6 +122,9 @@ private final class CameraScreenComponent: CombinedComponent {
if lhs.hasAppeared != rhs.hasAppeared { if lhs.hasAppeared != rhs.hasAppeared {
return false return false
} }
if lhs.panelWidth != rhs.panelWidth {
return false
}
return true return true
} }
@ -186,7 +196,7 @@ private final class CameraScreenComponent: CombinedComponent {
} }
}) })
Queue.mainQueue().async { Queue.concurrentDefaultQueue().async {
self.setupRecentAssetSubscription() self.setupRecentAssetSubscription()
} }
} }
@ -229,9 +239,18 @@ private final class CameraScreenComponent: CombinedComponent {
self.hapticFeedback.impact(.light) 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.camera.togglePosition()
self.hapticFeedback.impact(.light) self.hapticFeedback.impact(.light)
action.invoke(Void())
} }
func toggleDualCamera() { func toggleDualCamera() {
@ -256,7 +275,7 @@ private final class CameraScreenComponent: CombinedComponent {
case .began: case .began:
return .single(.pendingImage) return .single(.pendingImage)
case let .finished(mainImage, additionalImage, _): case let .finished(mainImage, additionalImage, _):
return .single(.image(mainImage, additionalImage)) return .single(.image(mainImage, additionalImage, .bottomRight))
case .failed: case .failed:
return .complete() return .complete()
} }
@ -282,9 +301,9 @@ private final class CameraScreenComponent: CombinedComponent {
func stopVideoRecording() { func stopVideoRecording() {
self.cameraState = self.cameraState.updatedRecording(.none).updatedDuration(0.0) self.cameraState = self.cameraState.updatedRecording(.none).updatedDuration(0.0)
self.resultDisposable.set((self.camera.stopRecording() self.resultDisposable.set((self.camera.stopRecording()
|> deliverOnMainQueue).start(next: { [weak self] pathAndTransitionImage in |> deliverOnMainQueue).start(next: { [weak self] result in
if let self, let (path, transitionImage) = pathAndTransitionImage { if let self, case let .finished(mainResult, additionalResult, _) = result {
self.completion.invoke(.single(.video(path, transitionImage, PixelDimensions(width: 1080, height: 1920)))) self.completion.invoke(.single(.video(mainResult.0, mainResult.1, additionalResult?.0, additionalResult?.1, PixelDimensions(width: 1080, height: 1920), .bottomRight)))
} }
})) }))
self.isTransitioning = true self.isTransitioning = true
@ -316,15 +335,13 @@ private final class CameraScreenComponent: CombinedComponent {
let zoomControl = Child(ZoomComponent.self) let zoomControl = Child(ZoomComponent.self)
let flashButton = Child(CameraButton.self) let flashButton = Child(CameraButton.self)
let flipButton = Child(CameraButton.self) let flipButton = Child(CameraButton.self)
// let dualButton = Child(CameraButton.self) let dualButton = Child(CameraButton.self)
let modeControl = Child(ModeComponent.self) let modeControl = Child(ModeComponent.self)
let hintLabel = Child(HintLabelComponent.self) let hintLabel = Child(HintLabelComponent.self)
let timeBackground = Child(RoundedRectangle.self) let timeBackground = Child(RoundedRectangle.self)
let timeLabel = Child(MultilineTextComponent.self) let timeLabel = Child(MultilineTextComponent.self)
let flipAnimationAction = ActionSlot<Void>()
return { context in return { context in
let environment = context.environment[ViewControllerComponentContainer.Environment.self].value let environment = context.environment[ViewControllerComponentContainer.Environment.self].value
let component = context.component let component = context.component
@ -339,6 +356,9 @@ private final class CameraScreenComponent: CombinedComponent {
isTablet = false isTablet = false
} }
let smallPanelWidth = min(component.panelWidth, 88.0)
let panelWidth = min(component.panelWidth, 185.0)
let topControlInset: CGFloat = 20.0 let topControlInset: CGFloat = 20.0
if case .none = state.cameraState.recording, !state.isTransitioning { if case .none = state.cameraState.recording, !state.isTransitioning {
let cancelButton = cancelButton.update( let cancelButton = cancelButton.update(
@ -363,7 +383,7 @@ private final class CameraScreenComponent: CombinedComponent {
transition: .immediate transition: .immediate
) )
context.add(cancelButton 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)) .appear(.default(scale: true))
.disappear(.default(scale: true)) .disappear(.default(scale: true))
) )
@ -423,36 +443,36 @@ private final class CameraScreenComponent: CombinedComponent {
transition: .immediate transition: .immediate
) )
context.add(flashButton 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)) .appear(.default(scale: true))
.disappear(.default(scale: true)) .disappear(.default(scale: true))
) )
// if #available(iOS 13.0, *) { if #available(iOS 13.0, *), !isTablet && !"".isEmpty {
// let dualButton = dualButton.update( let dualButton = dualButton.update(
// component: CameraButton( component: CameraButton(
// content: AnyComponentWithIdentity( content: AnyComponentWithIdentity(
// id: "dual", id: "dual",
// component: AnyComponent( component: AnyComponent(
// DualIconComponent(isSelected: state.cameraState.isDualCamEnabled) DualIconComponent(isSelected: state.cameraState.isDualCamEnabled)
// ) )
// ), ),
// action: { [weak state] in action: { [weak state] in
// guard let state else { guard let state else {
// return return
// } }
// state.toggleDualCamera() state.toggleDualCamera()
// } }
// ).tagged(dualButtonTag), ).tagged(dualButtonTag),
// availableSize: CGSize(width: 40.0, height: 40.0), availableSize: CGSize(width: 40.0, height: 40.0),
// transition: .immediate transition: .immediate
// ) )
// context.add(dualButton context.add(dualButton
// .position(CGPoint(x: availableSize.width / 2.0, y: environment.safeInsets.top + topControlInset + dualButton.size.height / 2.0)) .position(CGPoint(x: availableSize.width / 2.0, y: environment.safeInsets.top + topControlInset + dualButton.size.height / 2.0))
// .appear(.default(scale: true)) .appear(.default(scale: true))
// .disappear(.default(scale: true)) .disappear(.default(scale: true))
// ) )
// } }
} }
if case .holding = state.cameraState.recording { 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( let captureControls = captureControls.update(
component: CaptureControlsComponent( component: CaptureControlsComponent(
isTablet: isTablet, isTablet: isTablet,
hasAppeared: component.hasAppeared,
shutterState: shutterState, shutterState: shutterState,
lastGalleryAsset: state.lastGalleryAsset, lastGalleryAsset: state.lastGalleryAsset,
tag: captureControlsTag, tag: captureControlsTag,
@ -537,7 +565,7 @@ private final class CameraScreenComponent: CombinedComponent {
guard let state else { guard let state else {
return return
} }
state.togglePosition() state.togglePosition(flipAnimationAction)
}, },
galleryTapped: { galleryTapped: {
guard let controller = environment.controller() as? CameraScreen else { guard let controller = environment.controller() as? CameraScreen else {
@ -550,45 +578,50 @@ private final class CameraScreenComponent: CombinedComponent {
}, },
zoomUpdated: { fraction in zoomUpdated: { fraction in
state.updateZoom(fraction: fraction) state.updateZoom(fraction: fraction)
} },
flipAnimationAction: flipAnimationAction
), ),
availableSize: availableSize, availableSize: captureControlsAvailableSize,
transition: context.transition 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 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 { if isTablet {
let flipButton = flipButton.update( let flipButton = flipButton.update(
component: CameraButton( component: CameraButton(
content: AnyComponentWithIdentity( content: AnyComponentWithIdentity(
id: "flip", id: "flip",
component: AnyComponent( component: AnyComponent(
FlipButtonContentComponent(action: flipAnimationAction) FlipButtonContentComponent(
action: flipAnimationAction,
maskFrame: .zero
)
) )
), ),
minSize: CGSize(width: 44.0, height: 44.0), minSize: CGSize(width: 44.0, height: 44.0),
action: { action: {
// let currentTimestamp = CACurrentMediaTime() state.togglePosition(flipAnimationAction)
// if let lastFlipTimestamp = self.lastFlipTimestamp, currentTimestamp - lastFlipTimestamp < 1.3 {
// return
// }
// self.lastFlipTimestamp = currentTimestamp
state.togglePosition()
flipAnimationAction.invoke(Void())
} }
), ),
availableSize: availableSize, availableSize: availableSize,
transition: context.transition transition: context.transition
) )
context.add(flipButton 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 var isVideoRecording = false
if case .video = state.cameraState.mode { if case .video = state.cameraState.mode, isTablet {
isVideoRecording = true isVideoRecording = true
} else if state.cameraState.recording != .none { } else if state.cameraState.recording != .none {
isVideoRecording = true isVideoRecording = true
@ -607,6 +640,13 @@ private final class CameraScreenComponent: CombinedComponent {
transition: context.transition 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 { if state.cameraState.recording != .none {
let timeBackground = timeBackground.update( let timeBackground = timeBackground.update(
component: RoundedRectangle(color: videoRedColor, cornerRadius: 4.0), component: RoundedRectangle(color: videoRedColor, cornerRadius: 4.0),
@ -614,19 +654,19 @@ private final class CameraScreenComponent: CombinedComponent {
transition: context.transition transition: context.transition
) )
context.add(timeBackground context.add(timeBackground
.position(CGPoint(x: availableSize.width / 2.0, y: environment.safeInsets.top + 40.0)) .position(timePosition)
.appear(.default(alpha: true)) .appear(.default(alpha: true))
.disappear(.default(alpha: true)) .disappear(.default(alpha: true))
) )
} }
context.add(timeLabel context.add(timeLabel
.position(CGPoint(x: availableSize.width / 2.0, y: environment.safeInsets.top + 40.0)) .position(timePosition)
.appear(.default(alpha: true)) .appear(.default(alpha: true))
.disappear(.default(alpha: true)) .disappear(.default(alpha: true))
) )
if case .holding = state.cameraState.recording { if case .holding = state.cameraState.recording, !isTablet {
let hintText: String? let hintText: String?
switch state.swipeHint { switch state.swipeHint {
case .none: case .none:
@ -656,8 +696,15 @@ private final class CameraScreenComponent: CombinedComponent {
} }
if case .none = state.cameraState.recording, !state.isTransitioning { 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( let modeControl = modeControl.update(
component: ModeComponent( component: ModeComponent(
isTablet: isTablet,
availableModes: [.photo, .video], availableModes: [.photo, .video],
currentMode: state.cameraState.mode, currentMode: state.cameraState.mode,
updatedMode: { [weak state] mode in updatedMode: { [weak state] mode in
@ -667,12 +714,18 @@ private final class CameraScreenComponent: CombinedComponent {
}, },
tag: modeControlTag tag: modeControlTag
), ),
availableSize: availableSize, availableSize: availableModeControlSize,
transition: context.transition 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 context.add(modeControl
.clipsToBounds(true) .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)) .appear(.default(alpha: true))
.disappear(.default(alpha: true)) .disappear(.default(alpha: true))
) )
@ -734,12 +787,30 @@ public class CameraScreen: ViewController {
case instantVideo case instantVideo
} }
public enum PIPPosition {
case topLeft
case topRight
case bottomLeft
case bottomRight
}
public enum Result { public enum Result {
case pendingImage case pendingImage
case image(UIImage, UIImage?) case image(UIImage, UIImage?, CameraScreen.PIPPosition)
case video(String, UIImage?, PixelDimensions) case video(String, UIImage?, String?, UIImage?, PixelDimensions, CameraScreen.PIPPosition)
case asset(PHAsset) case asset(PHAsset)
case draft(MediaEditorDraft) 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 { public final class TransitionIn {
@ -846,6 +917,10 @@ public class CameraScreen: ViewController {
fileprivate var previewBlurPromise = ValuePromise<Bool>(false) fileprivate var previewBlurPromise = ValuePromise<Bool>(false)
private let flipAnimationAction = ActionSlot<Void>()
private var pipPosition: PIPPosition = .bottomRight
init(controller: CameraScreen) { init(controller: CameraScreen) {
self.controller = controller self.controller = controller
self.context = controller.context self.context = controller.context
@ -994,9 +1069,13 @@ public class CameraScreen: ViewController {
self.completion.connect { [weak self] result in self.completion.connect { [weak self] result in
if let self { if let self {
let pipPosition = self.pipPosition
self.animateOutToEditor() self.animateOutToEditor()
self.controller?.completion( self.controller?.completion(
result result
|> map { result in
return result.withPIPPosition(pipPosition)
}
|> beforeNext { [weak self] value in |> beforeNext { [weak self] value in
guard let self else { guard let self else {
return return
@ -1070,6 +1149,9 @@ public class CameraScreen: ViewController {
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:))) let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:)))
self.effectivePreviewView.addGestureRecognizer(tapGestureRecognizer) 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.focus(at: CGPoint(x: 0.5, y: 0.5), autoFocus: true)
self.camera.startCapture() self.camera.startCapture()
} }
@ -1128,7 +1210,30 @@ public class CameraScreen: ViewController {
self.camera.focus(at: point, autoFocus: false) 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() { func animateIn() {
self.transitionDimView.alpha = 0.0
self.backgroundView.alpha = 0.0 self.backgroundView.alpha = 0.0
UIView.animate(withDuration: 0.4, animations: { UIView.animate(withDuration: 0.4, animations: {
self.backgroundView.alpha = 1.0 self.backgroundView.alpha = 1.0
@ -1185,6 +1290,8 @@ public class CameraScreen: ViewController {
view.layer.animatePosition(from: view.center, to: destinationLocalFrame.center, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) 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) 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.componentHost.view?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
@ -1288,7 +1395,7 @@ public class CameraScreen: ViewController {
} }
func updateModalTransitionFactor(_ value: CGFloat, transition: ContainedViewLayoutTransition) { func updateModalTransitionFactor(_ value: CGFloat, transition: ContainedViewLayoutTransition) {
guard let layout = self.validLayout else { guard let layout = self.validLayout, case .compact = layout.metrics.widthClass else {
return return
} }
@ -1337,7 +1444,11 @@ public class CameraScreen: ViewController {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let result = super.hitTest(point, with: event) let result = super.hitTest(point, with: event)
if result == self.componentHost.view { 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 return result
} }
@ -1379,6 +1490,19 @@ public class CameraScreen: ViewController {
let topInset: CGFloat = (layout.statusBarHeight ?? 0.0) + 12.0 let topInset: CGFloat = (layout.statusBarHeight ?? 0.0) + 12.0
let bottomInset = layout.size.height - previewSize.height - topInset 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( let environment = ViewControllerComponentContainer.Environment(
statusBarHeight: layout.statusBarHeight ?? 0.0, statusBarHeight: layout.statusBarHeight ?? 0.0,
navigationHeight: 0.0, navigationHeight: 0.0,
@ -1408,7 +1532,7 @@ public class CameraScreen: ViewController {
self.hasAppeared = hasAppeared self.hasAppeared = hasAppeared
transition = transition.withUserData(CameraScreenTransition.finishedAnimateIn) transition = transition.withUserData(CameraScreenTransition.finishedAnimateIn)
self.presentDualCameraTooltip() // self.presentDualCameraTooltip()
} }
let componentSize = self.componentHost.update( let componentSize = self.componentHost.update(
@ -1419,6 +1543,8 @@ public class CameraScreen: ViewController {
camera: self.camera, camera: self.camera,
updateState: self.updateState, updateState: self.updateState,
hasAppeared: self.hasAppeared, hasAppeared: self.hasAppeared,
panelWidth: panelWidth,
flipAnimationAction: self.flipAnimationAction,
present: { [weak self] c in present: { [weak self] c in
self?.controller?.present(c, in: .window(.root)) 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)) 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) transition.setFrame(view: self.previewContainerView, frame: previewFrame)
self.currentPreviewView.layer.cornerRadius = 0.0 self.currentPreviewView.layer.cornerRadius = 0.0
transition.setFrame(view: self.currentPreviewView, frame: CGRect(origin: .zero, size: previewFrame.size)) 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 { if let additionalPreviewView = self.currentAdditionalPreviewView {
additionalPreviewView.layer.cornerRadius = 80.0 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.setPosition(view: additionalPreviewView, position: additionalPreviewFrame.center)
transition.setBounds(view: additionalPreviewView, bounds: CGRect(origin: .zero, size: additionalPreviewFrame.size)) 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.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))) 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.navigationPresentation = .flatModal
self.requestAudioSession() self.requestAudioSession()
if #available(iOS 13.0, *) {
try? AVAudioSession.sharedInstance().setAllowHapticsAndSystemSoundsDuringRecording(true)
}
} }
required public init(coder: NSCoder) { required public init(coder: NSCoder) {
@ -1571,6 +1724,9 @@ public class CameraScreen: ViewController {
deinit { deinit {
self.audioSessionDisposable?.dispose() self.audioSessionDisposable?.dispose()
if #available(iOS 13.0, *) {
try? AVAudioSession.sharedInstance().setAllowHapticsAndSystemSoundsDuringRecording(false)
}
} }
override public func loadDisplayNode() { override public func loadDisplayNode() {
@ -1611,7 +1767,17 @@ public class CameraScreen: ViewController {
if let current = self.galleryController { if let current = self.galleryController {
controller = current controller = current
} else { } 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 { if let self {
stopCameraCapture() stopCameraCapture()
@ -1665,15 +1831,21 @@ public class CameraScreen: ViewController {
self.node.camera.stopCapture(invalidate: true) self.node.camera.stopCapture(invalidate: true)
self.isDismissed = true self.isDismissed = true
if animated { if animated {
self.statusBar.updateStatusBarStyle(.Ignore, animated: true) if let layout = self.validLayout, case .regular = layout.metrics.widthClass {
if !interactive { self.node.animateOut(completion: {
if let navigationController = self.navigationController as? NavigationController { self.dismiss(animated: false)
navigationController.updateRootContainerTransitionOffset(self.node.frame.width, transition: .immediate) })
} 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 { } else {
self.dismiss(animated: false) self.dismiss(animated: false)
} }
@ -1694,6 +1866,9 @@ public class CameraScreen: ViewController {
} }
public func updateTransitionProgress(_ transitionFraction: CGFloat, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void = {}) { 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) 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.backgroundView.layer, transform: CGAffineTransform(translationX: offsetX, y: 0.0))
transition.updateTransform(layer: self.node.containerView.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) { public func completeWithTransitionProgress(_ transitionFraction: CGFloat, velocity: CGFloat, dismissing: Bool) {
if let layout = self.validLayout, case .regular = layout.metrics.widthClass {
return
}
if dismissing { if dismissing {
if transitionFraction < 0.7 || velocity < -1000.0 { if transitionFraction < 0.7 || velocity < -1000.0 {
self.statusBar.updateStatusBarStyle(.Ignore, animated: true) 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) 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) private let buttonSize = CGSize(width: 55.0, height: 44.0)
final class ModeComponent: Component { final class ModeComponent: Component {
let isTablet: Bool
let availableModes: [CameraMode] let availableModes: [CameraMode]
let currentMode: CameraMode let currentMode: CameraMode
let updatedMode: (CameraMode) -> Void let updatedMode: (CameraMode) -> Void
let tag: AnyObject? let tag: AnyObject?
init( init(
isTablet: Bool,
availableModes: [CameraMode], availableModes: [CameraMode],
currentMode: CameraMode, currentMode: CameraMode,
updatedMode: @escaping (CameraMode) -> Void, updatedMode: @escaping (CameraMode) -> Void,
tag: AnyObject? tag: AnyObject?
) { ) {
self.isTablet = isTablet
self.availableModes = availableModes self.availableModes = availableModes
self.currentMode = currentMode self.currentMode = currentMode
self.updatedMode = updatedMode self.updatedMode = updatedMode
@ -36,6 +39,9 @@ final class ModeComponent: Component {
} }
static func ==(lhs: ModeComponent, rhs: ModeComponent) -> Bool { static func ==(lhs: ModeComponent, rhs: ModeComponent) -> Bool {
if lhs.isTablet != rhs.isTablet {
return false
}
if lhs.availableModes != rhs.availableModes { if lhs.availableModes != rhs.availableModes {
return false return false
} }
@ -115,13 +121,15 @@ final class ModeComponent: Component {
func update(component: ModeComponent, availableSize: CGSize, transition: Transition) -> CGSize { func update(component: ModeComponent, availableSize: CGSize, transition: Transition) -> CGSize {
self.component = component self.component = component
let isTablet = component.isTablet
let updatedMode = component.updatedMode let updatedMode = component.updatedMode
let spacing: CGFloat = 14.0 let spacing: CGFloat = isTablet ? 9.0 : 14.0
var i = 0 var i = 0
var itemFrame = CGRect(origin: .zero, size: buttonSize) var itemFrame = CGRect(origin: .zero, size: buttonSize)
var selectedCenter = itemFrame.minX var selectedCenter = itemFrame.minX
for mode in component.availableModes { for mode in component.availableModes {
let itemView: ItemView let itemView: ItemView
if self.itemViews.count == i { if self.itemViews.count == i {
@ -137,20 +145,37 @@ final class ModeComponent: Component {
itemView.update(value: mode.title, selected: mode == component.currentMode) itemView.update(value: mode.title, selected: mode == component.currentMode)
itemView.bounds = CGRect(origin: .zero, size: itemFrame.size) itemView.bounds = CGRect(origin: .zero, size: itemFrame.size)
itemView.center = CGPoint(x: itemFrame.midX, y: itemFrame.midY)
if mode == component.currentMode { if isTablet {
selectedCenter = itemFrame.midX 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 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) let totalSize: CGSize
transition.setFrame(view: self.containerView, frame: CGRect(origin: CGPoint(x: availableSize.width / 2.0 - selectedCenter, y: 0.0), size: totalSize)) 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 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 { private class ShutterBlobLayer: MetalImageLayer {
@ -214,12 +208,14 @@ final class ShutterBlobView: UIView {
private var displayLink: SharedDisplayLinkDriver.Link? private var displayLink: SharedDisplayLinkDriver.Link?
private var primarySize = AnimatableProperty<CGFloat>(value: 0.63) 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 primaryRedness = AnimatableProperty<CGFloat>(value: 0.0)
private var primaryCornerRadius = AnimatableProperty<CGFloat>(value: 0.63) private var primaryCornerRadius = AnimatableProperty<CGFloat>(value: 0.63)
private var secondarySize = AnimatableProperty<CGFloat>(value: 0.34) 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 var secondaryRedness = AnimatableProperty<CGFloat>(value: 0.0)
private(set) var state: BlobState = .generic private(set) var state: BlobState = .generic
@ -309,22 +305,42 @@ final class ShutterBlobView: UIView {
self.tick() self.tick()
} }
func updatePrimaryOffset(_ offset: CGFloat, transition: Transition = .immediate) { func updatePrimaryOffsetX(_ offset: CGFloat, transition: Transition = .immediate) {
guard self.frame.height > 0.0 else { guard self.frame.height > 0.0 else {
return return
} }
let mappedOffset = offset / self.frame.height * 2.0 let mappedOffset = offset / self.frame.height * 2.0
self.primaryOffset.update(value: mappedOffset, transition: transition) self.primaryOffsetX.update(value: mappedOffset, transition: transition)
self.tick() 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 { guard self.frame.height > 0.0 else {
return return
} }
let mappedOffset = offset / self.frame.height * 2.0 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() self.tick()
} }
@ -332,11 +348,13 @@ final class ShutterBlobView: UIView {
private func updateAnimations() { private func updateAnimations() {
let properties = [ let properties = [
self.primarySize, self.primarySize,
self.primaryOffset, self.primaryOffsetX,
self.primaryOffsetY,
self.primaryRedness, self.primaryRedness,
self.primaryCornerRadius, self.primaryCornerRadius,
self.secondarySize, self.secondarySize,
self.secondaryOffset, self.secondaryOffsetX,
self.secondaryOffsetY,
self.secondaryRedness self.secondaryRedness
] ]
@ -407,20 +425,31 @@ final class ShutterBlobView: UIView {
var resolution = simd_uint2(UInt32(drawableSize.width), UInt32(drawableSize.height)) var resolution = simd_uint2(UInt32(drawableSize.width), UInt32(drawableSize.height))
renderEncoder.setFragmentBytes(&resolution, length: MemoryLayout<simd_uint2>.size * 2, index: 0) 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.primarySize.presentationValue),
Float(self.primaryOffset.presentationValue),
Float(self.primaryRedness.presentationValue), Float(self.primaryRedness.presentationValue),
Float(self.primaryCornerRadius.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.secondarySize.presentationValue),
Float(self.secondaryOffset.presentationValue),
Float(self.secondaryRedness.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.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 6, instanceCount: 1)
renderEncoder.endEncoding() renderEncoder.endEncoding()

View File

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

View File

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

View File

@ -20,6 +20,8 @@ func composerEntitiesForDrawingEntity(account: Account, entity: DrawingEntity, c
content = .file(file) content = .file(file)
case let .image(image): case let .image(image):
content = .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)] 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]) { } 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 { public enum Content {
case file(TelegramMediaFile) case file(TelegramMediaFile)
case image(UIImage) case image(UIImage)
case video(String)
var file: TelegramMediaFile? { var file: TelegramMediaFile? {
if case let .file(file) = self { if case let .file(file) = self {
@ -90,7 +93,10 @@ private class MediaEditorComposerStickerEntity: MediaEditorComposerEntity {
var source: AnimatedStickerNodeSource? var source: AnimatedStickerNodeSource?
var frameSource = Promise<QueueLocalObject<AnimatedStickerDirectFrameSource>?>() var frameSource = Promise<QueueLocalObject<AnimatedStickerDirectFrameSource>?>()
var videoFrameSource = Promise<QueueLocalObject<VideoStickerDirectFrameSource>?>() var videoFrameSource = Promise<QueueLocalObject<VideoStickerDirectFrameSource>?>()
var isVideo = false var isVideoSticker = false
var assetReader: AVAssetReader?
var videoOutput: AVAssetReaderTrackOutput?
var frameCount: Int? var frameCount: Int?
var frameRate: Int? var frameRate: Int?
@ -118,9 +124,9 @@ private class MediaEditorComposerStickerEntity: MediaEditorComposerEntity {
case let .file(file): case let .file(file):
if file.isAnimatedSticker || file.isVideoSticker || file.mimeType == "video/webm" { if file.isAnimatedSticker || file.isVideoSticker || file.mimeType == "video/webm" {
self.isAnimated = true 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) let pathPrefix = account.postbox.mediaBox.shortLivedResourceCachePathPrefix(file.resource.id)
if let source = self.source { if let source = self.source {
let dimensions = file.dimensions ?? PixelDimensions(width: 512, height: 512) 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]) { if let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: [.mappedRead]) {
let queue = strongSelf.queue let queue = strongSelf.queue
if strongSelf.isVideo { if strongSelf.isVideoSticker {
let frameSource = QueueLocalObject<VideoStickerDirectFrameSource>(queue: queue, generate: { 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)! 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): case let .image(image):
self.isAnimated = false self.isAnimated = false
self.imagePromise.set(.single(image)) 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() self.disposables.dispose()
} }
var tested = false private var circleMaskFilter: CIFilter?
func image(for time: CMTime, frameRate: Float, completion: @escaping (CIImage?) -> Void) { 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) let currentTime = CMTimeGetSeconds(time)
var tintColor: UIColor? var tintColor: UIColor?
@ -262,7 +332,7 @@ private class MediaEditorComposerStickerEntity: MediaEditorComposerEntity {
} }
} }
if self.isVideo { if self.isVideoSticker {
self.disposables.add((self.videoFrameSource.get() self.disposables.add((self.videoFrameSource.get()
|> take(1) |> take(1)
|> deliverOn(self.queue)).start(next: { [weak self] frameSource in |> 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]) 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) } self.renderPasses.forEach { $0.setup(device: device, library: library) }
} }
var renderPassedEnabled = true
func renderFrame() { func renderFrame() {
let device: MTLDevice? let device: MTLDevice?
if let renderTarget = self.renderTarget { if let renderTarget = self.renderTarget {
@ -181,9 +183,11 @@ final class MediaEditorRenderer: TextureConsumer {
return return
} }
for renderPass in self.renderPasses { if self.renderPassedEnabled {
if let nextTexture = renderPass.process(input: texture, device: device, commandBuffer: commandBuffer) { for renderPass in self.renderPasses {
texture = nextTexture if let nextTexture = renderPass.process(input: texture, device: device, commandBuffer: commandBuffer) {
texture = nextTexture
}
} }
} }
self.finalTexture = texture self.finalTexture = texture

View File

@ -37,6 +37,7 @@ swift_library(
"//submodules/Components/BlurredBackgroundComponent", "//submodules/Components/BlurredBackgroundComponent",
"//submodules/AvatarNode", "//submodules/AvatarNode",
"//submodules/TelegramUI/Components/ShareWithPeersScreen", "//submodules/TelegramUI/Components/ShareWithPeersScreen",
"//submodules/TelegramUI/Components/CameraButtonComponent",
], ],
visibility = [ visibility = [
"//visibility:public", "//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 Foundation
import UIKit import UIKit
import CoreServices
import Display import Display
import AsyncDisplayKit import AsyncDisplayKit
import ComponentFlow import ComponentFlow
@ -23,6 +24,7 @@ import ShareWithPeersScreen
import PresentationDataUtils import PresentationDataUtils
import ContextUI import ContextUI
import BundleIconComponent import BundleIconComponent
import CameraButtonComponent
enum DrawingScreenType { enum DrawingScreenType {
case drawing case drawing
@ -41,6 +43,7 @@ final class MediaEditorScreenComponent: Component {
let isDisplayingTool: Bool let isDisplayingTool: Bool
let isInteractingWithEntities: Bool let isInteractingWithEntities: Bool
let isSavingAvailable: Bool let isSavingAvailable: Bool
let hasAppeared: Bool
let isDismissing: Bool let isDismissing: Bool
let mediaEditor: MediaEditor? let mediaEditor: MediaEditor?
let privacy: MediaEditorResultPrivacy let privacy: MediaEditorResultPrivacy
@ -54,6 +57,7 @@ final class MediaEditorScreenComponent: Component {
isDisplayingTool: Bool, isDisplayingTool: Bool,
isInteractingWithEntities: Bool, isInteractingWithEntities: Bool,
isSavingAvailable: Bool, isSavingAvailable: Bool,
hasAppeared: Bool,
isDismissing: Bool, isDismissing: Bool,
mediaEditor: MediaEditor?, mediaEditor: MediaEditor?,
privacy: MediaEditorResultPrivacy, privacy: MediaEditorResultPrivacy,
@ -66,6 +70,7 @@ final class MediaEditorScreenComponent: Component {
self.isDisplayingTool = isDisplayingTool self.isDisplayingTool = isDisplayingTool
self.isInteractingWithEntities = isInteractingWithEntities self.isInteractingWithEntities = isInteractingWithEntities
self.isSavingAvailable = isSavingAvailable self.isSavingAvailable = isSavingAvailable
self.hasAppeared = hasAppeared
self.isDismissing = isDismissing self.isDismissing = isDismissing
self.mediaEditor = mediaEditor self.mediaEditor = mediaEditor
self.privacy = privacy self.privacy = privacy
@ -88,6 +93,9 @@ final class MediaEditorScreenComponent: Component {
if lhs.isSavingAvailable != rhs.isSavingAvailable { if lhs.isSavingAvailable != rhs.isSavingAvailable {
return false return false
} }
if lhs.hasAppeared != rhs.hasAppeared {
return false
}
if lhs.isDismissing != rhs.isDismissing { if lhs.isDismissing != rhs.isDismissing {
return false return false
} }
@ -175,6 +183,8 @@ final class MediaEditorScreenComponent: Component {
deinit { deinit {
self.playerStateDisposable?.dispose() self.playerStateDisposable?.dispose()
} }
var muteDidChange = false
} }
func makeState() -> State { func makeState() -> State {
@ -239,7 +249,7 @@ final class MediaEditorScreenComponent: Component {
case camera case camera
case gallery case gallery
} }
func animateIn(from source: TransitionAnimationSource) { func animateIn(from source: TransitionAnimationSource, completion: @escaping () -> Void = {}) {
let buttons = [ let buttons = [
self.drawButton, self.drawButton,
self.textButton, 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) view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2, delay: 0.0)
}) })
delay += 0.03 delay += 0.03
Queue.mainQueue().after(0.45, completion)
} }
} }
@ -452,11 +464,28 @@ final class MediaEditorScreenComponent: Component {
self.component = component self.component = component
self.state = state self.state = state
let isTablet: Bool
if case .regular = environment.metrics.widthClass {
isTablet = true
} else {
isTablet = false
}
let openDrawing = component.openDrawing let openDrawing = component.openDrawing
let openTools = component.openTools let openTools = component.openTools
let buttonSideInset: CGFloat = 10.0 let buttonSideInset: CGFloat
let buttonBottomInset: CGFloat = 8.0 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( let cancelButtonSize = self.cancelButton.update(
transition: transition, 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) 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( let drawButtonSize = self.drawButton.update(
transition: transition, transition: transition,
component: AnyComponent(Button( component: AnyComponent(Button(
@ -549,7 +588,7 @@ final class MediaEditorScreenComponent: Component {
containerSize: CGSize(width: 40.0, height: 40.0) containerSize: CGSize(width: 40.0, height: 40.0)
) )
let drawButtonFrame = CGRect( 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 size: drawButtonSize
) )
if let drawButtonView = self.drawButton.view { if let drawButtonView = self.drawButton.view {
@ -576,7 +615,7 @@ final class MediaEditorScreenComponent: Component {
containerSize: CGSize(width: 40.0, height: 40.0) containerSize: CGSize(width: 40.0, height: 40.0)
) )
let textButtonFrame = CGRect( 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 size: textButtonSize
) )
if let textButtonView = self.textButton.view { if let textButtonView = self.textButton.view {
@ -603,7 +642,7 @@ final class MediaEditorScreenComponent: Component {
containerSize: CGSize(width: 40.0, height: 40.0) containerSize: CGSize(width: 40.0, height: 40.0)
) )
let stickerButtonFrame = CGRect( 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 size: stickerButtonSize
) )
if let stickerButtonView = self.stickerButton.view { if let stickerButtonView = self.stickerButton.view {
@ -630,7 +669,7 @@ final class MediaEditorScreenComponent: Component {
containerSize: CGSize(width: 40.0, height: 40.0) containerSize: CGSize(width: 40.0, height: 40.0)
) )
let toolsButtonFrame = CGRect( 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 size: toolsButtonSize
) )
if let toolsButtonView = self.toolsButton.view { if let toolsButtonView = self.toolsButton.view {
@ -719,6 +758,14 @@ final class MediaEditorScreenComponent: Component {
timeoutSelected = timeout != nil 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 self.inputPanel.parentState = state
let inputPanelSize = self.inputPanel.update( let inputPanelSize = self.inputPanel.update(
transition: transition, transition: transition,
@ -765,7 +812,7 @@ final class MediaEditorScreenComponent: Component {
bottomInset: 0.0 bottomInset: 0.0
)), )),
environment: {}, 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)) let fadeTransition = Transition(animation: .curve(duration: 0.3, curve: .easeInOut))
@ -802,7 +849,7 @@ final class MediaEditorScreenComponent: Component {
inputPanelBottomInset = environment.inputHeight - environment.safeInsets.bottom inputPanelBottomInset = environment.inputHeight - environment.safeInsets.bottom
inputPanelOffset = inputPanelBottomInset 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 let inputPanelView = self.inputPanel.view {
if inputPanelView.superview == nil { if inputPanelView.superview == nil {
self.addSubview(inputPanelView) self.addSubview(inputPanelView)
@ -839,6 +886,7 @@ final class MediaEditorScreenComponent: Component {
component: AnyComponent(Button( component: AnyComponent(Button(
content: AnyComponent( content: AnyComponent(
PrivacyButtonComponent( PrivacyButtonComponent(
backgroundColor: isTablet ? UIColor(rgb: 0x303030, alpha: 0.5) : UIColor(white: 0.0, alpha: 0.5),
icon: UIImage(bundleImageName: "Media Editor/Recipient")!, icon: UIImage(bundleImageName: "Media Editor/Recipient")!,
text: privacyText text: privacyText
) )
@ -852,10 +900,18 @@ final class MediaEditorScreenComponent: Component {
environment: {}, environment: {},
containerSize: CGSize(width: 44.0, height: 44.0) containerSize: CGSize(width: 44.0, height: 44.0)
) )
let privacyButtonFrame = CGRect( let privacyButtonFrame: CGRect
origin: CGPoint(x: 16.0, y: environment.safeInsets.top + 20.0 - inputPanelOffset), if isTablet {
size: privacyButtonSize 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 let privacyButtonView = self.privacyButton.view {
if privacyButtonView.superview == nil { if privacyButtonView.superview == nil {
self.addSubview(privacyButtonView) 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) transition.setAlpha(view: privacyButtonView, alpha: displayTopButtons && !component.isDismissing && !component.isInteractingWithEntities ? 1.0 : 0.0)
} }
let saveButtonSize = self.saveButton.update( let saveContentComponent: AnyComponentWithIdentity<Empty>
transition: transition, if component.hasAppeared {
component: AnyComponent(Button( saveContentComponent = AnyComponentWithIdentity(
content: AnyComponent( id: "animatedIcon",
component: AnyComponent(
LottieAnimationComponent( LottieAnimationComponent(
animation: LottieAnimationComponent.AnimationItem( animation: LottieAnimationComponent.AnimationItem(
name: "anim_storysave", name: "anim_storysave",
@ -877,9 +934,26 @@ final class MediaEditorScreenComponent: Component {
range: nil range: nil
), ),
colors: ["__allcolors__": .white], colors: ["__allcolors__": .white],
size: CGSize(width: 33.0, height: 33.0) size: CGSize(width: 30.0, height: 30.0)
).tagged(saveButtonTag) ).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 action: { [weak self] in
if let view = self?.saveButton.findTaggedView(tag: saveButtonTag) as? LottieAnimationComponent.View { if let view = self?.saveButton.findTaggedView(tag: saveButtonTag) as? LottieAnimationComponent.View {
view.playOnce() view.playOnce()
@ -916,22 +990,42 @@ final class MediaEditorScreenComponent: Component {
if let playerState = state.playerState, playerState.hasAudio { if let playerState = state.playerState, playerState.hasAudio {
let isVideoMuted = component.mediaEditor?.values.videoIsMuted ?? false let isVideoMuted = component.mediaEditor?.values.videoIsMuted ?? false
let muteButtonSize = self.muteButton.update(
transition: transition, let muteContentComponent: AnyComponentWithIdentity<Empty>
component: AnyComponent(Button( if component.hasAppeared {
content: AnyComponent( muteContentComponent = AnyComponentWithIdentity(
id: "animatedIcon",
component: AnyComponent(
LottieAnimationComponent( LottieAnimationComponent(
animation: LottieAnimationComponent.AnimationItem( animation: LottieAnimationComponent.AnimationItem(
name: "anim_storymute", 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) range: isVideoMuted ? (0.0, 0.5) : (0.5, 1.0)
), ),
colors: ["__allcolors__": .white], colors: ["__allcolors__": .white],
size: CGSize(width: 33.0, height: 33.0) size: CGSize(width: 30.0, height: 30.0)
).tagged(muteButtonTag) ).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 action: { [weak self, weak state] in
if let self, let mediaEditor = self.component?.mediaEditor { if let self, let mediaEditor = self.component?.mediaEditor {
state?.muteDidChange = true
let isMuted = !mediaEditor.values.videoIsMuted let isMuted = !mediaEditor.values.videoIsMuted
mediaEditor.setVideoIsMuted(isMuted) mediaEditor.setVideoIsMuted(isMuted)
state?.updated() state?.updated()
@ -1106,7 +1200,7 @@ final class MediaEditorScreenComponent: Component {
private let storyDimensions = CGSize(width: 1080.0, height: 1920.0) private let storyDimensions = CGSize(width: 1080.0, height: 1920.0)
private let storyMaxVideoDuration: Double = 60.0 private let storyMaxVideoDuration: Double = 60.0
public final class MediaEditorScreen: ViewController { public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate {
public enum TransitionIn { public enum TransitionIn {
public final class GalleryTransitionIn { public final class GalleryTransitionIn {
public weak var sourceView: UIView? public weak var sourceView: UIView?
@ -1166,6 +1260,7 @@ public final class MediaEditorScreen: ViewController {
private var wasPlaying = false private var wasPlaying = false
private let backgroundDimView: UIView private let backgroundDimView: UIView
fileprivate let containerView: UIView
fileprivate let componentHost: ComponentView<ViewControllerComponentContainer.Environment> fileprivate let componentHost: ComponentView<ViewControllerComponentContainer.Environment>
fileprivate let storyPreview: ComponentView<Empty> fileprivate let storyPreview: ComponentView<Empty>
fileprivate let toolValue: ComponentView<Empty> fileprivate let toolValue: ComponentView<Empty>
@ -1190,6 +1285,8 @@ public final class MediaEditorScreen: ViewController {
private var isDisplayingTool = false private var isDisplayingTool = false
private var isInteractingWithEntities = false private var isInteractingWithEntities = false
private var isEnhancing = false private var isEnhancing = false
private var hasAppeared = false
private var isDismissing = false private var isDismissing = false
private var dismissOffset: CGFloat = 0.0 private var dismissOffset: CGFloat = 0.0
private var isDismissed = false private var isDismissed = false
@ -1207,6 +1304,9 @@ public final class MediaEditorScreen: ViewController {
self.backgroundDimView.isHidden = true self.backgroundDimView.isHidden = true
self.backgroundDimView.backgroundColor = .black self.backgroundDimView.backgroundColor = .black
self.containerView = UIView()
self.containerView.clipsToBounds = true
self.componentHost = ComponentView<ViewControllerComponentContainer.Environment>() self.componentHost = ComponentView<ViewControllerComponentContainer.Environment>()
self.storyPreview = ComponentView<Empty>() self.storyPreview = ComponentView<Empty>()
self.toolValue = ComponentView<Empty>() self.toolValue = ComponentView<Empty>()
@ -1241,7 +1341,8 @@ public final class MediaEditorScreen: ViewController {
self.backgroundColor = .clear self.backgroundColor = .clear
self.view.addSubview(self.backgroundDimView) 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.gradientView)
self.previewContainerView.addSubview(self.entitiesContainerView) self.previewContainerView.addSubview(self.entitiesContainerView)
self.entitiesContainerView.addSubview(self.entitiesView) self.entitiesContainerView.addSubview(self.entitiesView)
@ -1276,7 +1377,7 @@ public final class MediaEditorScreen: ViewController {
areUnicodeEmojiEnabled: true, areUnicodeEmojiEnabled: true,
areCustomEmojiEnabled: true, areCustomEmojiEnabled: true,
chatPeerId: controller.context.account.peerId, chatPeerId: controller.context.account.peerId,
hasSearch: false, hasSearch: true,
forceHasPremium: true forceHasPremium: true
) )
@ -1287,7 +1388,7 @@ public final class MediaEditorScreen: ViewController {
stickerNamespaces: [Namespaces.ItemCollection.CloudStickerPacks], stickerNamespaces: [Namespaces.ItemCollection.CloudStickerPacks],
stickerOrderedItemListCollectionIds: [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers, Namespaces.OrderedItemList.CloudAllPremiumStickers], stickerOrderedItemListCollectionIds: [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers, Namespaces.OrderedItemList.CloudAllPremiumStickers],
chatPeerId: controller.context.account.peerId, chatPeerId: controller.context.account.peerId,
hasSearch: false, hasSearch: true,
hasTrending: true, hasTrending: true,
forceHasPremium: true forceHasPremium: true
) )
@ -1371,7 +1472,7 @@ public final class MediaEditorScreen: ViewController {
} }
self.entitiesView.add(mediaEntity, announce: false) 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 image = generateImage(CGSize(width: additionalImage.size.width, height: additionalImage.size.width), contextGenerator: { size, context in
let bounds = CGRect(origin: .zero, size: size) let bounds = CGRect(origin: .zero, size: size)
context.clear(bounds) context.clear(bounds)
@ -1386,8 +1487,15 @@ public final class MediaEditorScreen: ViewController {
imageEntity.referenceDrawingSize = storyDimensions imageEntity.referenceDrawingSize = storyDimensions
imageEntity.scale = 1.49 imageEntity.scale = 1.49
imageEntity.mirrored = true 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) self.entitiesView.add(imageEntity, announce: false)
} else if case let .video(_, _, additionalVideoPath, additionalVideoImage, _, position) = subject, let additionalVideoPath {
let videoEntity = DrawingStickerEntity(content: .video(additionalVideoPath, additionalVideoImage))
videoEntity.referenceDrawingSize = storyDimensions
videoEntity.scale = 1.49
videoEntity.mirrored = true
videoEntity.position = position.getPosition(storyDimensions)
self.entitiesView.add(videoEntity, announce: false)
} }
let initialPosition = mediaEntity.position let initialPosition = mediaEntity.position
@ -1451,6 +1559,31 @@ public final class MediaEditorScreen: ViewController {
} }
}) })
self.mediaEditor = mediaEditor 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() { override func didLoad() {
@ -1679,13 +1812,19 @@ public final class MediaEditorScreen: ViewController {
} }
func animateIn() { 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 { if let transitionIn = self.controller?.transitionIn {
switch transitionIn { switch transitionIn {
case .camera: case .camera:
if let view = self.componentHost.view as? MediaEditorScreenComponent.View { 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) self.setupTransitionImage(transitionImage)
} }
case let .gallery(transitionIn): case let .gallery(transitionIn):
@ -1703,7 +1842,9 @@ public final class MediaEditorScreen: ViewController {
let duration: Double = 0.4 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.animateScale(from: sourceScale, to: 1.0, duration: duration, timingFunction: kCAMediaTimingFunctionSpring)
self.previewContainerView.layer.animateBounds(from: CGRect(origin: CGPoint(x: 0.0, y: (self.previewContainerView.bounds.height - self.previewContainerView.bounds.width * sourceAspectRatio) / 2.0), size: CGSize(width: self.previewContainerView.bounds.width, height: self.previewContainerView.bounds.width * sourceAspectRatio)), to: self.previewContainerView.bounds, duration: duration, timingFunction: kCAMediaTimingFunctionSpring) self.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 { } else {
if let view = self.componentHost.view as? MediaEditorScreenComponent.View { 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) snapshotView.layer.anchorPoint = CGPoint(x: 0.0, y: 0.5)
let snapshotScale = self.previewContainerView.bounds.width / snapshotView.frame.width let snapshotScale = self.previewContainerView.bounds.width / snapshotView.frame.width
snapshotView.center = CGPoint(x: 0.0, y: self.previewContainerView.bounds.height / 2.0) snapshotView.center = CGPoint(x: 0.0, y: self.previewContainerView.bounds.height / 2.0)
snapshotView.layer.transform = CATransform3DMakeScale(snapshotScale, snapshotScale, 1.0)
let snapshotTransform = CATransform3DMakeScale(0.001, snapshotScale, 1.0) snapshotView.alpha = 0.0
//snapshotTransform.m34 = 1.0 / -500
//snapshotTransform = CATransform3DRotate(snapshotTransform, -90.0 * .pi / 180.0, 0.0, 1.0, 0.0)
let targetTransform = CATransform3DMakeScale(snapshotScale, snapshotScale, 1.0)
//snapshotTransform
//targetTransform = CATransform3DRotate(targetTransform, 0.0, 0.0, 1.0, 0.0)
snapshotView.layer.transform = snapshotTransform
Queue.mainQueue().after(0.15) { Queue.mainQueue().after(0.15) {
snapshotView.layer.transform = targetTransform snapshotView.alpha = 1.0
snapshotView.layer.animate(from: NSValue(caTransform3D: snapshotTransform), to: NSValue(caTransform3D: targetTransform), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25) snapshotView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
} }
self.previewContainerView.addSubview(snapshotView) self.previewContainerView.addSubview(snapshotView)
@ -2036,6 +2170,23 @@ public final class MediaEditorScreen: ViewController {
self.controller?.present(tooltipController, in: .current) 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? { override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let result = super.hitTest(point, with: event) let result = super.hitTest(point, with: event)
if result == self.componentHost.view { if result == self.componentHost.view {
@ -2045,24 +2196,44 @@ public final class MediaEditorScreen: ViewController {
return result return result
} }
func requestUpdate(transition: Transition = .immediate) { func requestUpdate(hasAppeared: Bool = false, transition: Transition = .immediate) {
if let layout = self.validLayout { if let layout = self.validLayout {
self.containerLayoutUpdated(layout: layout, transition: transition) self.containerLayoutUpdated(layout: layout, hasAppeared: hasAppeared, transition: transition)
} }
} }
private var drawingScreen: DrawingScreen? 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 { guard let controller = self.controller, !self.isDismissed else {
return return
} }
let isFirstTime = self.validLayout == nil let isFirstTime = self.validLayout == nil
self.validLayout = layout self.validLayout = layout
let previewSize = CGSize(width: layout.size.width, height: floorToScreenPixels(layout.size.width * 1.77778)) let isTablet: Bool
let topInset: CGFloat = (layout.statusBarHeight ?? 0.0) + 12.0 //floorToScreenPixels(layout.size.height - previewSize.height) / 2.0 if case .regular = layout.metrics.widthClass {
isTablet = true
} else {
isTablet = false
}
let topInset: CGFloat = (layout.statusBarHeight ?? 0.0) + 12.0
let previewSize: CGSize
if isTablet {
let previewHeight = layout.size.height - topInset - 75.0
previewSize = CGSize(width: floorToScreenPixels(previewHeight / 1.77778), height: previewHeight)
} else {
previewSize = CGSize(width: layout.size.width, height: floorToScreenPixels(layout.size.width * 1.77778))
}
let bottomInset = layout.size.height - previewSize.height - topInset 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( let environment = ViewControllerComponentContainer.Environment(
statusBarHeight: layout.statusBarHeight ?? 0.0, statusBarHeight: layout.statusBarHeight ?? 0.0,
navigationHeight: 0.0, navigationHeight: 0.0,
@ -2072,7 +2243,7 @@ public final class MediaEditorScreen: ViewController {
bottom: bottomInset, bottom: bottomInset,
right: layout.safeInsets.right right: layout.safeInsets.right
), ),
inputHeight: layout.inputHeight ?? 0.0, inputHeight: inputHeight,
metrics: layout.metrics, metrics: layout.metrics,
deviceMetrics: layout.deviceMetrics, deviceMetrics: layout.deviceMetrics,
orientation: nil, orientation: nil,
@ -2085,6 +2256,10 @@ public final class MediaEditorScreen: ViewController {
} }
) )
if hasAppeared && !self.hasAppeared {
self.hasAppeared = hasAppeared
}
let componentSize = self.componentHost.update( let componentSize = self.componentHost.update(
transition: transition, transition: transition,
component: AnyComponent( component: AnyComponent(
@ -2093,6 +2268,7 @@ public final class MediaEditorScreen: ViewController {
isDisplayingTool: self.isDisplayingTool, isDisplayingTool: self.isDisplayingTool,
isInteractingWithEntities: self.isInteractingWithEntities, isInteractingWithEntities: self.isInteractingWithEntities,
isSavingAvailable: controller.isSavingAvailable, isSavingAvailable: controller.isSavingAvailable,
hasAppeared: self.hasAppeared,
isDismissing: self.isDismissing, isDismissing: self.isDismissing,
mediaEditor: self.mediaEditor, mediaEditor: self.mediaEditor,
privacy: controller.state.privacy, privacy: controller.state.privacy,
@ -2113,14 +2289,24 @@ public final class MediaEditorScreen: ViewController {
case .sticker: case .sticker:
let controller = StickerPickerScreen(context: self.context, inputData: self.stickerPickerInputData.get()) let controller = StickerPickerScreen(context: self.context, inputData: self.stickerPickerInputData.get())
controller.completion = { [weak self] file in controller.completion = { [weak self] file in
if let self, let file { if let self {
let stickerEntity = DrawingStickerEntity(content: .file(file)) if let file {
self.interaction?.insertEntity(stickerEntity) let stickerEntity = DrawingStickerEntity(content: .file(file))
self.interaction?.insertEntity(stickerEntity)
self.controller?.isSavingAvailable = true self.controller?.isSavingAvailable = true
self.controller?.requestLayout(transition: .immediate) 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) self.controller?.present(controller, in: .current)
return return
case .text: case .text:
@ -2189,12 +2375,12 @@ public final class MediaEditorScreen: ViewController {
environment: { environment: {
environment environment
}, },
forceUpdate: forceUpdate || animateOut, forceUpdate: forceUpdate,
containerSize: layout.size containerSize: layout.size
) )
if let componentView = self.componentHost.view { if let componentView = self.componentHost.view {
if componentView.superview == nil { if componentView.superview == nil {
self.view.insertSubview(componentView, at: 3) self.containerView.addSubview(componentView)
componentView.clipsToBounds = true componentView.clipsToBounds = true
} }
transition.setFrame(view: componentView, frame: CGRect(origin: CGPoint(x: 0.0, y: self.dismissOffset), size: componentSize)) 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) transition.setAlpha(view: self.backgroundDimView, alpha: self.isDismissing ? 0.0 : 1.0)
var bottomInputOffset: CGFloat = 0.0 var bottomInputOffset: CGFloat = 0.0
if let inputHeight = layout.inputHeight, inputHeight > 0.0 { if inputHeight > 0.0 {
if self.entitiesView.selectedEntityView != nil || self.isDisplayingTool { if self.stickerScreen == nil {
bottomInputOffset = inputHeight / 2.0 if self.entitiesView.selectedEntityView != nil || self.isDisplayingTool {
} else { bottomInputOffset = inputHeight / 2.0
bottomInputOffset = inputHeight - bottomInset - 17.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) transition.setFrame(view: self.previewContainerView, frame: previewFrame)
let entitiesViewScale = previewSize.width / storyDimensions.width let entitiesViewScale = previewSize.width / storyDimensions.width
self.entitiesContainerView.transform = CGAffineTransformMakeScale(entitiesViewScale, entitiesViewScale) self.entitiesContainerView.transform = CGAffineTransformMakeScale(entitiesViewScale, entitiesViewScale)
@ -2278,15 +2469,35 @@ public final class MediaEditorScreen: ViewController {
return self.displayNode as! Node return self.displayNode as! Node
} }
public enum PIPPosition {
case topLeft
case topRight
case bottomLeft
case bottomRight
func getPosition(_ size: CGSize) -> CGPoint {
switch self {
case .topLeft:
return CGPoint(x: 224.0, y: 477.0)
case .topRight:
return CGPoint(x: size.width - 224.0, y: 477.0)
case .bottomLeft:
return CGPoint(x: 224.0, y: size.height - 477.0)
case .bottomRight:
return CGPoint(x: size.width - 224.0, y: size.height - 477.0)
}
}
}
public enum Subject { public enum Subject {
case image(UIImage, PixelDimensions, UIImage?) case image(UIImage, PixelDimensions, UIImage?, PIPPosition)
case video(String, UIImage?, PixelDimensions) case video(String, UIImage?, String?, UIImage?, PixelDimensions, PIPPosition)
case asset(PHAsset) case asset(PHAsset)
case draft(MediaEditorDraft, Int64?) case draft(MediaEditorDraft, Int64?)
var dimensions: PixelDimensions { var dimensions: PixelDimensions {
switch self { switch self {
case let .image(_, dimensions, _), let .video(_, _, dimensions): case let .image(_, dimensions, _, _), let .video(_, _, _, _, dimensions, _):
return dimensions return dimensions
case let .asset(asset): case let .asset(asset):
return PixelDimensions(width: Int32(asset.pixelWidth), height: Int32(asset.pixelHeight)) return PixelDimensions(width: Int32(asset.pixelWidth), height: Int32(asset.pixelHeight))
@ -2297,9 +2508,9 @@ public final class MediaEditorScreen: ViewController {
var editorSubject: MediaEditor.Subject { var editorSubject: MediaEditor.Subject {
switch self { switch self {
case let .image(image, dimensions, _): case let .image(image, dimensions, _, _):
return .image(image, dimensions) return .image(image, dimensions)
case let .video(videoPath, transitionImage, dimensions): case let .video(videoPath, transitionImage, _, _, dimensions, _):
return .video(videoPath, transitionImage, dimensions) return .video(videoPath, transitionImage, dimensions)
case let .asset(asset): case let .asset(asset):
return .asset(asset) return .asset(asset)
@ -2310,9 +2521,9 @@ public final class MediaEditorScreen: ViewController {
var mediaContent: DrawingMediaEntity.Content { var mediaContent: DrawingMediaEntity.Content {
switch self { switch self {
case let .image(image, dimensions, _): case let .image(image, dimensions, _, _):
return .image(image, dimensions) return .image(image, dimensions)
case let .video(videoPath, _, dimensions): case let .video(videoPath, _, _, _, dimensions, _):
return .video(videoPath, dimensions) return .video(videoPath, dimensions)
case let .asset(asset): case let .asset(asset):
return .asset(asset) return .asset(asset)
@ -2381,6 +2592,9 @@ public final class MediaEditorScreen: ViewController {
self.displayNode = Node(controller: self) self.displayNode = Node(controller: self)
super.displayNodeDidLoad() super.displayNodeDidLoad()
let dropInteraction = UIDropInteraction(delegate: self)
self.displayNode.view.addInteraction(dropInteraction)
} }
func openPrivacySettings() { func openPrivacySettings() {
@ -2735,9 +2949,9 @@ public final class MediaEditorScreen: ViewController {
} }
switch subject { switch subject {
case let .image(image, dimensions, _): case let .image(image, dimensions, _, _):
saveImageDraft(image, dimensions) saveImageDraft(image, dimensions)
case let .video(path, _, dimensions): case let .video(path, _, _, _, dimensions, _):
saveVideoDraft(path, dimensions) saveVideoDraft(path, dimensions)
case let .asset(asset): case let .asset(asset):
if asset.mediaType == .video { if asset.mediaType == .video {
@ -2780,6 +2994,7 @@ public final class MediaEditorScreen: ViewController {
self.dismissAllTooltips() self.dismissAllTooltips()
mediaEditor.seek(0.0, andPlay: false)
mediaEditor.invalidate() mediaEditor.invalidate()
self.node.entitiesView.invalidate() self.node.entitiesView.invalidate()
@ -2802,14 +3017,14 @@ public final class MediaEditorScreen: ViewController {
let videoResult: Result.VideoResult let videoResult: Result.VideoResult
let duration: Double let duration: Double
switch subject { switch subject {
case let .image(image, _, _): case let .image(image, _, _, _):
let tempImagePath = NSTemporaryDirectory() + "\(Int64.random(in: Int64.min ... Int64.max)).jpg" let tempImagePath = NSTemporaryDirectory() + "\(Int64.random(in: Int64.min ... Int64.max)).jpg"
if let data = image.jpegData(compressionQuality: 0.85) { if let data = image.jpegData(compressionQuality: 0.85) {
try? data.write(to: URL(fileURLWithPath: tempImagePath)) try? data.write(to: URL(fileURLWithPath: tempImagePath))
} }
videoResult = .imageFile(path: tempImagePath) videoResult = .imageFile(path: tempImagePath)
duration = 5.0 duration = 5.0
case let .video(path, _, _): case let .video(path, _, _, _, _, _):
videoResult = .videoFile(path: path) videoResult = .videoFile(path: path)
if let videoTrimRange = mediaEditor.values.videoTrimRange { if let videoTrimRange = mediaEditor.values.videoTrimRange {
duration = videoTrimRange.upperBound - videoTrimRange.lowerBound duration = videoTrimRange.upperBound - videoTrimRange.lowerBound
@ -2840,14 +3055,20 @@ public final class MediaEditorScreen: ViewController {
duration = 5.0 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() // makeEditorImageComposition(account: self.context.account, inputImage: image, dimensions: storyDimensions, values: mediaEditor.values, time: .zero, completion: { [weak self] coverImage in
Queue.mainQueue().justDispatch { // if let self {
finished() 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 { if case let .draft(draft, id) = subject, id == nil {
removeStoryDraft(engine: self.context.engine, path: draft.path, delete: true) 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> let exportSubject: Signal<MediaEditorVideoExport.Subject, NoError>
switch subject { switch subject {
case let .video(path, _, _): case let .video(path, _, _, _, _, _):
let asset = AVURLAsset(url: NSURL(fileURLWithPath: path) as URL) let asset = AVURLAsset(url: NSURL(fileURLWithPath: path) as URL)
exportSubject = .single(.video(asset)) exportSubject = .single(.video(asset))
case let .image(image, _, _): case let .image(image, _, _, _):
exportSubject = .single(.image(image)) exportSubject = .single(.image(image))
case let .asset(asset): case let .asset(asset):
exportSubject = Signal { subscriber in exportSubject = Signal { subscriber in
@ -3061,21 +3282,60 @@ public final class MediaEditorScreen: ViewController {
(self.displayNode as! Node).containerLayoutUpdated(layout: layout, transition: Transition(transition)) (self.displayNode as! Node).containerLayoutUpdated(layout: layout, transition: Transition(transition))
} }
@available(iOSApplicationExtension 11.0, iOS 11.0, *)
public func dropInteraction(_ interaction: UIDropInteraction, canHandle session: UIDropSession) -> Bool {
return session.hasItemsConforming(toTypeIdentifiers: [kUTTypeImage as String])
}
@available(iOSApplicationExtension 11.0, iOS 11.0, *)
public func dropInteraction(_ interaction: UIDropInteraction, sessionDidUpdate session: UIDropSession) -> UIDropProposal {
let operation: UIDropOperation
operation = .copy
return UIDropProposal(operation: operation)
}
@available(iOSApplicationExtension 11.0, iOS 11.0, *)
public func dropInteraction(_ interaction: UIDropInteraction, performDrop session: UIDropSession) {
session.loadObjects(ofClass: UIImage.self) { [weak self] imageItems in
guard let self else {
return
}
let images = imageItems as! [UIImage]
if images.count == 1, let image = images.first, max(image.size.width, image.size.height) > 1.0 {
self.node.interaction?.insertEntity(DrawingStickerEntity(content: .image(image)))
}
}
}
@available(iOSApplicationExtension 11.0, iOS 11.0, *)
public func dropInteraction(_ interaction: UIDropInteraction, sessionDidExit session: UIDropSession) {
}
@available(iOSApplicationExtension 11.0, iOS 11.0, *)
public func dropInteraction(_ interaction: UIDropInteraction, sessionDidEnd session: UIDropSession) {
}
} }
final class PrivacyButtonComponent: CombinedComponent { final class PrivacyButtonComponent: CombinedComponent {
let backgroundColor: UIColor
let icon: UIImage let icon: UIImage
let text: String let text: String
init( init(
backgroundColor: UIColor,
icon: UIImage, icon: UIImage,
text: String text: String
) { ) {
self.backgroundColor = backgroundColor
self.icon = icon self.icon = icon
self.text = text self.text = text
} }
static func ==(lhs: PrivacyButtonComponent, rhs: PrivacyButtonComponent) -> Bool { static func ==(lhs: PrivacyButtonComponent, rhs: PrivacyButtonComponent) -> Bool {
if lhs.backgroundColor != rhs.backgroundColor {
return false
}
if lhs.text != rhs.text { if lhs.text != rhs.text {
return false return false
} }
@ -3106,7 +3366,7 @@ final class PrivacyButtonComponent: CombinedComponent {
let backgroundSize = CGSize(width: text.size.width + 38.0, height: 30.0) let backgroundSize = CGSize(width: text.size.width + 38.0, height: 30.0)
let background = background.update( let background = background.update(
component: BlurredBackgroundComponent(color: UIColor(white: 0.0, alpha: 0.5)), component: BlurredBackgroundComponent(color: context.component.backgroundColor),
availableSize: backgroundSize, availableSize: backgroundSize,
transition: .immediate transition: .immediate
) )

View File

@ -323,15 +323,32 @@ private final class MediaToolsScreenComponent: Component {
self.component = component self.component = component
self.state = state 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 mediaEditor = (environment.controller() as? MediaToolsScreen)?.mediaEditor
let sectionUpdated = component.sectionUpdated 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 buttonSideInset: CGFloat
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 buttonBottomInset: CGFloat = 8.0 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( let cancelButtonSize = self.cancelButton.update(
transition: transition, transition: transition,
@ -396,6 +413,16 @@ private final class MediaToolsScreenComponent: Component {
transition.setFrame(view: doneButtonView, frame: doneButtonFrame) 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( let adjustmentsButtonSize = self.adjustmentsButton.update(
transition: transition, transition: transition,
component: AnyComponent(Button( component: AnyComponent(Button(
@ -412,7 +439,7 @@ private final class MediaToolsScreenComponent: Component {
containerSize: CGSize(width: 40.0, height: 40.0) containerSize: CGSize(width: 40.0, height: 40.0)
) )
let adjustmentsButtonFrame = CGRect( 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 size: adjustmentsButtonSize
) )
if let adjustmentsButtonView = self.adjustmentsButton.view { if let adjustmentsButtonView = self.adjustmentsButton.view {
@ -438,7 +465,7 @@ private final class MediaToolsScreenComponent: Component {
containerSize: CGSize(width: 40.0, height: 40.0) containerSize: CGSize(width: 40.0, height: 40.0)
) )
let tintButtonFrame = CGRect( 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 size: tintButtonSize
) )
if let tintButtonView = self.tintButton.view { if let tintButtonView = self.tintButton.view {
@ -464,7 +491,7 @@ private final class MediaToolsScreenComponent: Component {
containerSize: CGSize(width: 40.0, height: 40.0) containerSize: CGSize(width: 40.0, height: 40.0)
) )
let blurButtonFrame = CGRect( 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 size: blurButtonSize
) )
if let blurButtonView = self.blurButton.view { if let blurButtonView = self.blurButton.view {
@ -490,7 +517,7 @@ private final class MediaToolsScreenComponent: Component {
containerSize: CGSize(width: 40.0, height: 40.0) containerSize: CGSize(width: 40.0, height: 40.0)
) )
let curvesButtonFrame = CGRect( 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 size: curvesButtonSize
) )
if let curvesButtonView = self.curvesButton.view { if let curvesButtonView = self.curvesButton.view {
@ -640,10 +667,31 @@ private final class MediaToolsScreenComponent: Component {
} }
)), )),
environment: {}, 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: case .tint:
self.curvesState = nil self.curvesState = nil
optionsSize = self.toolOptions.update( optionsSize = self.toolOptions.update(
@ -676,10 +724,31 @@ private final class MediaToolsScreenComponent: Component {
} }
)), )),
environment: {}, 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: case .blur:
self.curvesState = nil self.curvesState = nil
optionsSize = self.toolOptions.update( optionsSize = self.toolOptions.update(
@ -706,7 +775,7 @@ private final class MediaToolsScreenComponent: Component {
} }
)), )),
environment: {}, environment: {},
containerSize: availableSize containerSize: previewContainerFrame.size
) )
let blurToolScreen: ComponentView<Empty> let blurToolScreen: ComponentView<Empty>
@ -764,7 +833,7 @@ private final class MediaToolsScreenComponent: Component {
internalState: internalState internalState: internalState
)), )),
environment: {}, environment: {},
containerSize: availableSize containerSize: previewContainerFrame.size
) )
let curvesToolScreen: ComponentView<Empty> let curvesToolScreen: ComponentView<Empty>
@ -918,8 +987,21 @@ public final class MediaToolsScreen: ViewController {
let isFirstTime = self.validLayout == nil let isFirstTime = self.validLayout == nil
self.validLayout = layout self.validLayout = layout
let previewSize = CGSize(width: layout.size.width, height: floorToScreenPixels(layout.size.width * 1.77778)) let isTablet: Bool
if case .regular = layout.metrics.widthClass {
isTablet = true
} else {
isTablet = false
}
let previewSize: CGSize
let topInset: CGFloat = (layout.statusBarHeight ?? 0.0) + 12.0 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 bottomInset = layout.size.height - previewSize.height - topInset
let environment = ViewControllerComponentContainer.Environment( 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( let componentSize = self.componentHost.update(
transition: transition, transition: transition,
component: AnyComponent( component: AnyComponent(

View File

@ -1,7 +1,7 @@
{ {
"images" : [ "images" : [
{ {
"filename" : "off shadow.pdf", "filename" : "off.pdf",
"idiom" : "universal" "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" : { "info" : {
"author" : "xcode", "author" : "xcode",
"version" : 1 "version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
} }
} }

View File

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

View File

@ -1,7 +1,7 @@
{ {
"images" : [ "images" : [
{ {
"filename" : "flash.pdf", "filename" : "magnifying.pdf",
"idiom" : "universal" "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) 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 { 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, completion: completion, dismissed: dismissed) return storyMediaPickerController(context: context, getSourceRect: getSourceRect, completion: completion, dismissed: dismissed)
} }
public func makeProxySettingsController(sharedContext: SharedAccountContext, account: UnauthorizedAccount) -> ViewController { public func makeProxySettingsController(sharedContext: SharedAccountContext, account: UnauthorizedAccount) -> ViewController {

View File

@ -27,6 +27,7 @@ import LocalMediaResources
import ShareWithPeersScreen import ShareWithPeersScreen
import ImageCompression import ImageCompression
import TextFormat import TextFormat
import UndoUI
private class DetailsChatPlaceholderNode: ASDisplayNode, NavigationDetailsPlaceholderNode { private class DetailsChatPlaceholderNode: ASDisplayNode, NavigationDetailsPlaceholderNode {
private var presentationData: PresentationData private var presentationData: PresentationData
@ -254,6 +255,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
controller.view.endEditing(true) controller.view.endEditing(true)
let context = self.context let context = self.context
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
var presentImpl: ((ViewController) -> Void)? var presentImpl: ((ViewController) -> Void)?
var returnToCameraImpl: (() -> Void)? var returnToCameraImpl: (() -> Void)?
@ -287,13 +289,25 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
completion: { result, resultTransition, dismissed in completion: { result, resultTransition, dismissed in
let subject: Signal<MediaEditorScreen.Subject?, NoError> = result let subject: Signal<MediaEditorScreen.Subject?, NoError> = result
|> map { value -> MediaEditorScreen.Subject? in |> 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 { switch value {
case .pendingImage: case .pendingImage:
return nil return nil
case let .image(image, additionalImage): case let .image(image, additionalImage, pipPosition):
return .image(image, PixelDimensions(image.size), additionalImage) return .image(image, PixelDimensions(image.size), additionalImage, editorPIPPosition(pipPosition))
case let .video(path, transitionImage, dimensions): case let .video(path, transitionImage, additionalPath, additionalTransitionImage, dimensions, pipPosition):
return .video(path, transitionImage, dimensions) return .video(path, transitionImage, additionalPath, additionalTransitionImage, dimensions, editorPIPPosition(pipPosition))
case let .asset(asset): case let .asset(asset):
return .asset(asset) return .asset(asset)
case let .draft(draft): case let .draft(draft):
@ -362,6 +376,17 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
Queue.mainQueue().after(0.2) { Queue.mainQueue().after(0.2) {
chatListController.updateStoryUploadProgress(nil) 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({}) commit({})
} }
} }
case let .video(content, _, values, duration, dimensions, caption): case let .video(content, image, values, duration, dimensions, caption):
let adjustments: VideoMediaResourceAdjustments let adjustments: VideoMediaResourceAdjustments
if let valuesData = try? JSONEncoder().encode(values) { if let valuesData = try? JSONEncoder().encode(values) {
let data = MemoryBuffer(data: valuesData) let data = MemoryBuffer(data: valuesData)
@ -451,6 +476,19 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
Queue.mainQueue().after(0.2) { Queue.mainQueue().after(0.2) {
chatListController.updateStoryUploadProgress(nil) 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)
}
} }
} }
}) })