mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Camera and editor improvements
This commit is contained in:
parent
0882817bed
commit
f1218abc9b
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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()
|
||||||
|
@ -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 {
|
||||||
|
@ -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) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
19
submodules/TelegramUI/Components/CameraButtonComponent/BUILD
Normal file
19
submodules/TelegramUI/Components/CameraButtonComponent/BUILD
Normal 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",
|
||||||
|
],
|
||||||
|
)
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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",
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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",
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
)
|
)
|
||||||
|
@ -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(
|
||||||
|
Binary file not shown.
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"images" : [
|
"images" : [
|
||||||
{
|
{
|
||||||
"filename" : "off shadow.pdf",
|
"filename" : "off.pdf",
|
||||||
"idiom" : "universal"
|
"idiom" : "universal"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
File diff suppressed because it is too large
Load Diff
378
submodules/TelegramUI/Images.xcassets/Camera/FlashOffIcon.imageset/off.pdf
vendored
Normal file
378
submodules/TelegramUI/Images.xcassets/Camera/FlashOffIcon.imageset/off.pdf
vendored
Normal 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
|
@ -8,5 +8,8 @@
|
|||||||
"info" : {
|
"info" : {
|
||||||
"author" : "xcode",
|
"author" : "xcode",
|
||||||
"version" : 1
|
"version" : 1
|
||||||
|
},
|
||||||
|
"properties" : {
|
||||||
|
"template-rendering-intent" : "template"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,5 +8,8 @@
|
|||||||
"info" : {
|
"info" : {
|
||||||
"author" : "xcode",
|
"author" : "xcode",
|
||||||
"version" : 1
|
"version" : 1
|
||||||
|
},
|
||||||
|
"properties" : {
|
||||||
|
"template-rendering-intent" : "template"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"images" : [
|
"images" : [
|
||||||
{
|
{
|
||||||
"filename" : "flash.pdf",
|
"filename" : "magnifying.pdf",
|
||||||
"idiom" : "universal"
|
"idiom" : "universal"
|
||||||
}
|
}
|
||||||
],
|
],
|
126
submodules/TelegramUI/Images.xcassets/Camera/ZoomIcon.imageset/magnifying.pdf
vendored
Normal file
126
submodules/TelegramUI/Images.xcassets/Camera/ZoomIcon.imageset/magnifying.pdf
vendored
Normal 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
|
12
submodules/TelegramUI/Images.xcassets/Media Editor/MuteIcon.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Media Editor/MuteIcon.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "speaker_30.pdf",
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
163
submodules/TelegramUI/Images.xcassets/Media Editor/MuteIcon.imageset/speaker_30.pdf
vendored
Normal file
163
submodules/TelegramUI/Images.xcassets/Media Editor/MuteIcon.imageset/speaker_30.pdf
vendored
Normal 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
|
12
submodules/TelegramUI/Images.xcassets/Media Editor/SaveIcon.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Media Editor/SaveIcon.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "squareandarrow_30.pdf",
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
96
submodules/TelegramUI/Images.xcassets/Media Editor/SaveIcon.imageset/squareandarrow_30.pdf
vendored
Normal file
96
submodules/TelegramUI/Images.xcassets/Media Editor/SaveIcon.imageset/squareandarrow_30.pdf
vendored
Normal 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
|
@ -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 {
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
Loading…
x
Reference in New Issue
Block a user